-
Notifications
You must be signed in to change notification settings - Fork 926
Handle IndexedDB errors in unsubscribe callback #3162
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
Changes from 1 commit
9686261
131c6cd
024c2e7
0e00088
1f56feb
182d100
b45bb4f
91e915c
9a5358f
f124462
657def9
f334261
282cc41
09672eb
837cc1b
3947788
4b8b439
726697f
7cdfa36
d3ca86c
5ae98a4
7bcb1ff
d88058e
6960241
5b26aed
0d2b01d
7042a71
01abb6b
0bdd73e
af7e24d
db788d1
1ba0fe1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -399,8 +399,29 @@ abstract class TestRunner { | |
const query = parseQuery(querySpec); | ||
const eventEmitter = this.queryListeners.get(query); | ||
debugAssert(!!eventEmitter, 'There must be a query to unlisten too!'); | ||
this.queryListeners.delete(query); | ||
await this.queue.enqueue(() => this.eventManager.unlisten(eventEmitter!)); | ||
|
||
const deferred = new Deferred<void>(); | ||
await this.queue.enqueueRetryable(async () => { | ||
try { | ||
await this.eventManager.unlisten(eventEmitter!); | ||
const currentEventEmitter = this.queryListeners.get(query); | ||
// Before removing the listener, verify that the query hasn't been | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I must be missing some context for this comment. If a user attempts to unlisten, that fails, gets in the retry queue, and then they listen to the same query while we're retrying, doesn't the event manager have two listeners for that query? The first query will stil succeed in unlistening. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
// relistened to in between retry events. We only remove the listener | ||
// if the current event emitter still matches the event emitter at the | ||
// time of the unlisten. | ||
if (currentEventEmitter === eventEmitter) { | ||
this.queryListeners.delete(query); | ||
} | ||
} catch (e) { | ||
expect(this.persistence.injectFailures).contains('Release target'); | ||
throw e; | ||
} finally { | ||
// Resolve the deferred Promise even if the operation failed. This allows | ||
// the spec tests to manually retry the failed user change. | ||
deferred.resolve(); | ||
} | ||
}); | ||
return deferred.promise; | ||
} | ||
|
||
private doSet(setSpec: SpecUserSet): Promise<void> { | ||
|
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.
Why is this necessary? I thought that if we failed we tore down the streams? At that point haven't we effectively unlistened anyway? All that's missing is updating our in-memory bookkeeping to indicate that we don't want to restart the target when IndexedDB becomes available again.
One other concern is that if IndexedDB is failing we've likely been put in the background and may not come back for a while--possibly more than the 30 minutes for which a resume token means free query resumption. If we were asleep that long the user would be charged for the query anew--for a query they were trying to stop listening to. If all we're losing is the resume token and the sequence numbers for exact GC ordering, it seems better to let the unlisten succeed despite failing to write.
So in the primary tab, if we fail trying to save metadata data about unlistening that's not really a big deal. Can a secondary tab fail trying to tell the primary that it wants to unlisten? In that case we should probably retry.
If this squares with your understanding, it seems like this line may actually be correct: we do want to retry unlistens but only when it's the secondary tab asking the primary tab to do it. The part that's missing is that primary tabs should handle IndexedDB errors and succeed.
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 code path calls into LocalStore to invoke "Release Target" and then calls RemoteStore. We could change the plumbing here to drive this from RemoteStore, in which case RemoteStore could go offline and do its own retry. That sounds a like a fair bit of code churn that may lead to some unexpected side effects - the mapping between the
lastListen
state in EventManager and the active list of targets in SyncEngine/LocalStore may go out of sync.One easy thing we could do here is just swallow IndexedDB failures if "Release Target" fails, which would even remove some of the newly added error handling in RemoteStore. Do you think that's worth exploring?
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 what I was suggesting: releasing a target in the LocalStore is just about making sure the final updates are made for GC metadata (and the in-memory tracking of the target). It may be that even LocalStore itself may need to change so that even if the transaction fails it will remove the target from its own set of live things.
I'm not sure this undoes the error handling in RemoteStore, since it's really only unsubscribe can work this way.
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 updated the PR to ignore errors in "Release target". Unfortunately, since a failed Target can generate a "fake" Remote Event (for Limbo resolutions), the code path that calls into this from RemoteStore still has to use the IndexedDB fallback.