Skip to content

Conversation

@hiranya911
Copy link
Contributor

@hiranya911 hiranya911 commented Sep 25, 2017

This is a follow up to #74. It attempts to solve several issues in the current design:

  • ThreadManager interface returns a ScheduledExecutorService. This makes implementing the ThreadManager API cumbersome, since the default implementation provided in Java (ScheduledThreadPoolExecutor) is not very amenable to configuration.
  • The different uses of the getExecutor() and getThreadManager() methods are not well defined in the ThreadManager interface. At the moment it is something along the lines of:
    • getThreadManager(): for RTDB
    • getExecutor(): for everything else
  • Proactive token refresher always get started when calling initializeApp() (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:

  • Clearly define the contract of the ThreadManager interface.
    • 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.
  • Return an ExecutorService from ThreadManager, whose default implementation (ThreadPoolExecutor) is much easier to configure.
  • Invert the control of the proactive token refresher. Instead of always starting it from the app, let RTDB (or any other interested component) start it on demand.

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 ThreadManager that uses request-scoped threads for short-lived tasks, which is one way to avoid this drawback.

@hiranya911
Copy link
Contributor Author

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 ThreadManager:

static class BasicScalingThreadManager extends ThreadManager { @Override protected ExecutorService getExecutor(@NonNull FirebaseApp firebaseApp) { // Use a single-threaded executor which keeps threads alive for 1 minute. return new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), getThreadFactory()); } @Override protected void releaseExecutor(@NonNull FirebaseApp firebaseApp, @NonNull ExecutorService executorService) { executorService.shutdownNow(); } @Override protected ThreadFactory getThreadFactory() { return com.google.appengine.api.ThreadManager.backgroundThreadFactory(); } } 

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.

15:36:57.769 GET 404 270 B 7.2 s Unknown /_ah/start GET 404 270 B 7.2 s Unknown 15:37:06.974 GET 200 2 B 71.6 s Unknown /_ah/background GET 200 2 B 71.6 s Unknown 15:37:16.326 GET 200 119 B 231 ms curl/7.53.1 /users?uid=jALVj6oEyvXmwLZuMcUCOeRWxhS2&count=1 GET 200 119 B 231 ms curl/7.53.1 15:37:17.414 GET 200 119 B 230 ms curl/7.53.1 /users?uid=jALVj6oEyvXmwLZuMcUCOeRWxhS2&count=1 GET 200 119 B 230 ms curl/7.53.1 15:37:18.383 GET 200 119 B 213 ms curl/7.53.1 /users?uid=jALVj6oEyvXmwLZuMcUCOeRWxhS2&count=1 GET 200 119 B 213 ms curl/7.53.1 15:39:19.068 GET 200 2 B 93 ms Unknown /_ah/stop GET 200 2 B 93 ms Unknown 15:39:57.372 GET 200 119 B 13.3 s curl/7.53.1 /users?uid=jALVj6oEyvXmwLZuMcUCOeRWxhS2&count=1 GET 200 119 B 13.3 s curl/7.53.1 15:39:57.382 GET 404 270 B 8.3 s Unknown /_ah/start GET 404 270 B 8.3 s Unknown 15:40:06.735 GET 200 2 B 64 s Unknown /_ah/background GET 200 2 B 64 s Unknown 15:42:11.231 GET 200 2 B 93 ms Unknown /_ah/stop GET 200 2 B 93 ms Unknown 
@googlebot
Copy link

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 signed it!) and we'll verify. Thanks.


  • If you've already signed a CLA, it's possible we don't have your GitHub username or you're using a different email address. Check your existing CLA data and verify that your email is set on your git commits.
  • If your company signed a CLA, they designated a Point of Contact who decides which employees are authorized to participate. You may need to contact the Point of Contact for your company and ask to be added to the group of authorized contributors. If you don't know who your Point of Contact is, direct the project maintainer to go/cla#troubleshoot.
  • In order to pass this check, please resolve this problem and have the pull request author add another comment and the bot will run again.
@hiranya911
Copy link
Contributor Author

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.

@googlebot
Copy link

CLAs look good, thanks!

private final Map<String, FirebaseService> services = new HashMap<>();
private final ListeningScheduledExecutorService executor;

private volatile ScheduledExecutorService scheduledExecutor;
Copy link
Contributor

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.

Copy link
Contributor Author

@hiranya911 hiranya911 Oct 4, 2017

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.

Copy link
Contributor

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;
Copy link
Contributor

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

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. 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);
Copy link
Contributor

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

Copy link
Contributor Author

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.

Copy link
Contributor

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) {
Copy link
Contributor

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.

Copy link
Contributor Author

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);
Copy link
Contributor

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.

Copy link
Contributor Author

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().

Copy link
Contributor

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?

Copy link
Contributor Author

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 {
Copy link
Contributor

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 :)

Copy link
Contributor Author

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?

Copy link
Contributor

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"?

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. I renamed the existing FirebaseExecutors to FirebaseThreadManagers, and renamed this to FirebaseExecutors.


@Override
protected ScheduledExecutorService getExecutor(FirebaseApp app) {
protected synchronized ExecutorService getExecutor(FirebaseApp app) {
Copy link
Contributor

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).

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! 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) {
Copy link
Contributor

Choose a reason for hiding this comment

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

As above

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

* 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.
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice!

Copy link
Contributor

@schmidt-sebastian schmidt-sebastian left a 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.

@hiranya911 hiranya911 changed the base branch from hkj-thread-mgt to m19 October 4, 2017 23:12
@hiranya911 hiranya911 changed the base branch from m19 to hkj-thread-mgt October 4, 2017 23:12
@hiranya911 hiranya911 changed the base branch from hkj-thread-mgt to m19 October 4, 2017 23:32
*/
final synchronized void start() {
// Allow starting only from the ready state.
if (!state.compareAndSet(State.READY, State.STARTED)) {
Copy link
Contributor

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.

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'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
Copy link
Contributor

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.

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

Copy link
Contributor

@schmidt-sebastian schmidt-sebastian left a 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.

@hiranya911
Copy link
Contributor Author

Made some of the suggested changes. Responded to all open comments. On to the next round.

@hiranya911 hiranya911 merged commit 122595a into m19 Oct 5, 2017
@hiranya911 hiranya911 deleted the hkj-exec-cleanup branch October 5, 2017 21:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

3 participants