Skip to content

Adding getActiveClients() #586

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

Merged
merged 4 commits into from
Mar 22, 2018
Merged
Show file tree
Hide file tree
Changes from 2 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
13 changes: 13 additions & 0 deletions packages/firestore/src/local/indexeddb_persistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,19 @@ export class IndexedDbPersistence implements Persistence {
});
}

getActiveClients(): Promise<ClientKey[]> {
const clientKeys: ClientKey[] = [];
return this.simpleDb
.runTransaction('readonly', [DbClientMetadata.store], txn => {
return clientMetadataStore(txn).iterate((key, value) => {
if (this.isWithinMaxAge(value.updateTimeMs)) {
clientKeys.push(value.clientKey);
}
});
})
.then(() => clientKeys);
}

getMutationQueue(user: User): MutationQueue {
return IndexedDbMutationQueue.forUser(user, this.serializer);
}
Expand Down
7 changes: 7 additions & 0 deletions packages/firestore/src/local/memory_persistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ import {
import { PersistencePromise } from './persistence_promise';
import { QueryCache } from './query_cache';
import { RemoteDocumentCache } from './remote_document_cache';
import { ClientKey } from './shared_client_state';
import { AsyncQueue } from '../util/async_queue';
import { AutoId } from '../util/misc';

const LOG_TAG = 'MemoryPersistence';

Expand All @@ -49,6 +51,7 @@ export class MemoryPersistence implements Persistence {
private mutationQueues: { [user: string]: MutationQueue } = {};
private remoteDocumentCache = new MemoryRemoteDocumentCache();
private queryCache = new MemoryQueryCache();
private readonly clientId: ClientKey = AutoId.newId();

private started = false;

Expand All @@ -66,6 +69,10 @@ export class MemoryPersistence implements Persistence {
this.started = false;
}

async getActiveClients(): Promise<ClientKey[]> {
return [this.clientId];
}

setPrimaryStateListener(primaryStateListener: PrimaryStateListener) {
// All clients using memory persistence act as primary.
this.queue.enqueue(() => primaryStateListener(true));
Expand Down
12 changes: 12 additions & 0 deletions packages/firestore/src/local/persistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { PersistenceTransaction } from './persistence';
import { PersistencePromise } from './persistence_promise';
import { QueryCache } from './query_cache';
import { RemoteDocumentCache } from './remote_document_cache';
import { ClientKey } from './shared_client_state';

/**
* Opaque interface representing a persistence transaction.
Expand Down Expand Up @@ -94,9 +95,20 @@ export interface Persistence {
* Registers a listener that gets called when the primary state of the
* instance changes. Upon registering, this listener is invoked immediately
* with the current primary state.
*
* PORTING NOTE: This is only used for Web multi-tab.
*/
setPrimaryStateListener(primaryStateListener: PrimaryStateListener);

/**
* Returns the IDs of the clients that are currently active. If multi-tab
* is not supported, returns an array that only contains the local client's
* ID.
*
* PORTING NOTE: This is only used for Web multi-tab.
*/
getActiveClients(): Promise<ClientKey[]>;

/**
* Returns a MutationQueue representing the persisted mutations for the
* given user.
Expand Down
14 changes: 14 additions & 0 deletions packages/firestore/test/unit/specs/persistence_spec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,20 @@ describeSpec('Persistence:', [], () => {
);
});

specTest('Detects all active clients', ['multi-client'], () => {
return (
client(0)
// While we don't verify the client's visibility in this test, the spec
// test framework requires an explicit action before setting an
// expectation.
.becomeHidden()
.expectNumActiveClients(1)
.client(1)
.becomeVisible()
.expectNumActiveClients(2)
);
});

specTest('Single tab acquires primary lease', ['multi-client'], () => {
// This test simulates primary state handoff between two background tabs.
// With all instances in the background, the first active tab acquires
Expand Down
13 changes: 13 additions & 0 deletions packages/firestore/test/unit/specs/spec_builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,14 @@ export class SpecBuilder {
return this;
}

expectNumActiveClients(num: number): SpecBuilder {
this.assertStep('Expectations require previous step');
const currentStep = this.currentStep!;
currentStep.stateExpect = currentStep.stateExpect || {};
currentStep.stateExpect.numActiveClients = num;
return this;
}

expectPrimaryState(isPrimary: boolean): SpecBuilder {
this.assertStep('Expectations requires previous step');
const currentStep = this.currentStep!;
Expand Down Expand Up @@ -928,6 +936,11 @@ export class MultiClientSpecBuilder extends SpecBuilder {
return this;
}

expectNumActiveClients(num: number): MultiClientSpecBuilder {
super.expectNumActiveClients(num);
return this;
}

expectPrimaryState(isPrimary: boolean): MultiClientSpecBuilder {
super.expectPrimaryState(isPrimary);
return this;
Expand Down
39 changes: 27 additions & 12 deletions packages/firestore/test/unit/specs/spec_test_runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,8 @@ interface OutstandingWrite {
abstract class TestRunner {
protected queue: AsyncQueue;

private started = false;

private connection: MockConnection;
private eventManager: EventManager;
private syncEngine: SyncEngine;
Expand Down Expand Up @@ -419,15 +421,19 @@ abstract class TestRunner {
serializer: JsonProtoSerializer
): Persistence;

async start(): Promise<void> {
this.connection.reset();
await this.persistence.start();
await this.localStore.start();
await this.remoteStore.start();
async startIfNeeded(): Promise<void> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why does this need to be idempotent now? I'm not exactly following what changed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The way this was implemented before called start() for all clients before execution any of the steps in the tests. That meant by the time client(0).expectNumActiveClients(1) was called, we had already initialized both clients, causing this test to fail.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh... I see. We're creating clients at the start, but not starting them until later... Would it be possible / make sense to just not create them until needed?

I'm a bit worried about making SpecTestRunner deviate too much from other platforms. If nothing else, it seems like we should have some "PORTING NOTE" comments if we're going to change how start() works here...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added a PORTING NOTE comment and moved this logic to an inner function (ensureRunner).

if (!this.started) {
this.connection.reset();
await this.persistence.start();
await this.localStore.start();
await this.remoteStore.start();

this.persistence.setPrimaryStateListener(isPrimary =>
this.syncEngine.applyPrimaryState(isPrimary)
);
this.persistence.setPrimaryStateListener(isPrimary =>
this.syncEngine.applyPrimaryState(isPrimary)
);

this.started = true;
}
}

async shutdown(): Promise<void> {
Expand All @@ -440,7 +446,7 @@ abstract class TestRunner {
await this.doStep(step);
await this.queue.drain();
this.validateStepExpectations(step.expect!);
this.validateStateExpectations(step.stateExpect!);
await this.validateStateExpectations(step.stateExpect!);
this.eventList = [];
}

Expand Down Expand Up @@ -847,13 +853,19 @@ abstract class TestRunner {
}
}

private validateStateExpectations(expectation: StateExpectation): void {
private async validateStateExpectations(
expectation: StateExpectation
): Promise<void> {
if (expectation) {
if ('numOutstandingWrites' in expectation) {
expect(this.remoteStore.outstandingWrites()).to.equal(
expectation.numOutstandingWrites
);
}
if ('numActiveClients' in expectation) {
const activeClients = await this.persistence.getActiveClients();
expect(activeClients.length).to.equal(expectation.numActiveClients);
}
if ('writeStreamRequestCount' in expectation) {
expect(this.connection.writeStreamRequestCount).to.equal(
expectation.writeStreamRequestCount
Expand Down Expand Up @@ -1154,15 +1166,16 @@ export async function runSpec(
} else {
runners.push(new MemoryTestRunner(name, platform, config));
}
await runners[i].start();
}
let lastStep = null;
let count = 0;
try {
await sequence(steps, async step => {
++count;
lastStep = step;
return runners[step.clientIndex || 0].run(step);
const clientIndex = step.clientIndex || 0;
await runners[clientIndex].startIfNeeded();
return runners[clientIndex].run(step);
});
} catch (err) {
console.warn(
Expand Down Expand Up @@ -1396,6 +1409,8 @@ export interface SpecExpectation {
export interface StateExpectation {
/** Number of outstanding writes in the datastore queue. */
numOutstandingWrites?: number;
/** Number of clients currently marked active. Used in multi-client tests. */
numActiveClients?: number;
/** Number of requests sent to the write stream. */
writeStreamRequestCount?: number;
/** Number of requests sent to the watch stream. */
Expand Down