-
Notifications
You must be signed in to change notification settings - Fork 947
Don't crash if Target cannot be persisted #2944
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
Binary Size ReportAffected SDKs
Test Logs
|
1a67dfc
to
56ada10
Compare
56ada10
to
83f0a22
Compare
@@ -59,15 +67,14 @@ export class EventManager implements SyncEngineListener { | |||
this.syncEngine.subscribe(this); | |||
} | |||
|
|||
listen(listener: QueryListener): Promise<TargetId> { | |||
async listen(listener: QueryListener): 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.
I removed the return value since a failed target doesn't have an ID. The only consumer of the targetId result were the spec tests, which already verify the target ID in every step as part of expectedActiveTargets
.
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 doesn't failing to listen reject the promise here? If it did then you could return the TargetId in the successful case.
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 I reject the Promise here, then FirestoreClient has to handle the rejected Promise. This is actually pretty nice and keeps the code in EventManager much cleaner. The calling code is also still clean, since it was simple to begin with.
The problem I see is that this makes this code non-testable. The spec tests don't use FirestoreClient, and the integration cannot inject failures. I have made the change regardless and added some special handling in SyncEngine. I am not sure if this is an improvement though.
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 see what you mean; the spec test has to emulate this for itself. That's not completely awful but you're probably right that the other way was better because it was testable.
Would it help to put the error handling in SyncEngine
?
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 SyncEngine is not the right place for this since it can only convey two states: Success or failure. We really need three states: Success, failure (but don't crash AsyncQueue) and failure (crash AsyncQueue).
Unless we change SyncEngine.listen()
return a tristate, we have to wrap any error thrown by SyncEngine in the same if (error.name === '...')
check when we invoke the QueryListener.
Given this and the test ability, I moved the code back to EventListener. It's not as clean looking, but it will probably be easier to keep regression free.
@@ -246,8 +252,7 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer { | |||
} | |||
} | |||
|
|||
this.syncEngineListener!.onWatchChange([viewSnapshot]); |
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 had to remove this indirection. onWatchChange
doesn't fire if EventManager doesn't execute this.queries.set(query, queryInfo)
first.
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.
At first I was concerned about this because it seemed like this would prevent calling onViewSnapshot
on the listener, but actually it looks like code to do that was already explicitly in the EventManager
(in if (queryInfo.viewSnap)
, and I'm guessing that the explicit code was just being treated as a no-op because previously queryInfo.viewSnap
would be undefined before?
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 am not sure I fully understand this, but I hope that my previous comment reliefs your concerns. Let me know if it doesn't.
@@ -98,6 +98,12 @@ class QueryView { | |||
) {} | |||
} | |||
|
|||
/** Result type returned during initial query registration. */ | |||
export interface QueryRegistration { |
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 now have three different versions of this interface: QueryRegistration. ViewChange, QueryListenersInfo
They are all a bit different and i don't see a way to combine them. ViewChange
would be the best candidate, but the View does not expose the targetId.
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 old arrangement where the sync engine was responsible for the first snapshot avoided this problem.
I wonder if would be possible for the sync engine to call back indicating the listen failed instead? That way EventManager.listen could proceed optimistically and then just surface the error/remove the listener as normal without any changes.
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 issue here is actually the success case - the problem here is the ViewSnapshot for the initial listen needs to be raised. The current flow is:
- EventManager's listen() registers target in local data structure
- EventManager calls SyncEngine listen()
- SyncEngine's listen() calls back EventManager
- EventManager looks in local data structure to raise snapshot
- SyncEngine finishes listen()
- EventManager finishes listen()
This code seems fragile to me The callback in SyncEngine relies on an implementation detail of EventManager. I was very surprised that re-arranging the internal code of EventManager broke tests that were unrelated to my change. Because of this, I think it is better to return a ViewSnapshot.
Having said that, I was able to remove this type after all because EventManager doesn't actually use the target ID (other than to pass it through). I updated listen() to just return the initial snapshot.
374948d
to
d2ac3b0
Compare
83f0a22
to
a72091c
Compare
a72091c
to
45d44ff
Compare
@@ -1,4 +1,6 @@ | |||
# Unreleased | |||
- [fixed] Firestore now rejects `onSnapshot()` listeners if they cannot be |
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 and the next release note could probably be combined. Something like:
Firestore now rejects
onSnapshot
listeners are writes if they cannot be persisted in IndexedDB. ...
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 prior change is going out this week. Unfortunately, this Changelog is not all that clean, but this is mostly due to the fact the version numbers are impossible to figure out. Maybe we can discuss this with the team and clean it up once and for all.
@@ -59,15 +67,14 @@ export class EventManager implements SyncEngineListener { | |||
this.syncEngine.subscribe(this); | |||
} | |||
|
|||
listen(listener: QueryListener): Promise<TargetId> { | |||
async listen(listener: QueryListener): 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 doesn't failing to listen reject the promise here? If it did then you could return the TargetId in the successful case.
queryInfo.targetId = targetId; | ||
queryInfo.viewSnap = snapshot; | ||
} catch (e) { | ||
if (e.name === 'IndexedDbTransactionError') { |
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 may not be possible, but it seems like could benefit from handling these errors consistently in our API.
For example, we try to make FirestoreClient the place where we centrally handle dispatching onto the AsyncQueue. Should we try to similarly make handling retries (or error handling like this) a part of the SyncEngine?
This doesn't seem to work in this case because you're returning a successful promise in case of a known error, but that seems to be catering to the caller's use of this function in a way that makes this more complicated than it needs to be. Maybe the caller should handle this in the block submitted to the async queue?
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 did end up updating this, but as stated this pushes the error handling to both FirestoreClient and SpecTestRunner.
@@ -246,8 +252,7 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer { | |||
} | |||
} | |||
|
|||
this.syncEngineListener!.onWatchChange([viewSnapshot]); |
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.
At first I was concerned about this because it seemed like this would prevent calling onViewSnapshot
on the listener, but actually it looks like code to do that was already explicitly in the EventManager
(in if (queryInfo.viewSnap)
, and I'm guessing that the explicit code was just being treated as a no-op because previously queryInfo.viewSnap
would be undefined before?
@@ -98,6 +98,12 @@ class QueryView { | |||
) {} | |||
} | |||
|
|||
/** Result type returned during initial query registration. */ | |||
export interface QueryRegistration { |
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 old arrangement where the sync engine was responsible for the first snapshot avoided this problem.
I wonder if would be possible for the sync engine to call back indicating the listen failed instead? That way EventManager.listen could proceed optimistically and then just surface the error/remove the listener as normal without any changes.
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 for the review. I have another controversial version for you :)
@@ -1,4 +1,6 @@ | |||
# Unreleased | |||
- [fixed] Firestore now rejects `onSnapshot()` listeners if they cannot be |
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 prior change is going out this week. Unfortunately, this Changelog is not all that clean, but this is mostly due to the fact the version numbers are impossible to figure out. Maybe we can discuss this with the team and clean it up once and for all.
@@ -59,15 +67,14 @@ export class EventManager implements SyncEngineListener { | |||
this.syncEngine.subscribe(this); | |||
} | |||
|
|||
listen(listener: QueryListener): Promise<TargetId> { | |||
async listen(listener: QueryListener): 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.
If I reject the Promise here, then FirestoreClient has to handle the rejected Promise. This is actually pretty nice and keeps the code in EventManager much cleaner. The calling code is also still clean, since it was simple to begin with.
The problem I see is that this makes this code non-testable. The spec tests don't use FirestoreClient, and the integration cannot inject failures. I have made the change regardless and added some special handling in SyncEngine. I am not sure if this is an improvement though.
queryInfo.targetId = targetId; | ||
queryInfo.viewSnap = snapshot; | ||
} catch (e) { | ||
if (e.name === 'IndexedDbTransactionError') { |
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 did end up updating this, but as stated this pushes the error handling to both FirestoreClient and SpecTestRunner.
@@ -98,6 +98,12 @@ class QueryView { | |||
) {} | |||
} | |||
|
|||
/** Result type returned during initial query registration. */ | |||
export interface QueryRegistration { |
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 issue here is actually the success case - the problem here is the ViewSnapshot for the initial listen needs to be raised. The current flow is:
- EventManager's listen() registers target in local data structure
- EventManager calls SyncEngine listen()
- SyncEngine's listen() calls back EventManager
- EventManager looks in local data structure to raise snapshot
- SyncEngine finishes listen()
- EventManager finishes listen()
This code seems fragile to me The callback in SyncEngine relies on an implementation detail of EventManager. I was very surprised that re-arranging the internal code of EventManager broke tests that were unrelated to my change. Because of this, I think it is better to return a ViewSnapshot.
Having said that, I was able to remove this type after all because EventManager doesn't actually use the target ID (other than to pass it through). I updated listen() to just return the initial snapshot.
@@ -246,8 +252,7 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer { | |||
} | |||
} | |||
|
|||
this.syncEngineListener!.onWatchChange([viewSnapshot]); |
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 am not sure I fully understand this, but I hope that my previous comment reliefs your concerns. Let me know if it doesn't.
Friendly bump :) |
listener.onError( | ||
new FirestoreError( | ||
Code.UNAVAILABLE, | ||
`Failed to register query: ${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.
A few thoughts on this:
- From a user's point of view they listen to queries (or get them) but we don't have a public concept of "registration". I think it's probably fine to just say "failed to listen to query", "failed to start a listener to query" or maybe something more generic like "failed to start query".
- We should include details of the query that failed (a toString or similar). Whenever people are filing issues about broken queries our first instructions are to have people go figure out which query is causing the problem and they're usually at a loss. We should just include that in the error message.
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 to use "initialize" and also included the Query in the message.
queryInfo.viewSnap = await this.syncEngine.listen(query); | ||
} | ||
|
||
this.queries.set(query, queryInfo!); |
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 surprised the !
is required here. Isn't queryInfo
guaranteed to be set here? (What confuses me is you're not null-checking the assignment to e.g. queryInfo.viewSnap
).
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 indeed not needed. It might have been needed before the refactor, but now TS can determine that it is set.
.expectEvents(query1, {}) | ||
.failDatabase() | ||
.userListens(query2) | ||
.expectEvents(query2, { errorCode: Code.UNKNOWN }) |
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.
What's the source of the UNKNOWN
here? The spec test runner seems to be doing new FirestoreError(Code.UNAVAILABLE, e.message)
. Is anything even handling errorCode
?
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 don't validate the error code at all. I added this validation.
} | ||
|
||
if (firstListen) { | ||
queryInfo.viewSnap = await this.syncEngine.listen(query); |
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.
Nothing seems to populate queryInfo.targetId
anymore?
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.
queryInfo.targetId
is never read and I removed it.
@@ -59,15 +67,14 @@ export class EventManager implements SyncEngineListener { | |||
this.syncEngine.subscribe(this); | |||
} | |||
|
|||
listen(listener: QueryListener): Promise<TargetId> { | |||
async listen(listener: QueryListener): 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.
I see what you mean; the spec test has to emulate this for itself. That's not completely awful but you're probably right that the other way was better because it was testable.
Would it help to put the error handling in SyncEngine
?
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.
LGTM
This PR rejects a Query listener if its Target cannot be allocated.
Addresses #2755