Skip to content

Use sequential executor in place of synchronized blocks for cached new release #4430

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Dec 9, 2022

Conversation

lfkellogg
Copy link
Contributor

@lfkellogg lfkellogg commented Dec 7, 2022

Continuing to remove synchronized blocks based on conversation on #4350.

This also uses an executor for more Task continuations.

@google-oss-bot
Copy link
Contributor

1 Warning
⚠️ Did you forget to add a changelog entry? (Add the 'no-changelog' label to the PR to silence this warning.)

Generated by 🚫 Danger

@google-oss-bot
Copy link
Contributor

google-oss-bot commented Dec 7, 2022

Coverage Report 1

Affected Products

  • firebase-appdistribution

    Overall coverage changed from ? (45c2d7c) to 79.52% (98ea42c) by ?.

    36 individual files with coverage change

    FilenameBase (45c2d7c)Merge (98ea42c)Diff
    AabUpdater.java?98.67%?
    ApkInstaller.java?96.88%?
    ApkUpdater.java?93.70%?
    AppDistributionReleaseImpl.java?100.00%?
    AppDistributionReleaseInternal.java?100.00%?
    AppIconSource.java?85.71%?
    AutoValue_AppDistributionReleaseImpl.java?65.45%?
    AutoValue_AppDistributionReleaseInternal.java?71.58%?
    AutoValue_TesterApiDisabledErrorDetails.java?29.41%?
    AutoValue_TesterApiDisabledErrorDetails_HelpLink.java?54.17%?
    AutoValue_UpdateProgressImpl.java?65.96%?
    ErrorMessages.java?0.00%?
    FirebaseAppDistributionExceptions.java?80.00%?
    FirebaseAppDistributionFileProvider.java?0.00%?
    FirebaseAppDistributionImpl.java?95.60%?
    FirebaseAppDistributionLifecycleNotifier.java?75.00%?
    FirebaseAppDistributionNotificationsManager.java?80.00%?
    FirebaseAppDistributionRegistrar.java?91.67%?
    FirebaseAppDistributionTesterApiClient.java?87.63%?
    HttpsUrlConnectionFactory.java?50.00%?
    InstallActivity.java?2.53%?
    LogWrapper.java?50.00%?
    NewReleaseFetcher.java?77.55%?
    PackageInfoUtils.java?42.86%?
    ReleaseIdentifier.java?88.00%?
    ReleaseUtils.java?83.33%?
    SequentialReference.java?100.00%?
    SignInResultActivity.java?0.00%?
    SignInStorage.java?42.86%?
    TaskCache.java?100.00%?
    TaskUtils.java?91.43%?
    TesterApiDisabledErrorDetails.java?93.75%?
    TesterApiHttpClient.java?89.19%?
    TesterSignInManager.java?93.62%?
    UpdateProgressImpl.java?100.00%?
    UpdateTaskImpl.java?76.00%?

Test Logs

  1. https://storage.googleapis.com/firebase-sdk-metric-reports/Onp7uSJlBD.html

@google-oss-bot
Copy link
Contributor

google-oss-bot commented Dec 7, 2022

Size Report 1

Affected Products

  • base

    TypeBase (45c2d7c)Merge (98ea42c)Diff
    apk (aggressive)?8.39 kB? (?)
    apk (release)?8.65 kB? (?)
  • firebase-annotations

    TypeBase (45c2d7c)Merge (98ea42c)Diff
    apk (aggressive)?8.39 kB? (?)
    apk (release)?9.46 kB? (?)
  • firebase-appdistribution

    TypeBase (45c2d7c)Merge (98ea42c)Diff
    aar?145 kB? (?)
    apk (aggressive)?787 kB? (?)
    apk (release)?2.01 MB? (?)
  • firebase-appdistribution-api

    TypeBase (45c2d7c)Merge (98ea42c)Diff
    aar?14.2 kB? (?)
    apk (aggressive)?95.8 kB? (?)
    apk (release)?707 kB? (?)
  • firebase-common

    TypeBase (45c2d7c)Merge (98ea42c)Diff
    aar?67.4 kB? (?)
    apk (aggressive)?95.1 kB? (?)
    apk (release)?700 kB? (?)
  • firebase-components

    TypeBase (45c2d7c)Merge (98ea42c)Diff
    aar?44.9 kB? (?)
    apk (aggressive)?8.68 kB? (?)
    apk (release)?33.6 kB? (?)
  • firebase-installations

    TypeBase (45c2d7c)Merge (98ea42c)Diff
    aar?55.0 kB? (?)
    apk (aggressive)?96.6 kB? (?)
    apk (release)?724 kB? (?)
  • firebase-installations-interop

    TypeBase (45c2d7c)Merge (98ea42c)Diff
    aar?8.05 kB? (?)
    apk (aggressive)?65.0 kB? (?)
    apk (release)?651 kB? (?)

Test Logs

  1. https://storage.googleapis.com/firebase-sdk-metric-reports/Bvk7OzPcrR.html

Comment on lines +224 to +234
if (!isTesterSignedIn()) {
return Tasks.forException(
new FirebaseAppDistributionException("Tester is not signed in", AUTHENTICATION_FAILURE));
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also pulled this out of the getOrCreateTask because it does not actually depend on whether there is a cached task.

* A reference to an object that uses a sequential executor to ensure non-concurrent reads and
* writes while preventing lock contention.
*/
class SequentialReference<T> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have to admit I don't really grok this. I really hope @vkryachko does.

Or maybe there is a different paradigm that can be used? This looks like low-level infrastructure which seems to be useful in general - so I'd assume there is some established pattern to do things like this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated this to be simpler based on our conversation today.

@lfkellogg lfkellogg force-pushed the lk/new-release-cache branch from f11660b to bd925b7 Compare December 9, 2022 13:43
@@ -82,14 +78,17 @@ class FirebaseAppDistributionImpl implements FirebaseAppDistribution {
@NonNull ApkUpdater apkUpdater,
@NonNull AabUpdater aabUpdater,
@NonNull SignInStorage signInStorage,
@NonNull FirebaseAppDistributionLifecycleNotifier lifecycleNotifier) {
@NonNull FirebaseAppDistributionLifecycleNotifier lifecycleNotifier,
@NonNull Executor lightweightExecutor) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: consider annotating this argument with @Lightweight for increased readability.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

private T value;

/** Get a {@link SequentialReference} that controls access uses the given sequential executor. */
static SequentialReference withSequentialExecutor(Executor sequentialExecutor) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

qq, afaicr from our discussion, this is to allow having multiple references that are still synchronized?

If so, this may be error prone as it requires all continuations to use directExecutor, otherwise there would be no guarantees of synchronization. So maybe instead of allowing to share the sequential executor we should instead have T be a composite class of all state that needs to be synchronized together, wdyt?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I actually wasn't considering synchronization in terms of needing to perform reads of multiple values at once, but makes sense that you would need to do that. Agreed, in that case we should just have all of the state be combined into a single reference. I'll remove this.

Comment on lines 78 to 81
@VisibleForTesting
T getSnapshot() {
return value;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm, this may actually be a source of flaky tests, since this is a data race, so observing the latest write is not guaranteed. How about using reference.get().get() in tests?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think that's a good move (technically we'll have to use Tasks.await()).

I was assuming the tests would do their own awaiting on the executor but might as well enforce it to be safe. Updated.

.checkForNewRelease()
.onSuccessTask(lightweightExecutor, release -> cachedNewRelease.set(release))
.onSuccessTask(
FirebaseExecutors.directExecutor(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is directExecutor required for correctness here? if not, consider lightweight instead
https://firebase.github.io/firebase-android-sdk/components/executors.html#direct-executor

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed, direct executor doesn't actually improve correctness in all cases. And correctness isn't even strictly necessary here. I'll remove it.

Comment on lines 68 to 70
* <p>Unless a direct executer is passed to {@link Task#addOnSuccessListener(Executor,
* OnSuccessListener)} or similar methods, be aware that the value may have changed by the time it
* is used.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it's actually a problem, i.e. when directExecutor is used here's the sequence of events:

  1. state = 0
  2. get() called(returns 0)
  3. a concurrent set(1) is queued but waits for 1 to complete
  4. get() continuation executes and returns the transformed task using 0
  5. task from 3 is returned to the caller
  6. 2 updates the value to 1
  7. the caller observes results based on 0 when it is actually now equal 1

Now let's what happens when a "real" executor is used:

  1. state = 0
  2. get() called(returns 0)
  3. a concurrent set(1) and updates the state to 1
  4. get() continuation executes and returns the transformed task using 0
  5. task from 3 is returned to the caller
  6. the caller observes results based on 0 when it is actually now equal 1

So regardless of the sequence of events, the outcome is the same, the value may have changed by the time the caller observes the result, unless direct executor is used throughout the whole continuation change(which generally is not a good idea).

Point being: consider using lightweight throughout unless there is some internal invariant that I am missing and will get violated by this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, good call.

I think there's another reason not to necessarily recommend the direct executor. I was able to reproduce some slightly different behavior: if I introduce an artificial delay in the execution of the initial get(), then only a direct executor ensures the entire chain off of the get() will execute with the correct value before the first set(1) happens. This is because any callbacks get called on the sequential executor's thread immediately after the read or write happens. I was able to repro this.

But this was an artificial test, and it gets complicated in the case where the get() succeeds immediately. When the task is already complete before the handlers are added, the callbacks end up being called immediately on the calling thread, not the sequential executor's thread. And in that case there's no practical difference between using a direct executor or a "real" executor.

So a direct executor really doesn't guarantee an order of execution. More reason not to recommend it IMO. I think we just need to understand when data might be old and whether that's a problem.

.setDownloadUrl(TEST_URL)
.build();

private final ExecutorService testExecutor = Executors.newSingleThreadExecutor();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's actually preferable to use the actual executor from components as it will detect violations, i.e. disk reads, network io, etc and crash the tests. Otherwise there is no guarantee that the code under test uses the right executor which will slow down all of firebase and cause logcat warnings in developers' apps

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, that's awesome. Done.

@lfkellogg lfkellogg force-pushed the lk/new-release-cache branch 2 times, most recently from a29b902 to e1bd329 Compare December 9, 2022 18:36
@github-actions
Copy link
Contributor

github-actions bot commented Dec 9, 2022

Unit Test Results

129 tests   129 ✔️  33s ⏱️
  13 suites      0 💤
  13 files        0

Results for commit 9ef0e50.

♻️ This comment has been updated with latest results.

@@ -236,7 +237,7 @@ public void checkForNewRelease_authenticationFailure_signOutTester() throws Inte
Task<AppDistributionRelease> task = firebaseAppDistribution.checkForNewRelease();

awaitTaskFailure(task, AUTHENTICATION_FAILURE, "Test");
awaitTermination(testExecutor);
awaitTermination(lightweightExecutor);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm, these executors cannot be shutdown so I am not sure awaitTermination would work

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but looks like tests have passed, so use your judgement

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I'm realizing that all of our awaitTermination() calls are really just sleeping for 100 ms because we're never shutting them down first. We need a better way to wait for executor-run tasks throughout our tests. Since these are passing right now though I'm inclined to tackle that in a followup.

@google-oss-bot
Copy link
Contributor

Startup Time Report 1

Note: Layout is sometimes suboptimal due to limited formatting support on GitHub. Please check this report on GCS.

Startup time comparison between the CI merge commit (98ea42c) and the base commit (45c2d7c) are not available.

No macrobenchmark data found for the base commit (45c2d7c). Analysis for the CI merge commit (98ea42c) can be found at:

  1. https://storage.googleapis.com/firebase-sdk-metric-reports/FUvG35aCeN/index.html

@lfkellogg lfkellogg merged commit 7192ac7 into fad/next Dec 9, 2022
@lfkellogg lfkellogg deleted the lk/new-release-cache branch December 9, 2022 21:48
lfkellogg added a commit that referenced this pull request Jan 4, 2023
…w release (#4430)

* Remove some synchronized blocks from FirebaseAppDistributionImpl

* Rename UpdateTaskCache.java to TaskCache.java

* Remove more sync blocks around new release cache

* Some refactoring

* Java formatting

* Add tests

* Add javadocs

* Simplify SequentialReference and update tests

* Update registrar

* Fix test and add static imports from TestUtils

* Incorporate some feedback

* Use actual lightweight executor in test
@firebase firebase locked and limited conversation to collaborators Jan 9, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants