- Notifications
You must be signed in to change notification settings - Fork 298
Introducing the ThreadManager API #74
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
| I've been running some tests with this PR on App Engine, and I'll document my observations here. Manual ScalingWorks as usual. All async tasks (from auth, database and everything else) runs normally with the default Automatic ScalingWorks as usual. All async tasks, except for the ones started by database, runs normally with the default Basic ScalingThis is similar to manual scaling. So everything, including database, will work as usual. However, the long-lived threads started by the database, and the proactive token refresher will prevent the instances from shutting down due to instance idle time (unless
We will provide an API to achieve item 2 in the future. In the meantime, one can use a custom executor (specified via This essentially rejects all "scheduled" tasks, and only admits tasks meant for immediate execution (i.e. I had my basic-scaled instance set to shutdown after 1 minute of idle time: With the above custom executor, I observed that my instance is shutting down after 2 minutes of idle time (1 extra minute due to the 60s keep alive time set on the executor). So overall it was a successful test. I had similar results with both background and request-scoped threads on basic-scaling. Once we provide a means to explicitly turn off proactive token refreshing, the custom executor implementation can be made as simple as |
| checkState(!deleted.get(), "FirebaseApp was deleted %s", this); | ||
| } | ||
| | ||
| private ListeningScheduledExecutorService getExecutorService() { |
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.
How often is this not called? Can we initialize the executor in the constructor?
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. In performance-sensitive environments like GAE we lazy load the underlying executor implementation itself. So it's safe to always initialize the high-level executor in the FirebaseApp constructor.
| | ||
| private final AtomicBoolean deleted = new AtomicBoolean(); | ||
| private final Map<String, FirebaseService> services = new HashMap<>(); | ||
| private final AtomicReference<ListeningScheduledExecutorService> executorReference = |
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.
Since this is only ever accessed under lock, does it need to be an AtomicReference?
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.
| ScheduledExecutorService executor = executorReference.get(); | ||
| if (executor != null) { | ||
| threadManager.releaseExecutor(this, executor); | ||
| executorReference.set(null); |
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 think it would be cleaner to call checkNotDeleted() in the schedule methods below and optionally not clean up this reference.
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.
Now that the AtomicReference is gone, this is a lot simpler.
| } | ||
| | ||
| /** | ||
| @NonNull |
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.
Should this be public to follow the other getters more closely?
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.
We already have getCredentials() which is package-protected. So there is some precedent here. I feel more comfortable keeping this hidden from other packages and also user code.
| /** | ||
| * Returns the main thread pool for an app. Implementations may return the same instance of | ||
| * <code>ScheduledExecutorService</code> for multiple apps. The returned thread pool is used by | ||
| * all components of an app except for the Realtime Database. Database has far stricter and |
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 is really hard to reason about. Would it be possible to only ever return a Thread Factory?
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 agree. Semantics of this API are not well defined at the moment. I'm putting together #77 to address this issue. Once we do those changes we will have a clear separation:
getExecutor(): Returns a thread pool for short-lived tasksgetThreadFactory(): Returns a ThreadFactory for long-lived tasks (includes RTDB and other scheduled tasks)
To answer you specific question, now that we initialize the executor in FirebaseApp constructor, it is no longer possible to get away with returning null from getExecutor().
| accessToken.getExpirationTime().getTime()); | ||
| return Tasks.forResult(googleToken); | ||
| } catch (IOException e) { | ||
| throw new RuntimeException(e); |
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.
Does the Task library not have its own way of passing along errors?
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. On a related note both Task and FirebaseCredential are now deprecated, and are on their way out.
| this.threadFactory = checkNotNull(threadFactory); | ||
| } | ||
| | ||
| ThreadInitializer getInitializer() { |
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 is somewhat hard to reason about (from the name alone) what the difference between a ThreadInitializer and a ThreadFactory is. Can you add comments (or rename the new ThreadInitializer)?
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
| public class WebSocket { | ||
| | ||
| private static final ThreadConfig DEFAULT_THREAD_CONFIG = new ThreadConfig( | ||
| ThreadInitializer.defaultInstance, Executors.defaultThreadFactory()); |
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.
Should this get the thread initializer from Platform.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.
This constant is not used during normal execution. It is only used in the hidden Websocket(URL url) constructor, which is only called by some internal test classes we have implemented (a set of executable main methods!). So I believe this is ok.
We can discuss later whether we need this at all. I think this and the test client code that use it can all be removed.
| assertEquals(Event.TYPE_RELEASE_EXECUTOR, event.type); | ||
| assertEquals(app, event.arguments[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 you add tests that exercise the "setup-teardown-setup-teardown" logic you implemented?
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 think that's covered in the FirebaseExecutorsTest. Let me know if that's not what you were referring to.
| private final int type; | ||
| private final Object[] arguments; | ||
| | ||
| public Event(int type, Object... arguments) { |
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 would prefer if you explicitly called out the arguments.
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
…to documentation and tests.
| Made most of the suggested changes. A few more changes are on their way in #77. In particular some of the complexities you pointed out in the semantics of |
| * Returns the <code>ThreadFactory</code> to be used for creating any additional threads | ||
| * required by the SDK. This is used mainly to create the run loop, event target and web socket | ||
| * reader/writer threads of the Realtime Database client. | ||
| * required by the SDK. This is used mainly to create the long-lived worker threads for |
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.
missing "the"
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
| | ||
| /** | ||
| * The {@link ThreadInitializer} implementation to be used for further configuring already | ||
| * created threads. In particular, this instance is used to rename the created threads, and turn |
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 suggests that it would work with threads that were created way back in the days. This class is used to configure newly created threads, while the factory is used to create new threads.
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.
Updated the text.
| return app.getThreadFactory(); | ||
| } | ||
| | ||
| public static <T> Task<T> submit(@NonNull FirebaseApp app, @NonNull Callable<T> command) { |
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.
submitCallable maybe? 'submit' seems generic in the context of this 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.
Done
| * @return A non-null {@link ScheduledExecutorService} instance. | ||
| */ | ||
| @NonNull | ||
| protected abstract ScheduledExecutorService 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.
Could we combine the two methods (this and above) and always return a ListeningScheduledExecutorService?
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 would prefer to keep ThreadManager API decoupled from any Guava types. That way developers don't have to learn about Guava APIs to extend this API.
| GoogleOAuthAccessToken googleToken = new GoogleOAuthAccessToken(accessToken.getTokenValue(), | ||
| accessToken.getExpirationTime().getTime()); | ||
| return Tasks.forResult(googleToken); | ||
| } catch (IOException e) { |
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.
Just Exception?
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 final ThreadInitializer threadInitializer; | ||
| private final ThreadFactory threadFactory; | ||
| | ||
| public ThreadConfig(ThreadInitializer threadInitializer, ThreadFactory threadFactory) { |
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: Swap the order here. First thread get 'factoried' and then initialized.
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 ScheduledExecutorService executorService; | ||
| | ||
| @Override | ||
| protected synchronized ScheduledExecutorService 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.
I would suggest not to synchronize on the public facing object (since this can also be done by consumers). Can you use a private member (like executorService)?
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
| .setDaemon(true) | ||
| .setThreadFactory(getThreadFactory()) | ||
| .build(); | ||
| logger.debug("Initializing default executor with {} max threads", cores); |
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 believe there is any guarantee that ScheduledThreadPoolExecutor will try to schedule each thread on a dedicated core, so using this number here may not make much sense.
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.
Correct. I just used that as a max limit since ScheduledThreadPoolExecutor must be given some core thread pool count. I think we can go back to cached thread pool when we implement #77.
| Made the suggested changes. Over to @schmidt-sebastian for another round. |
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.
Thanks!
Adding the new
ThreadManagerinterface which controls how an app obtains its thread pools and thread factories. Developers can specify their own implementations of this interface viaFirebaseOptions. We provide 2 implementations (one for basic JVM and one for GAE), one of which will be used by default.This PR also ensures:
app.delete()terminates any and all threads started by the SDK.app.delete().ThreadFactoryprovided byThreadManagerto start all of its threads (event target, run loop, socket reader/writer etc.)TaskExecutorsfor any thing. However, we continue to keep it in the code base for backward compatibility reasons.