Skip to content

Commit 7f03511

Browse files
author
Greg Soltis
authored
Implement LRU Reference Delegate for Memory Persistence (#1237)
* Implement lru reference delegate for memory persistence
1 parent 80733ab commit 7f03511

11 files changed

+300
-34
lines changed

packages/firestore/src/local/indexeddb_mutation_queue.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,7 @@ export class IndexedDbMutationQueue implements MutationQueue {
472472
this.removeCachedMutationKeys(batch.batchId);
473473
if (this.garbageCollector !== null) {
474474
for (const key of removedDocuments) {
475+
// TODO(gsoltis): tell reference delegate that mutation was ack'd
475476
this.garbageCollector.addPotentialGarbageKey(key);
476477
}
477478
}

packages/firestore/src/local/memory_mutation_queue.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,7 @@ export class MemoryMutationQueue implements MutationQueue {
336336
if (this.garbageCollector !== null) {
337337
this.garbageCollector.addPotentialGarbageKey(key);
338338
}
339+
// TODO(gsoltis): tell reference delegate that mutation was ack'd
339340

340341
const ref = new DocReference(key, batch.batchId);
341342
references = references.delete(ref);

packages/firestore/src/local/memory_persistence.ts

Lines changed: 173 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,15 @@
1515
*/
1616

1717
import { User } from '../auth/user';
18+
import { DocumentKey } from '../model/document_key';
1819
import { debug } from '../util/log';
20+
import { ObjectMap } from '../util/obj_map';
21+
import { encode } from './encoded_resource_path';
22+
import {
23+
ActiveTargets,
24+
LruDelegate,
25+
LruGarbageCollector
26+
} from './lru_garbage_collector';
1927

2028
import { MemoryMutationQueue } from './memory_mutation_queue';
2129
import { MemoryQueryCache } from './memory_query_cache';
@@ -24,14 +32,16 @@ import { MutationQueue } from './mutation_queue';
2432
import {
2533
Persistence,
2634
PersistenceTransaction,
27-
PrimaryStateListener
35+
PrimaryStateListener,
36+
ReferenceDelegate
2837
} from './persistence';
2938
import { PersistencePromise } from './persistence_promise';
30-
import { QueryCache } from './query_cache';
31-
import { RemoteDocumentCache } from './remote_document_cache';
39+
import { QueryData } from './query_data';
40+
import { ReferenceSet } from './reference_set';
3241
import { ClientId } from './shared_client_state';
3342
import { ListenSequenceNumber } from '../core/types';
3443
import { ListenSequence } from '../core/listen_sequence';
44+
import * as obj from '../util/obj';
3545

3646
const LOG_TAG = 'MemoryPersistence';
3747

@@ -49,23 +59,39 @@ export class MemoryPersistence implements Persistence {
4959
*/
5060
private mutationQueues: { [user: string]: MutationQueue } = {};
5161
private remoteDocumentCache = new MemoryRemoteDocumentCache();
52-
private queryCache = new MemoryQueryCache();
62+
private readonly queryCache: MemoryQueryCache; // = new MemoryQueryCache();
63+
private readonly listenSequence = new ListenSequence(0);
5364

5465
private _started = false;
5566

67+
// TODO(gsoltis): remove option to be null once eager delegate is implemented.
68+
private _referenceDelegate: ReferenceDelegate | null;
69+
70+
static createLruPersistence(clientId: ClientId): MemoryPersistence {
71+
const persistence = new MemoryPersistence(clientId);
72+
persistence._referenceDelegate = new MemoryLruDelegate(persistence);
73+
return persistence;
74+
}
75+
5676
constructor(private readonly clientId: ClientId) {
5777
this._started = true;
78+
this.queryCache = new MemoryQueryCache(this);
5879
}
5980

60-
async shutdown(deleteData?: boolean): Promise<void> {
81+
shutdown(deleteData?: boolean): Promise<void> {
6182
// No durable state to ensure is closed on shutdown.
6283
this._started = false;
84+
return Promise.resolve();
6385
}
6486

6587
get started(): boolean {
6688
return this._started;
6789
}
6890

91+
get referenceDelegate(): ReferenceDelegate {
92+
return this._referenceDelegate!;
93+
}
94+
6995
async getActiveClients(): Promise<ClientId[]> {
7096
return [this.clientId];
7197
}
@@ -90,11 +116,11 @@ export class MemoryPersistence implements Persistence {
90116
return queue;
91117
}
92118

93-
getQueryCache(): QueryCache {
119+
getQueryCache(): MemoryQueryCache {
94120
return this.queryCache;
95121
}
96122

97-
getRemoteDocumentCache(): RemoteDocumentCache {
123+
getRemoteDocumentCache(): MemoryRemoteDocumentCache {
98124
return this.remoteDocumentCache;
99125
}
100126

@@ -107,9 +133,20 @@ export class MemoryPersistence implements Persistence {
107133
): Promise<T> {
108134
debug(LOG_TAG, 'Starting transaction:', action);
109135
return transactionOperation(
110-
new MemoryTransaction(ListenSequence.INVALID)
136+
new MemoryTransaction(this.listenSequence.next())
111137
).toPromise();
112138
}
139+
140+
mutationQueuesContainKey(
141+
transaction: PersistenceTransaction,
142+
key: DocumentKey
143+
): PersistencePromise<boolean> {
144+
return PersistencePromise.or(
145+
obj
146+
.values(this.mutationQueues)
147+
.map(queue => () => queue.containsKey(transaction, key))
148+
);
149+
}
113150
}
114151

115152
/**
@@ -119,3 +156,131 @@ export class MemoryPersistence implements Persistence {
119156
export class MemoryTransaction implements PersistenceTransaction {
120157
constructor(readonly currentSequenceNumber: ListenSequenceNumber) {}
121158
}
159+
160+
export class MemoryLruDelegate implements ReferenceDelegate, LruDelegate {
161+
private additionalReferences: ReferenceSet | null;
162+
private orphanedSequenceNumbers: ObjectMap<
163+
DocumentKey,
164+
ListenSequenceNumber
165+
> = new ObjectMap(k => encode(k.path));
166+
167+
readonly garbageCollector: LruGarbageCollector;
168+
169+
constructor(private readonly persistence: MemoryPersistence) {
170+
this.garbageCollector = new LruGarbageCollector(this);
171+
}
172+
173+
forEachTarget(
174+
txn: PersistenceTransaction,
175+
f: (q: QueryData) => void
176+
): PersistencePromise<void> {
177+
return this.persistence.getQueryCache().forEachTarget(txn, f);
178+
}
179+
180+
getTargetCount(txn: PersistenceTransaction): PersistencePromise<number> {
181+
return this.persistence.getQueryCache().getTargetCount(txn);
182+
}
183+
184+
forEachOrphanedDocumentSequenceNumber(
185+
txn: PersistenceTransaction,
186+
f: (sequenceNumber: ListenSequenceNumber) => void
187+
): PersistencePromise<void> {
188+
this.orphanedSequenceNumbers.forEach((_, sequenceNumber) =>
189+
f(sequenceNumber)
190+
);
191+
return PersistencePromise.resolve();
192+
}
193+
194+
setInMemoryPins(inMemoryPins: ReferenceSet): void {
195+
this.additionalReferences = inMemoryPins;
196+
}
197+
198+
removeTargets(
199+
txn: PersistenceTransaction,
200+
upperBound: ListenSequenceNumber,
201+
activeTargetIds: ActiveTargets
202+
): PersistencePromise<number> {
203+
return this.persistence
204+
.getQueryCache()
205+
.removeTargets(txn, upperBound, activeTargetIds);
206+
}
207+
208+
removeOrphanedDocuments(
209+
txn: PersistenceTransaction,
210+
upperBound: ListenSequenceNumber
211+
): PersistencePromise<number> {
212+
let count = 0;
213+
const cache = this.persistence.getRemoteDocumentCache();
214+
const p = cache.forEachDocumentKey(txn, key => {
215+
return this.isPinned(txn, key, upperBound).next(isPinned => {
216+
if (isPinned) {
217+
return PersistencePromise.resolve();
218+
} else {
219+
count++;
220+
return cache.removeEntry(txn, key);
221+
}
222+
});
223+
});
224+
return p.next(() => count);
225+
}
226+
227+
removeMutationReference(
228+
txn: PersistenceTransaction,
229+
key: DocumentKey
230+
): PersistencePromise<void> {
231+
this.orphanedSequenceNumbers.set(key, txn.currentSequenceNumber);
232+
return PersistencePromise.resolve();
233+
}
234+
235+
removeTarget(
236+
txn: PersistenceTransaction,
237+
queryData: QueryData
238+
): PersistencePromise<void> {
239+
const updated = queryData.copy({
240+
sequenceNumber: txn.currentSequenceNumber
241+
});
242+
return this.persistence.getQueryCache().updateQueryData(txn, updated);
243+
}
244+
245+
addReference(
246+
txn: PersistenceTransaction,
247+
key: DocumentKey
248+
): PersistencePromise<void> {
249+
this.orphanedSequenceNumbers.set(key, txn.currentSequenceNumber);
250+
return PersistencePromise.resolve();
251+
}
252+
253+
removeReference(
254+
txn: PersistenceTransaction,
255+
key: DocumentKey
256+
): PersistencePromise<void> {
257+
this.orphanedSequenceNumbers.set(key, txn.currentSequenceNumber);
258+
return PersistencePromise.resolve();
259+
}
260+
261+
updateLimboDocument(
262+
txn: PersistenceTransaction,
263+
key: DocumentKey
264+
): PersistencePromise<void> {
265+
this.orphanedSequenceNumbers.set(key, txn.currentSequenceNumber);
266+
return PersistencePromise.resolve();
267+
}
268+
269+
private isPinned(
270+
txn: PersistenceTransaction,
271+
key: DocumentKey,
272+
upperBound: ListenSequenceNumber
273+
): PersistencePromise<boolean> {
274+
return PersistencePromise.or([
275+
() => this.persistence.mutationQueuesContainKey(txn, key),
276+
() => this.additionalReferences!.containsKey(txn, key),
277+
() => this.persistence.getQueryCache().containsKey(txn, key),
278+
() => {
279+
const orphanedAt = this.orphanedSequenceNumbers.get(key);
280+
return PersistencePromise.resolve(
281+
orphanedAt !== undefined && orphanedAt > upperBound
282+
);
283+
}
284+
]);
285+
}
286+
}

packages/firestore/src/local/memory_query_cache.ts

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import { DocumentKey } from '../model/document_key';
2222
import { ObjectMap } from '../util/obj_map';
2323

2424
import { GarbageCollector } from './garbage_collector';
25+
import { ActiveTargets } from './lru_garbage_collector';
26+
import { MemoryPersistence } from './memory_persistence';
2527
import { PersistenceTransaction } from './persistence';
2628
import { PersistencePromise } from './persistence_promise';
2729
import { QueryCache } from './query_cache';
@@ -52,6 +54,20 @@ export class MemoryQueryCache implements QueryCache {
5254

5355
private targetIdGenerator = TargetIdGenerator.forQueryCache();
5456

57+
constructor(private readonly persistence: MemoryPersistence) {}
58+
59+
getTargetCount(txn: PersistenceTransaction): PersistencePromise<number> {
60+
return PersistencePromise.resolve(this.targetCount);
61+
}
62+
63+
forEachTarget(
64+
txn: PersistenceTransaction,
65+
f: (q: QueryData) => void
66+
): PersistencePromise<void> {
67+
this.queries.forEach((_, queryData) => f(queryData));
68+
return PersistencePromise.resolve();
69+
}
70+
5571
getLastRemoteSnapshotVersion(
5672
transaction: PersistenceTransaction
5773
): PersistencePromise<SnapshotVersion> {
@@ -134,6 +150,28 @@ export class MemoryQueryCache implements QueryCache {
134150
return PersistencePromise.resolve();
135151
}
136152

153+
removeTargets(
154+
transaction: PersistenceTransaction,
155+
upperBound: ListenSequenceNumber,
156+
activeTargetIds: ActiveTargets
157+
): PersistencePromise<number> {
158+
let count = 0;
159+
const removals: Array<PersistencePromise<void>> = [];
160+
this.queries.forEach((key, queryData) => {
161+
if (
162+
queryData.sequenceNumber <= upperBound &&
163+
!activeTargetIds[queryData.targetId]
164+
) {
165+
this.queries.delete(key);
166+
removals.push(
167+
this.removeMatchingKeysForTargetId(transaction, queryData.targetId)
168+
);
169+
count++;
170+
}
171+
});
172+
return PersistencePromise.waitFor(removals).next(() => count);
173+
}
174+
137175
getQueryCount(
138176
transaction: PersistenceTransaction
139177
): PersistencePromise<number> {
@@ -163,7 +201,14 @@ export class MemoryQueryCache implements QueryCache {
163201
targetId: TargetId
164202
): PersistencePromise<void> {
165203
this.references.addReferences(keys, targetId);
166-
return PersistencePromise.resolve();
204+
const referenceDelegate = this.persistence.referenceDelegate;
205+
const promises: Array<PersistencePromise<void>> = [];
206+
if (referenceDelegate) {
207+
keys.forEach(key => {
208+
promises.push(referenceDelegate.addReference(txn, key));
209+
});
210+
}
211+
return PersistencePromise.waitFor(promises);
167212
}
168213

169214
removeMatchingKeys(
@@ -172,7 +217,14 @@ export class MemoryQueryCache implements QueryCache {
172217
targetId: TargetId
173218
): PersistencePromise<void> {
174219
this.references.removeReferences(keys, targetId);
175-
return PersistencePromise.resolve();
220+
const referenceDelegate = this.persistence.referenceDelegate;
221+
const promises: Array<PersistencePromise<void>> = [];
222+
if (referenceDelegate) {
223+
keys.forEach(key => {
224+
promises.push(referenceDelegate.removeReference(txn, key));
225+
});
226+
}
227+
return PersistencePromise.waitFor(promises);
176228
}
177229

178230
removeMatchingKeysForTargetId(

packages/firestore/src/local/memory_remote_document_cache.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,17 @@ export class MemoryRemoteDocumentCache implements RemoteDocumentCache {
8282
return PersistencePromise.resolve(results);
8383
}
8484

85+
forEachDocumentKey(
86+
transaction: PersistenceTransaction,
87+
f: (key: DocumentKey) => PersistencePromise<void>
88+
): PersistencePromise<void> {
89+
const promises: Array<PersistencePromise<void>> = [];
90+
this.docs.forEach(key => {
91+
promises.push(f(key));
92+
});
93+
return PersistencePromise.waitFor(promises);
94+
}
95+
8596
getNewDocumentChanges(
8697
transaction: PersistenceTransaction
8798
): PersistencePromise<MaybeDocumentMap> {

packages/firestore/src/local/persistence.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,8 @@ export interface Persistence {
145145
*/
146146
readonly started: boolean;
147147

148+
readonly referenceDelegate: ReferenceDelegate;
149+
148150
/**
149151
* Releases any resources held during eager shutdown.
150152
*

0 commit comments

Comments
 (0)