-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
Refactor Remote Event #1379
Refactor Remote Event #1379
Conversation
35a1a8b
to
c97b8bf
Compare
dfe5c51
to
0e45a26
Compare
0e45a26
to
df59590
Compare
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.
First pass. I haven't gone through the tests in any detail yet.
*/ | ||
+ (instancetype)withEmptyResultAtKey:(DocumentKey)documentKey; | ||
|
||
- (instancetype) |
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.
init
methods should be 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.
I removed this initializer altogether.
@@ -48,6 +49,9 @@ | |||
@class FSTObjectValue; | |||
@protocol FSTFilter; | |||
|
|||
using firebase::firestore::model::DocumentKey; |
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.
using
statements shouldn't be in headers.
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.
Make sense. Removed here and everywhere.
}]; | ||
} | ||
|
||
- (instancetype) |
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.
Standard method order is init
methods, NSObject
other methods.
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 has been removed. The other classes that I added already follow this pattern.
* Creates an FSTTestTargetMetadataProvider that returns the given document for the existing key | ||
* callback (via `remoteKeysForTarget`) and marks queries as active (via `queryDataForTarget`). | ||
*/ | ||
+ (instancetype)withSingleResultAtKey:(DocumentKey)documentKey; |
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 names should reflect the return type as the first part. Probably providerWith
... This parallels e.g. [NSString stringWithFormat:]
.
Also, your comment indicates that it's a lookup "for the existing key". Perhaps this should be ...ForKey
?
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.
Renamed this and withEmptyResultAtKey
as per your suggestion.
initWithRemoteKeysForTargetCallback:^DocumentKeySet(FSTTargetID targetID) { | ||
return DocumentKeySet{}; | ||
} | ||
queryDataCallback:^FSTQueryData *(FSTTargetID 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.
This doesn't seem like a very rigorous testing mechanism. It basically will create a target for any target id and that target id will unconditionally be a document listen. If the system under test is grossly misbehaving and issuing all kinds of random queries this isn't going to catch it.
Additionally, I'm a little concerned about all the magical callbacks involved here.
A much more straightforward implementation would be a "fixed" metadata provider, where you register which queries are active and their results. In pseudocode:
- new fake provider
- add listener to query X with results Y (with target id Z? not sure)
- repeat for all active queries
Now you can test watch updates for queries that don't register as active and can avoid all this magical callback stuff.
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 changed the way I use the FSTTestTargetMetadataProvider
in the tests for RemoteEvent and also followed your suggestion to add a setter that can change the internal state. For the most part, the tests still follow the JS pattern and set up everything up front. All tests that only create a single remote event still set up the initial state in the aggregatorWith...
helper method.
The test testDocumentUpdate
has been updated to modify the existing keys after the first snapshot.
FWIW, I am mostly relying on spec tests to test the interactions between these callbacks and the watch change aggregator.
* Adds the provided document to the internal list of document updates and its document key to the | ||
* given target's mapping. | ||
*/ | ||
// Visible for testing. |
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.
Shouldn't this comment be in the @interface
?
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.
Hmpf. I meant to use this in my tests, but instead I am just using handleDocumentChange
with a synthesized FSTDocumentWatchChange
in this port. Removed the "visible for testing" comment.
@@ -605,44 +541,91 @@ - (void)recordResponseForTargetID:(FSTBoxedTargetID *)targetID { | |||
* preventing in preventing race conditions for a target where events arrive but the server hasn't | |||
* yet acknowledged the intended change in state. | |||
*/ | |||
- (BOOL)isActiveTarget:(FSTBoxedTargetID *)targetID { | |||
- (BOOL)isActiveTarget:(FSTTargetID)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.
What's the point of this change? This forces a caller that has a boxed target ID to unbox so it can be reboxed here.
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 calling this method from three places (handleTargetChange, addDocument and removeDocument). Only handleTargetChange
uses the boxed target ID. queryDataForActiveTarget
uses both the boxed and the unboxed ID, so passing one or the other doesn't make a huge difference (if we ignore the object allocation).
I left this code as is for now.
FSTQueryData *queryData = self.listenTargets[targetID]; | ||
return (queryData && !self.pendingTargetResponses[targetID]) ? queryData : nil; | ||
- (FSTQueryData *_Nullable)queryDataForActiveTarget:(FSTTargetID)targetID { | ||
const auto &targetState = _targetStates.find(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.
Again auto targetState
.
In general, make the types of the locals match the types of the return type.
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.
// document. This resolves the limbo state of the document, removing it from | ||
// limboDocumentRefs. | ||
// | ||
// TODO(dimond): Ideally we would have an explicit lookup query instead resulting in an |
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 can probably kill this TODO. We're not going to tackle this separately.
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.
Poor Jonny... Removed.
// TODO(dimond): Ideally we would have an explicit lookup query instead resulting in an | ||
// explicit delete message and we could remove this special logic. | ||
FSTDocumentKey *key = [FSTDocumentKey keyWithPath:queryData.query.path]; | ||
if (self->_pendingDocumentUpdates.find(key) == self->_pendingDocumentUpdates.end() && |
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.
self->
is unnecessary here.
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.
Removed
I think you should create a branch from master to merge this and #1380 into. Once both of these PRs land on that branch, the combination can be merged into master in another PR and we can observe travis passing before actually merging it. |
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 addressed all comments. I also changed the base branch of this PR and its friend to mrschmidt-eventaggregator-and-tests
@@ -48,6 +49,9 @@ | |||
@class FSTObjectValue; | |||
@protocol FSTFilter; | |||
|
|||
using firebase::firestore::model::DocumentKey; |
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.
Make sense. Removed here and everywhere.
(const firebase::firestore::model::DocumentKeySet &)existingKeys; | ||
|
||
@end | ||
using firebase::firestore::model::DocumentKey; |
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
*/ | ||
+ (FSTUpdateMapping *)mappingWithAddedDocuments:(NSArray<FSTDocument *> *)added | ||
removedDocuments:(NSArray<FSTDocument *> *)removed; | ||
- (FSTQueryData *_Nullable)queryDataForTarget:(FSTBoxedTargetID *)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.
Addressed here and in the other method declarations.
* Creates an FSTTestTargetMetadataProvider that returns the given document for the existing key | ||
* callback (via `remoteKeysForTarget`) and marks queries as active (via `queryDataForTarget`). | ||
*/ | ||
+ (instancetype)withSingleResultAtKey:(DocumentKey)documentKey; |
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.
Renamed this and withEmptyResultAtKey
as per your suggestion.
*/ | ||
+ (instancetype)withEmptyResultAtKey:(DocumentKey)documentKey; | ||
|
||
- (instancetype) |
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 this initializer altogether.
* Adds the provided document to the internal list of document updates and its document key to the | ||
* given target's mapping. | ||
*/ | ||
// Visible for testing. |
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.
Hmpf. I meant to use this in my tests, but instead I am just using handleDocumentChange
with a synthesized FSTDocumentWatchChange
in this port. Removed the "visible for testing" comment.
@@ -605,44 +541,91 @@ - (void)recordResponseForTargetID:(FSTBoxedTargetID *)targetID { | |||
* preventing in preventing race conditions for a target where events arrive but the server hasn't | |||
* yet acknowledged the intended change in state. | |||
*/ | |||
- (BOOL)isActiveTarget:(FSTBoxedTargetID *)targetID { | |||
- (BOOL)isActiveTarget:(FSTTargetID)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.
I am calling this method from three places (handleTargetChange, addDocument and removeDocument). Only handleTargetChange
uses the boxed target ID. queryDataForActiveTarget
uses both the boxed and the unboxed ID, so passing one or the other doesn't make a huge difference (if we ignore the object allocation).
I left this code as is for now.
// document. This resolves the limbo state of the document, removing it from | ||
// limboDocumentRefs. | ||
// | ||
// TODO(dimond): Ideally we would have an explicit lookup query instead resulting in an |
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.
Poor Jonny... Removed.
// TODO(dimond): Ideally we would have an explicit lookup query instead resulting in an | ||
// explicit delete message and we could remove this special logic. | ||
FSTDocumentKey *key = [FSTDocumentKey keyWithPath:queryData.query.path]; | ||
if (self->_pendingDocumentUpdates.find(key) == self->_pendingDocumentUpdates.end() && |
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.
Removed
@@ -5,6 +5,19 @@ | |||
<BuildAction | |||
parallelizeBuildables = "YES" | |||
buildImplicitDependencies = "YES"> | |||
<BuildActionEntries> |
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.
Opened #1388 for this.
@@ -154,10 +144,10 @@ - (void)removeDocumentChangeForKey:(const DocumentKey &)documentKey; | |||
|
|||
@implementation FSTTargetState { | |||
/** | |||
* The number of pending responses (adds or removes) that we are waiting on. We only consider | |||
* The number of outstanding responses (adds or removes) that we are waiting on. We only consider | |||
* targets active that have no pending responses. |
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.
s/pending/outstanding/
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
@@ -171,22 +161,22 @@ @implementation FSTTargetState { | |||
- (instancetype)init { | |||
if (self = [super init]) { | |||
_resumeToken = [NSData data]; | |||
_pendingResponses = 0; | |||
_outstandingResponses = 0; | |||
|
|||
// We initialize to 'true' so that newly-added targets are included in the next RemoteEvent. | |||
_hasPendingChanges = true; |
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.
BOOL
values should get YES
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
- (void)testWillAccumulateDocumentAddedAndRemovedEvents { | ||
FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO); | ||
FSTDocument *doc2 = FSTTestDoc("docs/2", 2, @{ @"value" : @2 }, NO); | ||
NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap = |
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.
There's now some pretty subtle interaction in here and it's not entirely clear to me how these pieces fit together.
Could you add some comments about how they're supposed to work? For example, on top of listenForTargets:
, what does the resulting map represent? Listens previously established?
Another piece that's missing: what if you omit a target id from listensForTargets:
and then create an FSTDocumentWatchChange
referring to that target id. I think that would simulate an update to a previously listened-to target, but I'm not entirely sure today and I'll certainly have no idea in 6 months :-).
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 renamed the listen
and limboListen
helpers to queryDataForTarget/queryDataForLimboTarget
, which should make it make a bit more obvious what is going on.
If you use a target in the tests that is not included in the targetMap
, then FSTTestTargetMetadataProvider
will hit an assert telling you that the target is unknown.
I have added a bunch of method comments to the top of this test file and commented the first test case as an example. I hope that clears up some of the confusion.
documentKey:doc1.key | ||
document:doc1]; | ||
|
||
FSTWatchTargetChange *markCurrent = |
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 order here seems mixed up. You've got
- new change 1
- new change 2
- new aggregator
- use change 1
- use change 2
- new change 3
- use change 3
I suggest creating all the changes first then pushing them through the aggregator to make it obvious what the sequence of changes is. Alternatively, create and use right away as you've done below.
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 have changed all test cases to declare the variables much closer to where they are used. The new "layout" is now "create doc, create change, apply change". That should be cleaner than what I had before.
/** | ||
* Creates an FSTTestTargetMetadataProvider that returns the given document for the existing key | ||
* callback (via `remoteKeysForTarget`) and additionally marks the given targets as active (via | ||
* `queryDataForTarget`). |
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 comment is helpful but it assumes you understand everything about how the system works. It would be even more helpful if you framed this in higher-level terms of what this represents in a user visible way or in terms of what history of events we're simulating. I have no idea if it's accurate or not, but something like this:
Creates an
FSTTestTargetMetadataProvider
that behaves as if there's an established listen for each of the giventargets
, where each target has already seen query results containing just the givendocumentKey
.Internally this means that the
remoteKeysForTarget
callback for these targets will return thedocumentKey
and that targets will be returned as active from thequeryDataForTarget
target.
Similar feedback applies below.
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 (I mostly went with your comment).
std::unordered_map<FSTTargetID, FSTQueryData *> _queryData; | ||
} | ||
|
||
+ (instancetype)providerWithSingleResultForKey:(firebase::firestore::model::DocumentKey)documentKey |
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: you can use using
declarations in the .mm
files.
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.
documentUpdates: | ||
(std::map<firebase::firestore::model::DocumentKey, FSTMaybeDocument *>)documentUpdates | ||
(std::unordered_map<firebase::firestore::model::DocumentKey, |
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.
Optional: consider adding a using
alias for this type so you don't have to repeat it throughout.
using DocumentUpdateMap = std::unordered_map<
firebase::firestore::model::DocumentKey,
FSTMaybeDocument *,
firebase::firestore::model::DocumentKeyHash>;
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 this is a good optimization, but I opted not to do this, as this conflicts somewhat with the MaybeDocumentMap. Using the explicit type makes it really clear what is going on here.
removedDocuments = removedDocuments.insert(entry.first); | ||
break; | ||
default: | ||
HARD_FAIL("Encountered invalid change type: %s", entry.second); |
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: extra spaces in the 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.
Removed.
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 is a port of firebase/firebase-js-sdk#784. This PR also ports the test runner changes from firebase/firebase-js-sdk#812.
Spec test updates for this change are in a separate PR #1380 (to keep this PR somewhat sane).
Not that with the spec tests included in this PR the unit tests don't pass. They pass with #1380 merged in.
As this is my first larger PR after the C++ migration, there's likely some very general feedback that I should address before it makes sense to look at all the details.