-   Notifications  You must be signed in to change notification settings 
- Fork 298
Improving the ThreadManager and TokenRefresher APIs #77
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
…read factories used by the SDK
… in default thread managers to ensure clean JVM exit
…to documentation and tests.
…-java into hkj-exec-cleanup
| I did some testing on GAE for this PR. Auto and manual scaling works as before. Basic scaling also works, and idle instance shutdown can be easily enabled with a custom  My test GAE instance also has a 1 minute idle timeout. With this setup, when the app doesn't receive requests for 2 minutes (1 minute for the threads to die, and 1 more minute for the GAE idle timeout to kick in), the instance is stopped.  | 
| Thanks for your pull request. It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). 📝 Please visit https://cla.developers.google.com/ to sign. Once you've signed, please reply here (e.g.  
 | 
| I've been conducting a long running test with this PR on App Engine (manual scaling). The test is coming up on 24 hours now and seem to be working fine. | 
| CLAs look good, thanks! | 
| private final Map<String, FirebaseService> services = new HashMap<>(); | ||
| private final ListeningScheduledExecutorService executor; | ||
|  | ||
| private volatile ScheduledExecutorService scheduledExecutor; | 
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 seems like 'ScheduledExecutorService' doesn't spawn its threads until it gets used for the first time. If you were to initialize it here, the code is this class could be drastically simplified.
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.
This will initialize the ThreadFactory though (i.e ThreadManager.getThreadFactory() will get called). On App Engine this attempts to start a no-op background thread to see if the background thread support is available. Probably not a big deal, but just wanted to get your opinion on it before I make the change.
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 went through great lengths to make sure that we don't kick off random threads in the first place, so we should probably keep this as is. Thanks!
| */ | ||
| static class TokenRefresher implements CredentialsChangedListener { | ||
|  | ||
| private static final int STATE_READY = 0; | 
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.
Can we use an Enum here and an AtomicReference below?
It may even be fine to just use a volatile enum: http://www.javamex.com/tutorials/synchronization_volatile.shtml
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. Used enum. Kept the atomic reference for compare-and-set method.
| // If the access token is null, or is about to expire (i.e. expires in less than 5 minutes), | ||
| // schedule a refresh event with 0 delay. Otherwise schedule a refresh event at the token | ||
| // expiry time, minus 5 minutes. | ||
| scheduleRefresh(refreshDelay); | 
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.
Subtract the 5 minutes here
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.
This happens in the call to getRefreshDelay() a couple of lines earlier. I reorg'ed the code and comments a bit to make this clear.
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.
If you substract the 5 minutes here, then getRefreshDelay() will return exactly the delay that is specified in the token and then "refreshDelay > 0" check will work for all delays.
| long refreshDelay = accessToken.getExpirationTime().getTime() | ||
| - System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(5); | ||
| long refreshDelay = getRefreshDelay(accessToken); | ||
| if (refreshDelay > 0) { | 
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.
This doesn't work for intervals <= 5 minutes.
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.
That is correct. However, in practice this doesn't really happen. New tokens minted by Google auth have a 1 hour TTL. If it does happen (due to some issue in the remote token server), we don't want to schedule refresh events aggressively, and cause a feedback loop. So we simply log a warning and let things run course. This is what we have done in the past releases too.
| */ | ||
| @NonNull | ||
| protected abstract ScheduledExecutorService getExecutor(@NonNull FirebaseApp app); | ||
| protected abstract ExecutorService getExecutor(@NonNull FirebaseApp app); | 
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 unclear to me when what the difference between and Executor and a FirebaseExecutor is.
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.
FirebaseExecutor is just a holder for the ListeningExecutorService and the underlying ExecutorService returned by the user code. By keeping a reference to the original ExecutorService around we can make sure that the argument passed to releaseExecutor() is same as the one returned by getExecutor().
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 don't know if we want to go down that road - but what if we required the user to provide a ListeningExecutorService in the first place? Would that be cleaner?
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 would make things simpler for us. But we would be putting the onus of managing these references on the user. I would also prefer not to tie our API to a Guava type.
| * original ExecutorService. This reference is used when it's time to release/cleanup the | ||
| * original ExecutorService. | ||
| */ | ||
| static final class FirebaseExecutor { | 
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.
This feels like it could be replaced by one line of code in the base class :)
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.
The problem here is once we wrap the ExecutorService in a ListeningExecutorService, there's no way to reference the original executor again unless we explicitly store a reference to the original object. There's no way to get the delegate from the ListeningExecutorService.
On the other hand we want to make sure that the argument passed to user's releaseExecutor() is the same object returned by user's getExecutor(). So it looks like this is the easiest solution available. Or have I overlooked something obvious?
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.
Can this class be renamed to "FirebaseExecutors", with two members: "userExecutor" & "listeningExecutor"?
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. I renamed the existing FirebaseExecutors to FirebaseThreadManagers, and renamed this to FirebaseExecutors.
|  | ||
| @Override | ||
| protected ScheduledExecutorService getExecutor(FirebaseApp app) { | ||
| protected synchronized ExecutorService getExecutor(FirebaseApp app) { | 
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.
Don't lock on the main object (and don't lock twice).
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! Missed it during the merge with #74. Thanks for pointing that out.
| @Override | ||
| protected void releaseExecutor(FirebaseApp app, ScheduledExecutorService executor) { | ||
| protected synchronized void releaseExecutor(FirebaseApp app, ExecutorService executor) { | ||
| synchronized (lock) { | 
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 above
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
| * background ThreadFactory with specific keep-alive times can easily facilitate GAE idle | ||
| * instance shutdown. Note that this often comes at the cost of losing scheduled tasks and RTDB | ||
| * support. Therefore, for these features, manual-scaling is the recommended GAE deployment mode | ||
| * regardless of the ThreadManager implementation 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.
Nice!
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 haven't yet looked at the tests. Will do after.
…ool in default JVM
| */ | ||
| final synchronized void start() { | ||
| // Allow starting only from the ready state. | ||
| if (!state.compareAndSet(State.READY, State.STARTED)) { | 
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: This might not be needed since state is only ever changed under lock.
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'm also using compareAndSet() to ensure that we cannot start the refresher once it has been stopped.
| scheduleCalls.incrementAndGet(); | ||
| } | ||
| }; | ||
| // stop() is allowed here, but since we didn't start(), no measurable state change | 
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: I know there are good reasons to keep this as a separate test case, but I would personally combine it with the previous test.
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
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.
Please let me know what you think about my feedback. And then please ping me to get around my habit of not checking for GitHub review requests :/
Sorry for the long turn around.
| Made some of the suggested changes. Responded to all open comments. On to the next round. | 
This is a follow up to #74. It attempts to solve several issues in the current design:
ThreadManagerinterface returns aScheduledExecutorService. This makes implementing theThreadManagerAPI cumbersome, since the default implementation provided in Java (ScheduledThreadPoolExecutor) is not very amenable to configuration.getExecutor()andgetThreadManager()methods are not well defined in theThreadManagerinterface. At the moment it is something along the lines of:getThreadManager(): for RTDBgetExecutor(): for everything elseinitializeApp()(which starts long-lived threads). But only RTDB requires it in practice. This is not ideal when using the SDK only for auth operations.To resolve these issue, this PR makes the following changes:
ThreadManagerinterface.getThreadMaanger(): Used to start long-lived threads (including RTDB, token refresher and anything else that would require long-lived threads in the future)getExecutor(): Used for short-lived tasks.ExecutorServicefromThreadManager, whose default implementation (ThreadPoolExecutor) is much easier to configure.The downside of this change is that we end up using a dedicated executor for proactive token refresher (we can use it for other scheduled tasks that we may add in the future -- but for now it's only used for token refresher). In GAE this will end up occupying a separate background thread. This shouldn't be a problem for those who are only using RTDB, but GAE users who access both RTDB and auth ends up spending another thread. However, it is possible to provide a custom
ThreadManagerthat uses request-scoped threads for short-lived tasks, which is one way to avoid this drawback.