Skip to content
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

Fix RTDB query getData caching behavior #9707

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions FirebaseDatabase/Sources/Core/FRepo.m
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,7 @@ - (void)getData:(FIRDatabaseQuery *)query
}];
return;
}
[self.persistenceManager setQueryActive:querySpec];
NSNumber *tag = [self.serverSyncTree registerQuery:querySpec];
[self.connection
jmwski marked this conversation as resolved.
Show resolved Hide resolved
getDataAtPath:[query.path toString]
withParams:querySpec.params.wireProtocolParams
Expand Down Expand Up @@ -561,6 +561,7 @@ - (void)getData:(FIRDatabaseQuery *)query
userInfo:errorDict],
nil);
}];
[self.serverSyncTree unregisterQuery:[query querySpec]];
return;
}
[self.eventRaiser raiseCallback:^{
Expand All @@ -569,10 +570,19 @@ - (void)getData:(FIRDatabaseQuery *)query
}];
} else {
node = [FSnapshotUtilities nodeFrom:data];
[self.eventRaiser
raiseEvents:[self.serverSyncTree
applyServerOverwriteAtPath:[query path]
newData:node]];
if ([query.querySpec loadsAllData]) {
[self.eventRaiser
raiseEvents:[self.serverSyncTree
applyServerOverwriteAtPath:[query path]
newData:node]];
} else {
[self.eventRaiser
raiseEvents:[self.serverSyncTree
applyTaggedQueryOverwriteAtPath:[query
path]
newData:node
tagId:tag]];
}
[self.eventRaiser raiseCallback:^{
block(
nil,
Expand All @@ -583,7 +593,7 @@ - (void)getData:(FIRDatabaseQuery *)query
index:querySpec.index]]);
}];
}
[self.persistenceManager setQueryInactive:querySpec];
[self.serverSyncTree unregisterQuery:querySpec];
}];
}

Expand Down
4 changes: 4 additions & 0 deletions FirebaseDatabase/Sources/Core/FSyncPoint.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,9 @@
- (BOOL)viewExistsForQuery:(FQuerySpec *)query;
- (BOOL)hasCompleteView;
- (FView *)completeView;
- (void)registerQuery:(FQuerySpec *)query
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add comments for any new functions?

writesCache:(FWriteTreeRef *)writesCache
serverCache:(FCacheNode *)serverCache;
- (BOOL)unregisterQuery:(FQuerySpec *)query;

@end
31 changes: 23 additions & 8 deletions FirebaseDatabase/Sources/Core/FSyncPoint.m
Original file line number Diff line number Diff line change
Expand Up @@ -161,14 +161,9 @@ - (FView *)getView:(FQuerySpec *)query
return [[FView alloc] initWithQuery:query initialViewCache:viewCache];
}

/**
* Add an event callback for the specified query
* Returns an array of events to raise.
*/
- (NSArray *)addEventRegistration:(id<FEventRegistration>)eventRegistration
forNonExistingViewForQuery:(FQuerySpec *)query
writesCache:(FWriteTreeRef *)writesCache
serverCache:(FCacheNode *)serverCache {
- (void)registerQuery:(FQuerySpec *)query
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method does everything that addEventRegistration did before actually adding an event registration to the view.

writesCache:(FWriteTreeRef *)writesCache
serverCache:(FCacheNode *)serverCache {
NSAssert(self.views[query.params] == nil, @"Found view for query: %@",
query.params);
// TODO: make writesCache take flag for complete server node
Expand All @@ -187,6 +182,17 @@ - (NSArray *)addEventRegistration:(id<FEventRegistration>)eventRegistration
[self.persistenceManager setTrackedQueryKeys:allKeys forQuery:query];
}
self.views[query.params] = view;
}

/**
* Add an event callback for the specified query
* Returns an array of events to raise.
*/
- (NSArray *)addEventRegistration:(id<FEventRegistration>)eventRegistration
forNonExistingViewForQuery:(FQuerySpec *)query
writesCache:(FWriteTreeRef *)writesCache
serverCache:(FCacheNode *)serverCache {
[self registerQuery:query writesCache:writesCache serverCache:serverCache];
return [self addEventRegistration:eventRegistration
forExistingViewForQuery:query];
}
Expand All @@ -199,6 +205,15 @@ - (NSArray *)addEventRegistration:(id<FEventRegistration>)eventRegistration
return [view initialEvents:eventRegistration];
}

- (BOOL)unregisterQuery:(FQuerySpec *)query {
Copy link
Contributor Author

@jmwski jmwski Apr 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method just removes the query from the view map if no other query added an event registration to it.

There is quite a bit of logic that isn't brought over from removeEventRegistration. In particular, we are not:

  • Removing all empty views from a syncpoint if the getData query is default.
  • Generating any FCancelEvents for the query since there is no event registration associated with it.

I'm least sure about this function.

FView *view = self.views[query.params];
NSAssert(view != nil, @"no view for query %@", query);
if ([view isEmpty]) {
[self.views removeObjectForKey:query.params];
}
return [view isEmpty];
}

/**
* Remove event callback(s). Return cancelEvents if a cancelError is specified.
*
Expand Down
2 changes: 2 additions & 0 deletions FirebaseDatabase/Sources/Core/FSyncTree.h
Original file line number Diff line number Diff line change
Expand Up @@ -81,5 +81,7 @@
- (id<FNode>)getServerValue:(FQuerySpec *)query;
- (id<FNode>)calcCompleteEventCacheAtPath:(FPath *)path
excludeWriteIds:(NSArray *)writeIdsToExclude;
- (NSNumber *)registerQuery:(FQuerySpec *)querySpec;
- (void)unregisterQuery:(FQuerySpec *)querySpec;

@end
62 changes: 62 additions & 0 deletions FirebaseDatabase/Sources/Core/FSyncTree.m
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,15 @@ - (id)initWithPersistenceManager:(FPersistenceManager *)persistenceManager
return self;
}

- (NSNumber *)tagQuery:(FQuerySpec *)query {
NSAssert(self.queryToTagMap[query] == nil,
@"View does not exist, but we have a tag");
NSNumber *tagId = [self.queryTagCounter getAndIncrement];
self.queryToTagMap[query] = tagId;
self.tagToQueryMap[tagId] = query;
return tagId;
}

#pragma mark -
#pragma mark Apply Operations

Expand Down Expand Up @@ -448,6 +457,42 @@ - (NSArray *)applyTaggedServerRangeMergeAtPath:(FPath *)path
}
}

- (NSNumber *)registerQuery:(FQuerySpec *)query {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Query is used a lot, including for listeners. Should we call this something like "registerGet"?

FPath *path = query.path;

__block BOOL foundAncestorDefaultView = NO;
[self.syncPointTree
forEachOnPath:query.path
whileBlock:^BOOL(FPath *pathToSyncPoint, FSyncPoint *syncPoint) {
foundAncestorDefaultView =
foundAncestorDefaultView || [syncPoint hasCompleteView];
return !foundAncestorDefaultView;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't actually think we're using this block, since we aren't checking foundAncestorDefaultView later in the function

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, will remove this.

}];

[self.persistenceManager setQueryActive:query];

FSyncPoint *syncPoint = [self.syncPointTree valueAtPath:path];
if (syncPoint == nil) {
syncPoint = [[FSyncPoint alloc]
initWithPersistenceManager:self.persistenceManager];
self.syncPointTree = [self.syncPointTree setValue:syncPoint
atPath:path];
}
NSNumber *tag = nil;
BOOL viewAlreadyExists = [syncPoint viewExistsForQuery:query];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the code up until here is reused from addEventRegistration, can we extract this out?

if (!viewAlreadyExists) {
FWriteTreeRef *writesCache =
[self.pendingWriteTree childWritesForPath:path];
FCacheNode *serverCache = [self serverCacheForQuery:query];
[syncPoint registerQuery:query
writesCache:writesCache
serverCache:serverCache];
} else if (![query loadsAllData]) {
tag = [self tagQuery:query];
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be moved into the

if (!viewAlreadyExists) {
  ...
}

bock above.

}
return tag;
}

/**
* Add an event callback for the specified query
* @return NSArray of FEvent to raise.
Expand Down Expand Up @@ -575,6 +620,23 @@ - (FCacheNode *)serverCacheForQuery:(FQuerySpec *)query {
return serverCache;
}

- (void)unregisterQuery:(FQuerySpec *)query {
FPath *path = query.path;
FSyncPoint *maybeSyncPoint = [self.syncPointTree valueAtPath:path];

if (maybeSyncPoint &&
([query isDefault] || [maybeSyncPoint viewExistsForQuery:query])) {
BOOL removed = [maybeSyncPoint unregisterQuery:query];
if ([maybeSyncPoint isEmpty]) {
self.syncPointTree = [self.syncPointTree removeValueAtPath:path];
}
if (removed) {
[self removeTags:@[ query ]];
[self.persistenceManager setQueryInactive:query];
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: only call setQueryInactive when we just removed the last query in the view. This is consistent with how removeEventRegistration works.

}
}
}

/**
* Remove event callback(s).
*
Expand Down
22 changes: 22 additions & 0 deletions FirebaseDatabase/Tests/Integration/FIRDatabaseGetTests.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#import <Foundation/Foundation.h>
#import "FirebaseDatabase/Tests/Helpers/FTestBase.h"

@interface FIRDatabaseGetTests : FTestBase

@end
68 changes: 68 additions & 0 deletions FirebaseDatabase/Tests/Integration/FIRDatabaseGetTests.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#import "FirebaseDatabase/Tests/Integration/FIRDatabaseGetTests.h"
#import "FirebaseCore/Sources/Public/FirebaseCore/FIROptions.h"
#import "FirebaseDatabase/Sources/Api/Private/FIRDatabaseQuery_Private.h"
#import "FirebaseDatabase/Sources/Constants/FConstants.h"
#import "FirebaseDatabase/Sources/Core/FQuerySpec.h"
#import "FirebaseDatabase/Sources/Utilities/FUtilities.h"
#import "FirebaseDatabase/Tests/Helpers/FEventTester.h"
#import "FirebaseDatabase/Tests/Helpers/FIRFakeApp.h"
#import "FirebaseDatabase/Tests/Helpers/FTestExpectations.h"
#import "FirebaseDatabase/Tests/Helpers/FTupleEventTypeString.h"

@implementation FIRDatabaseGetTests
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have a test where we perform a get, then perform a listen before the get is finished?


- (void)testGetDoesntTriggerExtraListens {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also add a test to ensure that a subsequent listen for the same query returns the cached data immediately?

FIRDatabaseReference* ref = [FTestHelpers getRandomNode];
FIRDatabaseReference* root = [ref root];
FIRDatabaseReference* list = [root child:@"list"];

__block BOOL removeDone = NO;
[root removeValueWithCompletionBlock:^(NSError* error, FIRDatabaseReference* ref) {
removeDone = YES;
}];
WAIT_FOR(removeDone);

[self waitForCompletionOf:[list childByAutoId] setValue:@{@"name" : @"child1"}];
[self waitForCompletionOf:[list childByAutoId] setValue:@{@"name" : @"child2"}];

// The original report of this issue makes a listen call first, and then
// performs a getData. However, in the testing environment, if the listen
// is made first, it will round trip and cache results before get has a
// chance to run, at which point it reads from cache.
// https://github.com/firebase/firebase-ios-sdk/issues/8286
__block BOOL getDone = NO;
[[[list queryOrderedByChild:@"name"] queryEqualToValue:@"child2"]
getDataWithCompletionBlock:^(NSError* error, FIRDataSnapshot* snapshot) {
XCTAssertNil(error);
XCTAssertEqual(snapshot.childrenCount, 1L);
getDone = YES;
}];
__block NSInteger numListenEvents = 0L;
FIRDatabaseHandle handle = [list observeEventType:FIRDataEventTypeValue
withBlock:^(FIRDataSnapshot* snapshot) {
XCTAssertEqual(snapshot.childrenCount, 2L);
numListenEvents += 1;
}];
WAIT_FOR(getDone);
[NSThread sleepForTimeInterval:1];
XCTAssertEqual(numListenEvents, 1);
[list removeObserverWithHandle:handle];
}

@end