-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
Improve (and shorten) query polling implementation. #4243
Conversation
This implementation has the following benefits: - It collapses the QueryScheduler abstraction into the QueryManager (which was always ultimately responsible for managing the lifetime of polling timers), thus simplifying the relationship between the QueryManager and its ObservableQuery objects. - It's about 100 bytes smaller than the previous implementation, after minification and gzip. - It uses setTimeout rather than setInterval, so event loop starvation never leads to a rapid succession of setInterval catch-up calls. - It guarantees at most one timeout will be pending for an arbitrary number of polling queries, rather than a separate timer for every distinct polling interval. - Fewer independent timers means better batching behavior, usually. - Though there may be a delay between the desired polling time for a given query and the actual polling time, the delay is never greater than the minimum polling interval across all queries, which changes dynamically as polling queries are started and stopped.
@@ -19,6 +19,5 @@ | |||
"src/core/__tests__/fetchPolicies.ts", | |||
"src/data/__tests__/queries.ts", | |||
"src/errors/__tests__/ApolloError.ts", | |||
"src/scheduler/__tests__/scheduler.ts" |
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 small change is deceptively significant, because it means the scheduler.ts
tests are now required to pass tsc
type checking.
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.
Right - at some point we should probably get all exclude
files tsc
ready, and remove them from the tsconfig.test.json
.
queryManager.startPollingQuery(queryOptions, queryId); | ||
|
||
let count = 0; | ||
queryManager.pollingInfoByQueryId.forEach((info: { interval: number }, qid: string) => { |
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 understand why TypeScript has trouble inferring the type of queryManager.pollingInfoByQueryId
here (it assumes any
).
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.
TS seems to be able to infer the type when I check on my side, after a full npm run clean; npm i
(I'm then able to drop the types in the forEach
function signature). Something might just be out synch behind the scenes.
@@ -537,13 +526,8 @@ export class ObservableQuery< | |||
); | |||
} | |||
|
|||
if (this.isCurrentlyPolling) { | |||
this.scheduler.stopPollingQuery(this.queryId); | |||
this.isCurrentlyPolling = false; |
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.
No need to keep track of isCurrentlyPolling
here in ObservableQuery
, because the QueryManager
tracks that information in pollingInfoByQueryId
, and stopping is no longer required before restarting.
@@ -662,7 +661,7 @@ export class QueryManager<TStore> { | |||
let transformedOptions = { ...options } as WatchQueryOptions; | |||
|
|||
return new ObservableQuery<T>({ | |||
scheduler: this.scheduler, | |||
queryManager: 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 hope it's safe to change ObservableQuery
constructor parameters like this. From what I can tell, ObservableQuery
is used as a type elsewhere, but this is the only place where new instances are created?
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 should be safe. We are exporting ObservableQuery
, but I doubt anyone is using it externally (for anything other than types) since they'd have to provide their own QueryScheduler
instance.
import { ObservableQuery } from '../../core/ObservableQuery'; | ||
|
||
// Used only for unit testing. | ||
function registerPollingQuery<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.
This used to be a method of QueryScheduler
, even though it was only used for testing. Turning test-only methods into test functions is an easy way to save bundle size.
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.
Awesome @benjamn - looks great! My comments here are just comments on your comments, so nothing new to add. LGTM!
@@ -662,7 +661,7 @@ export class QueryManager<TStore> { | |||
let transformedOptions = { ...options } as WatchQueryOptions; | |||
|
|||
return new ObservableQuery<T>({ | |||
scheduler: this.scheduler, | |||
queryManager: 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 think it should be safe. We are exporting ObservableQuery
, but I doubt anyone is using it externally (for anything other than types) since they'd have to provide their own QueryScheduler
instance.
queryManager.startPollingQuery(queryOptions, queryId); | ||
|
||
let count = 0; | ||
queryManager.pollingInfoByQueryId.forEach((info: { interval: number }, qid: string) => { |
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.
TS seems to be able to infer the type when I check on my side, after a full npm run clean; npm i
(I'm then able to drop the types in the forEach
function signature). Something might just be out synch behind the scenes.
@@ -19,6 +19,5 @@ | |||
"src/core/__tests__/fetchPolicies.ts", | |||
"src/data/__tests__/queries.ts", | |||
"src/errors/__tests__/ApolloError.ts", | |||
"src/scheduler/__tests__/scheduler.ts" |
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.
Right - at some point we should probably get all exclude
files tsc
ready, and remove them from the tsconfig.test.json
.
This implementation has the following benefits: - It collapses the QueryScheduler abstraction into the QueryManager (which was always ultimately responsible for managing the lifetime of polling timers), thus simplifying the relationship between the QueryManager and its ObservableQuery objects. - It's about 100 bytes smaller than the previous implementation, after minification and gzip. - It uses setTimeout rather than setInterval, so event loop starvation never leads to a rapid succession of setInterval catch-up calls. - It guarantees at most one timeout will be pending for an arbitrary number of polling queries, rather than a separate timer for every distinct polling interval. - Fewer independent timers means better batching behavior, usually. - Though there may be a delay between the desired polling time for a given query and the actual polling time, the delay is never greater than the minimum polling interval across all queries, which changes dynamically as polling queries are started and stopped.
This implementation has the following benefits: - It collapses the QueryScheduler abstraction into the QueryManager (which was always ultimately responsible for managing the lifetime of polling timers), thus simplifying the relationship between the QueryManager and its ObservableQuery objects. - It's about 100 bytes smaller than the previous implementation, after minification and gzip. - It uses setTimeout rather than setInterval, so event loop starvation never leads to a rapid succession of setInterval catch-up calls. - It guarantees at most one timeout will be pending for an arbitrary number of polling queries, rather than a separate timer for every distinct polling interval. - Fewer independent timers means better batching behavior, usually. - Though there may be a delay between the desired polling time for a given query and the actual polling time, the delay is never greater than the minimum polling interval across all queries, which changes dynamically as polling queries are started and stopped.
Breaks |
This reverts commit b4f0c8e. Should fix apollographql/react-apollo#2738, until we can find a better solution.
This reverts commit 9739ff6. Now that we have a uniform interface for terminating ApolloClient instances (#4336), there should be no need for any external code to access the QueryScheduler abstraction, which this commit removes. We should wait to merge and release this change until after apollographql/react-apollo#2741 has been merged and released, so that we don't break older versions of MockedProvider.
This reverts commit 9739ff6. Now that we have a uniform interface for terminating ApolloClient instances (#4336), there should be no need for any external code to access the QueryScheduler abstraction, which this commit removes. We should wait to merge and release this change until after apollographql/react-apollo#2741 has been merged and released, so that we don't break older versions of MockedProvider.
This reverts commit 9739ff6. Now that we have a uniform interface for terminating ApolloClient instances (#4336), there should be no need for any external code to access the QueryScheduler abstraction, which this commit removes. We should wait to merge and release this change until after apollographql/react-apollo#2741 has been merged and released, so that we don't break older versions of MockedProvider.
…removal Un-revert "Improve (and shorten) query polling implementation. (#4243)"
The last time I touched this code in #4243, I thought it was worthwhile to preserve the behavior of sometimes batching polling query fetches in the same tick of the event loop. However, as @voxtex points out in #4786, almost any batching strategy will lead to unpredictable fetch timing, skipped fetches, poor accounting for fetch duration (which could exceed the polling interval), and so on. On top of that, even the premise that "batching is good" comes into question if it doesn't happen consistently. An implementation which uses independent timers seems much simpler and more predictable.
The last time I touched this code in #4243, I thought it was worthwhile to preserve the behavior of sometimes batching polling query fetches in the same tick of the event loop. However, as @voxtex points out in #4786, almost any batching strategy will lead to unpredictable fetch timing, skipped fetches, poor accounting for fetch duration (which could exceed the polling interval), and so on. On top of that, even the premise that "batching is good" comes into question if it doesn't happen consistently. An implementation which uses independent timers seems much simpler and more predictable.
This implementation has the following benefits:
It collapses the
QueryScheduler
abstraction into theQueryManager
(which was always ultimately responsible for managing the lifetime of polling timers), thus simplifying the relationship between theQueryManager
and itsObservableQuery
objects.It's about 100 bytes smaller than the previous implementation, after minification and gzip.
It uses
setTimeout
rather thansetInterval
, so event loop starvation never leads to a rapid succession ofsetInterval
catch-up calls.It guarantees at most one timeout will be pending for an arbitrary number of polling queries, rather than a separate timer for every distinct polling interval.
Fewer independent timers means better batching behavior, usually.
Though there may be a delay between the desired polling time for a given query and the actual polling time, the delay is never greater than the minimum polling interval across all queries, which changes dynamically as polling queries are started and stopped.