Skip to content

Commit 8b257f9

Browse files
Allow failing of individual transactions
1 parent a57dac5 commit 8b257f9

File tree

6 files changed

+111
-33
lines changed

6 files changed

+111
-33
lines changed

packages/firestore/src/local/indexeddb_persistence.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import {
6363
} from './lru_garbage_collector';
6464
import {
6565
Persistence,
66+
PersistenceAction,
6667
PersistenceTransaction,
6768
PersistenceTransactionMode,
6869
PRIMARY_LEASE_LOST_ERROR_MSG,
@@ -749,7 +750,7 @@ export class IndexedDbPersistence implements Persistence {
749750
}
750751

751752
runTransaction<T>(
752-
action: string,
753+
action: PersistenceAction,
753754
mode: PersistenceTransactionMode,
754755
transactionOperation: (
755756
transaction: PersistenceTransaction

packages/firestore/src/local/persistence.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,35 @@ export const PRIMARY_LEASE_LOST_ERROR_MSG =
3131
'The current tab is not in the required state to perform this operation. ' +
3232
'It might be necessary to refresh the browser tab.';
3333

34+
/**
35+
* The cumulative list of actions run against Persistence. This is used by the
36+
* Spec tests to fail specific types of actions.
37+
*/
38+
export type PersistenceAction =
39+
| 'Get next mutation batch'
40+
| 'read document'
41+
| 'Allocate target'
42+
| 'Release target'
43+
| 'Execute query'
44+
| 'Handle user change'
45+
| 'Locally write mutations'
46+
| 'Acknowledge batch'
47+
| 'Reject batch'
48+
| 'Get highest unacknowledged batch id'
49+
| 'Get last stream token'
50+
| 'Set last stream token'
51+
| 'Get last remote snapshot version'
52+
| 'Set last remote snapshot version'
53+
| 'Apply remote event'
54+
| 'notifyLocalViewChanges'
55+
| 'Remote document keys'
56+
| 'Collect garbage'
57+
| 'maybeGarbageCollectMultiClientState'
58+
| 'Lookup mutation documents'
59+
| 'Get target data'
60+
| 'Get new document changes'
61+
| 'Synchronize last document change read time';
62+
3463
/**
3564
* A base class representing a persistence transaction, encapsulating both the
3665
* transaction's sequence numbers as well as a list of onCommitted listeners.
@@ -249,7 +278,7 @@ export interface Persistence {
249278
* @return A promise that is resolved once the transaction completes.
250279
*/
251280
runTransaction<T>(
252-
action: string,
281+
action: PersistenceAction,
253282
mode: PersistenceTransactionMode,
254283
transactionOperation: (
255284
transaction: PersistenceTransaction

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

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@ describeSpec('Persistence Recovery', ['no-ios', 'no-android'], () => {
3434
.client(1)
3535
.expectPrimaryState(false)
3636
.userSets('collection/a', { v: 1 })
37-
.failDatabase()
37+
.failDatabaseTransactions({
38+
'Locally write mutations': true,
39+
'Synchronize last document change read time': true,
40+
'Lookup mutation documents': true
41+
})
3842
.client(0)
3943
.writeAcks('collection/a', 1, { expectUserCallback: false })
4044
.client(1)
@@ -64,7 +68,11 @@ describeSpec('Persistence Recovery', ['no-ios', 'no-android'], () => {
6468
.client(1)
6569
.expectPrimaryState(false)
6670
.userListens(query)
67-
.failDatabase()
71+
.failDatabaseTransactions({
72+
'Allocate target': true,
73+
'Lookup mutation documents': true,
74+
'Get new document changes': true
75+
})
6876
.client(0)
6977
.expectListen(query)
7078
.watchAcksFull(query, 1000)
@@ -84,7 +92,10 @@ describeSpec('Persistence Recovery', ['no-ios', 'no-android'], () => {
8492
return (
8593
client(0)
8694
.expectPrimaryState(true)
87-
.failDatabase()
95+
.failDatabaseTransactions({
96+
'Allocate target': true,
97+
'Get target data': true
98+
})
8899
.client(1)
89100
.userListens(query)
90101
.client(0)
@@ -94,7 +105,10 @@ describeSpec('Persistence Recovery', ['no-ios', 'no-android'], () => {
94105
.recoverDatabase()
95106
.runTimer(TimerId.AsyncQueueRetry)
96107
.expectListen(query)
97-
.failDatabase()
108+
.failDatabaseTransactions({
109+
'Allocate target': true,
110+
'Release target': true
111+
})
98112
.client(1)
99113
.userUnlistens(query)
100114
.client(0)
@@ -113,7 +127,12 @@ describeSpec('Persistence Recovery', ['no-ios', 'no-android'], () => {
113127
return spec()
114128
.userSets('collection/key1', { foo: 'a' })
115129
.expectNumOutstandingWrites(1)
116-
.failDatabase()
130+
.failDatabaseTransactions({
131+
'Locally write mutations': true,
132+
notifyLocalViewChanges: true,
133+
'Get next mutation batch': true,
134+
'Get last stream token': true
135+
})
117136
.userSets('collection/key2', { bar: 'b' })
118137
.expectUserCallbacks({ rejected: ['collection/key2'] })
119138
.recoverDatabase()
@@ -149,7 +168,11 @@ describeSpec('Persistence Recovery', ['no-ios', 'no-android'], () => {
149168
fromCache: true,
150169
hasPendingWrites: true
151170
})
152-
.failDatabase()
171+
.failDatabaseTransactions({
172+
'Locally write mutations': true,
173+
notifyLocalViewChanges: true,
174+
'Get next mutation batch': true
175+
})
153176
.userSets('collection/key2', { foo: 'b' })
154177
.expectUserCallbacks({ rejected: ['collection/key2'] })
155178
.recoverDatabase()

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {
4545
parseQuery,
4646
runSpec,
4747
SpecConfig,
48+
SpecDatabaseFailures,
4849
SpecDocument,
4950
SpecQuery,
5051
SpecQueryFilter,
@@ -435,12 +436,15 @@ export class SpecBuilder {
435436
return this;
436437
}
437438

438-
/** Fails all database operations until `recoverDatabase()` is called. */
439-
failDatabase(): this {
439+
/**
440+
* Fails the specified database transaction until `recoverDatabase()` is
441+
* called.
442+
*/
443+
failDatabaseTransactions(failureMode: SpecDatabaseFailures): this {
440444
this.nextStep();
441445
this.injectFailures = true;
442446
this.currentStep = {
443-
failDatabase: true
447+
failDatabase: failureMode
444448
};
445449
return this;
446450
}

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

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
import {
2424
GarbageCollectionScheduler,
2525
Persistence,
26+
PersistenceAction,
2627
PersistenceTransaction,
2728
PersistenceTransactionMode
2829
} from '../../../src/local/persistence';
@@ -36,6 +37,7 @@ import {
3637
MemoryPersistence
3738
} from '../../../src/local/memory_persistence';
3839
import { LruParams } from '../../../src/local/lru_garbage_collector';
40+
import { SpecDatabaseFailures } from './spec_test_runner';
3941
import { Connection, Stream } from '../../../src/remote/connection';
4042
import { StreamBridge } from '../../../src/remote/stream_bridge';
4143
import * as api from '../../../src/protos/firestore_proto_api';
@@ -56,22 +58,27 @@ import { expect } from 'chai';
5658
* transaction failures.
5759
*/
5860
export class MockMemoryPersistence extends MemoryPersistence {
59-
injectFailures = false;
61+
injectFailures?: SpecDatabaseFailures;
6062

6163
runTransaction<T>(
62-
action: string,
64+
action: PersistenceAction,
6365
mode: PersistenceTransactionMode,
6466
transactionOperation: (
6567
transaction: PersistenceTransaction
6668
) => PersistencePromise<T>
6769
): Promise<T> {
6870
if (this.injectFailures) {
69-
return Promise.reject(
70-
new IndexedDbTransactionError(new Error('Simulated retryable error'))
71-
);
72-
} else {
73-
return super.runTransaction(action, mode, transactionOperation);
71+
if (this.injectFailures[action] === undefined) {
72+
throw fail('Failure mode not specified for action: ' + action);
73+
}
74+
if (this.injectFailures[action]) {
75+
return Promise.reject(
76+
new IndexedDbTransactionError(new Error('Simulated retryable error'))
77+
);
78+
}
7479
}
80+
81+
return super.runTransaction(action, mode, transactionOperation);
7582
}
7683
}
7784

@@ -80,22 +87,26 @@ export class MockMemoryPersistence extends MemoryPersistence {
8087
* transaction failures.
8188
*/
8289
export class MockIndexedDbPersistence extends IndexedDbPersistence {
83-
injectFailures = false;
90+
injectFailures?: SpecDatabaseFailures;
8491

8592
runTransaction<T>(
86-
action: string,
93+
action: PersistenceAction,
8794
mode: PersistenceTransactionMode,
8895
transactionOperation: (
8996
transaction: PersistenceTransaction
9097
) => PersistencePromise<T>
9198
): Promise<T> {
9299
if (this.injectFailures) {
93-
return Promise.reject(
94-
new IndexedDbTransactionError(new Error('Simulated retryable error'))
95-
);
96-
} else {
97-
return super.runTransaction(action, mode, transactionOperation);
100+
if (this.injectFailures[action] === undefined) {
101+
throw fail('Failure mode not specified for action: ' + action);
102+
} else if (this.injectFailures[action]) {
103+
return Promise.reject(
104+
new IndexedDbTransactionError(new Error('Simulated retryable error'))
105+
);
106+
}
98107
}
108+
109+
return super.runTransaction(action, mode, transactionOperation);
99110
}
100111
}
101112

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

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ import {
113113
SharedWriteTracker
114114
} from './spec_test_components';
115115
import { IndexedDbPersistence } from '../../../src/local/indexeddb_persistence';
116+
import { PersistenceAction } from '../../../src/local/persistence';
116117

117118
const ARBITRARY_SEQUENCE_NUMBER = 2;
118119

@@ -341,8 +342,8 @@ abstract class TestRunner {
341342
} else if ('changeUser' in step) {
342343
return this.doChangeUser(step.changeUser!);
343344
} else if ('failDatabase' in step) {
344-
return step.failDatabase!
345-
? this.doFailDatabase()
345+
return step.failDatabase
346+
? this.doFailDatabase(step.failDatabase!)
346347
: this.doRecoverDatabase();
347348
} else {
348349
return fail('Unknown step: ' + JSON.stringify(step));
@@ -446,7 +447,7 @@ abstract class TestRunner {
446447
() => this.rejectedDocs.push(...documentKeys)
447448
);
448449

449-
if (!this.persistence.injectFailures) {
450+
if (this.persistence.injectFailures?.['Locally write mutations'] !== true) {
450451
this.sharedWrites.push(mutations);
451452
}
452453

@@ -717,12 +718,14 @@ abstract class TestRunner {
717718
);
718719
}
719720

720-
private async doFailDatabase(): Promise<void> {
721-
this.persistence.injectFailures = true;
721+
private async doFailDatabase(
722+
failActions: SpecDatabaseFailures
723+
): Promise<void> {
724+
this.persistence.injectFailures = failActions;
722725
}
723726

724727
private async doRecoverDatabase(): Promise<void> {
725-
this.persistence.injectFailures = false;
728+
this.persistence.injectFailures = undefined;
726729
}
727730

728731
private validateExpectedSnapshotEvents(
@@ -1181,6 +1184,13 @@ export interface SpecConfig {
11811184
maxConcurrentLimboResolutions?: number;
11821185
}
11831186

1187+
/** Specifies failure or success for a list of database actions. */
1188+
export type SpecDatabaseFailures = Partial<
1189+
{
1190+
readonly [key in PersistenceAction]: boolean;
1191+
}
1192+
>;
1193+
11841194
/**
11851195
* Union type for each step. The step consists of exactly one `field`
11861196
* set and optionally expected events in the `expect` field.
@@ -1225,8 +1235,8 @@ export interface SpecStep {
12251235
/** Fail a write */
12261236
failWrite?: SpecWriteFailure;
12271237

1228-
/** Fail all database transactions. */
1229-
failDatabase?: boolean;
1238+
/** Fails the listed database actions. */
1239+
failDatabase?: false | SpecDatabaseFailures;
12301240

12311241
/**
12321242
* Run a queued timer task (without waiting for the delay to expire). See

0 commit comments

Comments
 (0)