Skip to content

Commit 3a922f1

Browse files
committed
Add spec tests for LRU GC
1 parent e8b53c6 commit 3a922f1

23 files changed

+371
-121
lines changed

packages/firestore/src/api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export {
3636
initializeFirestore,
3737
getFirestore,
3838
enableIndexedDbPersistence,
39+
enableMemoryLRUGarbageCollection,
3940
enableMultiTabIndexedDbPersistence,
4041
clearIndexedDbPersistence,
4142
waitForPendingWrites,

packages/firestore/src/api/database.ts

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -324,27 +324,27 @@ export function enableMemoryLRUGarbageCollection(
324324
}
325325

326326
/**
327-
* Attempts to enable persistent storage, if possible. //
328-
* //
329-
* Must be called before any other functions (other than //
330-
* {@link initializeFirestore}, {@link (getFirestore:1)} or //
331-
* {@link clearIndexedDbPersistence}. //
332-
* //
333-
* If this fails, `enableIndexedDbPersistence()` will reject the promise it //
334-
* returns. Note that even after this failure, the {@link Firestore} instance will //
335-
* remain usable, however offline persistence will be disabled. //
336-
* //
337-
* There are several reasons why this can fail, which can be identified by //
338-
* the `code` on the error. //
339-
* //
340-
* * failed-precondition: The app is already open in another browser tab. //
341-
* * unimplemented: The browser is incompatible with the offline //
342-
* persistence implementation. //
343-
* //
344-
* @param firestore - The {@link Firestore} instance to enable persistence for. //
345-
* @param persistenceSettings - Optional settings object to configure //
346-
* persistence. //
347-
* @returns A `Promise` that represents successfully enabling persistent storage. //
327+
* Attempts to enable persistent storage, if possible.
328+
*
329+
* Must be called before any other functions (other than
330+
* {@link initializeFirestore}, {@link (getFirestore:1)} or
331+
* {@link clearIndexedDbPersistence}.
332+
*
333+
* If this fails, `enableIndexedDbPersistence()` will reject the promise it
334+
* returns. Note that even after this failure, the {@link Firestore} instance will
335+
* remain usable, however offline persistence will be disabled.
336+
*
337+
* There are several reasons why this can fail, which can be identified by
338+
* the `code` on the error.
339+
*
340+
* * failed-precondition: The app is already open in another browser tab.
341+
* * unimplemented: The browser is incompatible with the offline
342+
* persistence implementation.
343+
*
344+
* @param firestore - The {@link Firestore} instance to enable persistence for.
345+
* @param persistenceSettings - Optional settings object to configure
346+
* persistence.
347+
* @returns A `Promise` that represents successfully enabling persistent storage.
348348
*/
349349
export function enableIndexedDbPersistence(
350350
firestore: Firestore,

packages/firestore/src/core/component_provider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import {
5454
remoteStoreShutdown
5555
} from '../remote/remote_store';
5656
import { JsonProtoSerializer } from '../remote/serializer';
57+
import { hardAssert } from '../util/assert';
5758
import { AsyncQueue } from '../util/async_queue';
5859
import { Code, FirestoreError } from '../util/error';
5960

@@ -73,7 +74,6 @@ import {
7374
syncEngineSynchronizeWithChangedDocuments
7475
} from './sync_engine_impl';
7576
import { OnlineStateSource } from './types';
76-
import { hardAssert } from '../util/assert';
7777

7878
export interface ComponentConfiguration {
7979
asyncQueue: AsyncQueue;

packages/firestore/src/local/lru_garbage_collector_impl.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,10 @@ export class LruScheduler implements Scheduler {
176176
}
177177
}
178178

179-
/** Implements the steps for LRU garbage collection. */
180-
class LruGarbageCollectorImpl implements LruGarbageCollector {
179+
/**
180+
* Implements the steps for LRU garbage collection. Exported for testing purpose only.
181+
*/
182+
export class LruGarbageCollectorImpl implements LruGarbageCollector {
181183
constructor(
182184
private readonly delegate: LruDelegate,
183185
readonly params: LruParams

packages/firestore/test/integration/api/database.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { Deferred } from '@firebase/util';
2020
import { expect, use } from 'chai';
2121
import chaiAsPromised from 'chai-as-promised';
2222

23+
import { LRU_MINIMUM_CACHE_SIZE_BYTES } from '../../../src/local/lru_garbage_collector_impl';
2324
import { EventsAccumulator } from '../util/events_accumulator';
2425
import {
2526
addDoc,
@@ -67,6 +68,7 @@ import {
6768
} from '../util/firebase_export';
6869
import {
6970
apiDescribe,
71+
withEnsuredGcTestDb,
7072
withTestCollection,
7173
withTestDbsSettings,
7274
withTestDb,
@@ -1751,4 +1753,22 @@ apiDescribe('Database', (persistence: boolean) => {
17511753
}
17521754
);
17531755
});
1756+
1757+
it('Can get document from cache with GC enabled.', () => {
1758+
const initialData = { key: 'value' };
1759+
return withEnsuredGcTestDb(
1760+
persistence,
1761+
LRU_MINIMUM_CACHE_SIZE_BYTES,
1762+
async db => {
1763+
const docRef = doc(collection(db, 'test-collection'));
1764+
await setDoc(docRef, initialData);
1765+
return getDoc(docRef).then(doc => {
1766+
expect(doc.exists()).to.be.true;
1767+
expect(doc.metadata.fromCache).to.be.false;
1768+
expect(doc.metadata.hasPendingWrites).to.be.false;
1769+
expect(doc.data()).to.deep.equal(initialData);
1770+
});
1771+
}
1772+
);
1773+
});
17541774
});

packages/firestore/test/integration/api/get_options.test.ts

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616
*/
1717

1818
import { expect } from 'chai';
19+
import { LRU_MINIMUM_CACHE_SIZE_BYTES } from '../../../src/local/lru_garbage_collector_impl';
1920

2021
import {
22+
collection,
2123
deleteDoc,
2224
disableNetwork,
2325
doc,
@@ -34,7 +36,8 @@ import {
3436
toDataMap,
3537
apiDescribe,
3638
withTestCollection,
37-
withTestDocAndInitialData
39+
withTestDocAndInitialData,
40+
withEnsuredGcTestDb
3841
} from '../util/helpers';
3942

4043
apiDescribe('GetOptions', (persistence: boolean) => {
@@ -68,20 +71,24 @@ apiDescribe('GetOptions', (persistence: boolean) => {
6871

6972
it('get document while offline with default get options', () => {
7073
const initialData = { key: 'value' };
71-
return withTestDocAndInitialData(persistence, initialData, (docRef, db) => {
72-
// Register a snapshot to force the data to stay in the cache and not be
73-
// garbage collected.
74-
onSnapshot(docRef, () => {});
75-
return getDoc(docRef)
76-
.then(() => disableNetwork(db))
77-
.then(() => getDoc(docRef))
78-
.then(doc => {
79-
expect(doc.exists()).to.be.true;
80-
expect(doc.metadata.fromCache).to.be.true;
81-
expect(doc.metadata.hasPendingWrites).to.be.false;
82-
expect(doc.data()).to.deep.equal(initialData);
83-
});
84-
});
74+
// Use an instance with Gc turned on.
75+
return withEnsuredGcTestDb(
76+
persistence,
77+
LRU_MINIMUM_CACHE_SIZE_BYTES,
78+
async db => {
79+
const docRef = doc(collection(db, 'test-collection'));
80+
await setDoc(docRef, initialData);
81+
return getDoc(docRef)
82+
.then(() => disableNetwork(db))
83+
.then(() => getDoc(docRef))
84+
.then(doc => {
85+
expect(doc.exists()).to.be.true;
86+
expect(doc.metadata.fromCache).to.be.true;
87+
expect(doc.metadata.hasPendingWrites).to.be.false;
88+
expect(doc.data()).to.deep.equal(initialData);
89+
});
90+
}
91+
);
8592
});
8693

8794
it('get collection while offline with default get options', () => {

packages/firestore/test/integration/util/helpers.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ import {
3232
PrivateSettings,
3333
SnapshotListenOptions,
3434
newTestFirestore,
35-
newTestApp
35+
newTestApp,
36+
enableMemoryLRUGarbageCollection
3637
} from './firebase_export';
3738
import {
3839
ALT_PROJECT_ID,
@@ -141,6 +142,25 @@ export function withTestDb(
141142
});
142143
}
143144

145+
export function withEnsuredGcTestDb(
146+
persistence: boolean,
147+
sizeBytes: number,
148+
fn: (db: Firestore) => Promise<void>
149+
): Promise<void> {
150+
return withTestDbsSettings(
151+
persistence,
152+
DEFAULT_PROJECT_ID,
153+
{ ...DEFAULT_SETTINGS, cacheSizeBytes: sizeBytes },
154+
1,
155+
async ([db]) => {
156+
if (!persistence) {
157+
await enableMemoryLRUGarbageCollection(db);
158+
}
159+
return fn(db);
160+
}
161+
);
162+
}
163+
144164
/** Runs provided fn with a db for an alternate project id. */
145165
export function withAlternateTestDb(
146166
persistence: boolean,

packages/firestore/test/unit/specs/bundle_spec.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ describeSpec('Bundles:', ['no-ios'], () => {
178178
spec()
179179
// TODO(b/160878667): Figure out what happens when memory eager GC is on
180180
// a bundle is loaded.
181-
.withGCEnabled(false)
181+
.withEagerGCForMemoryPersistence(false)
182182
.userListens(query1)
183183
.watchAcksFull(query1, 250, docA)
184184
.expectEvents(query1, {
@@ -224,7 +224,7 @@ describeSpec('Bundles:', ['no-ios'], () => {
224224

225225
return (
226226
spec()
227-
.withGCEnabled(false)
227+
.withEagerGCForMemoryPersistence(false)
228228
.userListens(query1)
229229
.watchAcksFull(query1, 250, docA)
230230
.expectEvents(query1, {
@@ -260,7 +260,7 @@ describeSpec('Bundles:', ['no-ios'], () => {
260260

261261
return (
262262
spec()
263-
.withGCEnabled(false)
263+
.withEagerGCForMemoryPersistence(false)
264264
.userListens(query1)
265265
.watchAcksFull(query1, 250)
266266
// Backend tells is there is no such doc.

packages/firestore/test/unit/specs/existence_filter_spec.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ describeSpec('Existence Filters:', [], () => {
6969
const doc1 = doc('collection/1', 2000, { v: 2 });
7070
return (
7171
spec()
72-
.withGCEnabled(false)
72+
.withEagerGCForMemoryPersistence(false)
7373
.userListens(query1)
7474
.watchAcksFull(query1, 1000, doc1)
7575
.expectEvents(query1, { added: [doc1] })
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/**
2+
* @license
3+
* Copyright 2023 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { setLogLevel } from '@firebase/logger';
19+
import { doc, filter, query } from '../../util/helpers';
20+
21+
import { describeSpec, specTest } from './describe_spec';
22+
import { spec } from './spec_builder';
23+
24+
describeSpec('Garbage Collection:', [], () => {
25+
specTest(
26+
'Contents of query are cleared when listen is removed.',
27+
['eager-gc'],
28+
'Explicitly tests eager GC behavior',
29+
() => {
30+
const query1 = query('collection');
31+
const docA = doc('collection/a', 1000, { key: 'a' });
32+
return (
33+
spec()
34+
.userListens(query1)
35+
.watchAcksFull(query1, 1000, docA)
36+
.expectEvents(query1, { added: [docA] })
37+
.userUnlistens(query1)
38+
// should get no events.
39+
.userListens(query1)
40+
);
41+
}
42+
);
43+
44+
specTest('Contents of query are kept after listen is removed.', [], () => {
45+
const query1 = query('collection');
46+
const docA = doc('collection/a', 1000, { key: 'a' });
47+
return spec()
48+
.withEagerGCForMemoryPersistence(false)
49+
.userListens(query1)
50+
.watchAcksFull(query1, 1000, docA)
51+
.expectEvents(query1, { added: [docA] })
52+
.userUnlistens(query1)
53+
.userListens(query1)
54+
.expectListen(query1, { resumeToken: 'resume-token-1000' })
55+
.expectEvents(query1, { added: [docA], fromCache: true });
56+
});
57+
58+
specTest(
59+
'Contents of query are kept after listen is removed, and GC threshold is not reached',
60+
[],
61+
() => {
62+
const query1 = query('collection');
63+
const docA = doc('collection/a', 1000, { key: 'a' });
64+
return spec()
65+
.withEagerGCForMemoryPersistence(false)
66+
.userListens(query1)
67+
.watchAcksFull(query1, 1000, docA)
68+
.expectEvents(query1, { added: [docA] })
69+
.userUnlistens(query1)
70+
.triggerLruGC(1000)
71+
.userListens(query1)
72+
.expectListen(query1, { resumeToken: 'resume-token-1000' })
73+
.expectEvents(query1, { added: [docA], fromCache: true });
74+
}
75+
);
76+
77+
specTest(
78+
'Contents of query are kept after listen is removed, and GC threshold is reached',
79+
[],
80+
() => {
81+
const query1 = query('collection');
82+
const docA = doc('collection/a', 1000, { key: 'a' });
83+
return (
84+
spec()
85+
.withEagerGCForMemoryPersistence(false)
86+
.userListens(query1)
87+
.watchAcksFull(query1, 1000, docA)
88+
.expectEvents(query1, { added: [docA] })
89+
.userUnlistens(query1)
90+
.triggerLruGC(1)
91+
.removeExpectedTargetMapping(query1)
92+
// should get no events.
93+
.userListens(query1)
94+
);
95+
}
96+
);
97+
98+
specTest(
99+
'Contents of active query are kept while inactive results are removed after GC',
100+
[],
101+
() => {
102+
const queryFull = query('collection');
103+
const queryA = query('collection', filter('key', '==', 'a'));
104+
const docA = doc('collection/a', 1000, { key: 'a' });
105+
const docB = doc('collection/b', 1000, { key: 'b' });
106+
const docC = doc('collection/c', 1000, { key: 'c' });
107+
const docCMutated = doc('collection/c', 1000, {
108+
key: 'c',
109+
extra: 'extra'
110+
}).setHasLocalMutations();
111+
const docD = doc('collection/d', 1000, { key: 'd' });
112+
return (
113+
spec()
114+
.withEagerGCForMemoryPersistence(false)
115+
.userListens(queryFull)
116+
.watchAcksFull(queryFull, 1000, docA, docB, docC, docD)
117+
.expectEvents(queryFull, { added: [docA, docB, docC, docD] })
118+
.userUnlistens(queryFull)
119+
.userListens(queryA)
120+
.expectEvents(queryA, { added: [docA], fromCache: true })
121+
.watchAcksFull(queryA, 1500, docA)
122+
.expectEvents(queryA, { fromCache: false })
123+
.userSets('collection/c', { key: 'c', extra: 'extra' })
124+
.triggerLruGC(1)
125+
.removeExpectedTargetMapping(queryFull)
126+
.userUnlistens(queryA)
127+
// should get no events.
128+
.userListens(queryFull)
129+
.expectEvents(queryFull, {
130+
added: [docA, docCMutated],
131+
hasPendingWrites: true,
132+
fromCache: true
133+
})
134+
);
135+
}
136+
);
137+
});

0 commit comments

Comments
 (0)