-
Notifications
You must be signed in to change notification settings - Fork 617
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
Conversation
Generated by 🚫 Danger |
Coverage Report 1Affected Products
Test Logs |
Size Report 1Affected Products
Test Logs |
if (!isTesterSignedIn()) { | ||
return Tasks.forException( | ||
new FirebaseAppDistributionException("Tester is not signed in", AUTHENTICATION_FAILURE)); | ||
} |
There was a problem hiding this comment.
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.
...e-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/UpdateTaskImpl.java
Outdated
Show resolved
Hide resolved
...st/java/com/google/firebase/appdistribution/impl/FirebaseAppDistributionServiceImplTest.java
Outdated
Show resolved
Hide resolved
...src/main/java/com/google/firebase/appdistribution/impl/FirebaseAppDistributionRegistrar.java
Outdated
Show resolved
Hide resolved
* A reference to an object that uses a sequential executor to ensure non-concurrent reads and | ||
* writes while preventing lock contention. | ||
*/ | ||
class SequentialReference<T> { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
...st/java/com/google/firebase/appdistribution/impl/FirebaseAppDistributionServiceImplTest.java
Show resolved
Hide resolved
f11660b
to
bd925b7
Compare
@@ -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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
@VisibleForTesting | ||
T getSnapshot() { | ||
return value; | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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(), |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
* <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. |
There was a problem hiding this comment.
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:
state
=0
get()
called(returns0
)- a concurrent
set(1)
is queued but waits for1
to complete get()
continuation executes and returns the transformed task using0
- task from 3 is returned to the caller
- 2 updates the value to
1
- the caller observes results based on
0
when it is actually now equal1
Now let's what happens when a "real" executor is used:
state
=0
get()
called(returns0
)- a concurrent
set(1)
and updates the state to1
get()
continuation executes and returns the transformed task using0
- task from 3 is returned to the caller
- the caller observes results based on
0
when it is actually now equal1
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.
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you can use this utility class to get a hold of the right executor:
https://github.com/firebase/firebase-android-sdk/blob/master/integ-testing/src/main/java/com/google/firebase/concurrent/TestOnlyExecutors.java
There was a problem hiding this comment.
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.
a29b902
to
e1bd329
Compare
@@ -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); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
Startup Time Report 1Note: 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: |
…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
Continuing to remove
synchronized
blocks based on conversation on #4350.This also uses an executor for more Task continuations.