Skip to content

Commit b48a685

Browse files
optimization: retry opening DB only in LockedAsyncDatabaseAdapter, not in SharedSyncImplementation. This should make locking more fair.
1 parent 977b2a0 commit b48a685

File tree

2 files changed

+80
-82
lines changed

2 files changed

+80
-82
lines changed

packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -313,8 +313,13 @@ export class LockedAsyncDatabaseAdapter
313313
return this._acquireLock(async () => {
314314
let holdId: string | null = null;
315315
try {
316-
// We can't await this since it uses the same lock as we're in now.
317-
if (this.databaseOpenPromise) {
316+
/**
317+
* We can't await this since it uses the same lock as we're in now.
318+
* If there is a pending open, this call will throw.
319+
* If there is no pending open, but there is also no database - the open
320+
* might have failed. We need to re-open the database.
321+
*/
322+
if (this.databaseOpenPromise || !this._db) {
318323
throw new ConnectionClosedError('Connection is busy re-opening');
319324
}
320325

packages/web/src/worker/sync/SharedSyncImplementation.ts

Lines changed: 73 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -496,94 +496,87 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
496496
* Opens a worker wrapped database connection. Using the last connected client port.
497497
*/
498498
protected async openInternalDB() {
499-
while (true) {
500-
try {
501-
const client = await this.getRandomWrappedPort();
502-
if (!client) {
503-
// Should not really happen in practice
504-
throw new Error(`Could not open DB connection since no client is connected.`);
505-
}
499+
const client = await this.getRandomWrappedPort();
500+
if (!client) {
501+
// Should not really happen in practice
502+
throw new Error(`Could not open DB connection since no client is connected.`);
503+
}
506504

507-
// Fail-safe timeout for opening a database connection.
508-
const timeout = setTimeout(() => {
509-
abortController.abort();
510-
}, 10_000);
511-
512-
/**
513-
* Handle cases where the client might close while opening a connection.
514-
*/
515-
const abortController = new AbortController();
516-
const closeListener = () => {
517-
abortController.abort();
518-
};
505+
// Fail-safe timeout for opening a database connection.
506+
const timeout = setTimeout(() => {
507+
abortController.abort();
508+
}, 10_000);
509+
510+
/**
511+
* Handle cases where the client might close while opening a connection.
512+
*/
513+
const abortController = new AbortController();
514+
const closeListener = () => {
515+
abortController.abort();
516+
};
519517

520-
const removeCloseListener = () => {
521-
const index = client.closeListeners.indexOf(closeListener);
522-
if (index >= 0) {
523-
client.closeListeners.splice(index, 1);
524-
}
525-
};
518+
const removeCloseListener = () => {
519+
const index = client.closeListeners.indexOf(closeListener);
520+
if (index >= 0) {
521+
client.closeListeners.splice(index, 1);
522+
}
523+
};
526524

527-
client.closeListeners.push(closeListener);
525+
client.closeListeners.push(closeListener);
528526

529-
const workerPort = await withAbort({
530-
action: () => client.clientProvider.getDBWorkerPort(),
531-
signal: abortController.signal,
532-
cleanupOnAbort: (port) => {
533-
port.close();
534-
}
535-
}).catch((ex) => {
536-
removeCloseListener();
537-
throw ex;
538-
});
527+
const workerPort = await withAbort({
528+
action: () => client.clientProvider.getDBWorkerPort(),
529+
signal: abortController.signal,
530+
cleanupOnAbort: (port) => {
531+
port.close();
532+
}
533+
}).catch((ex) => {
534+
removeCloseListener();
535+
throw ex;
536+
});
539537

540-
const remote = Comlink.wrap<OpenAsyncDatabaseConnection>(workerPort);
541-
const identifier = this.syncParams!.dbParams.dbFilename;
542-
543-
/**
544-
* The open could fail if the tab is closed while we're busy opening the database.
545-
* This operation is typically executed inside an exclusive portMutex lock.
546-
* We typically execute the closeListeners using the portMutex in a different context.
547-
* We can't rely on the closeListeners to abort the operation if the tab is closed.
548-
*/
549-
const db = await withAbort({
550-
action: () => remote(this.syncParams!.dbParams),
551-
signal: abortController.signal,
552-
cleanupOnAbort: (db) => {
553-
db.close();
554-
}
555-
}).finally(() => {
556-
// We can remove the close listener here since we no longer need it past this point.
557-
removeCloseListener();
558-
});
538+
const remote = Comlink.wrap<OpenAsyncDatabaseConnection>(workerPort);
539+
const identifier = this.syncParams!.dbParams.dbFilename;
540+
541+
/**
542+
* The open could fail if the tab is closed while we're busy opening the database.
543+
* This operation is typically executed inside an exclusive portMutex lock.
544+
* We typically execute the closeListeners using the portMutex in a different context.
545+
* We can't rely on the closeListeners to abort the operation if the tab is closed.
546+
*/
547+
const db = await withAbort({
548+
action: () => remote(this.syncParams!.dbParams),
549+
signal: abortController.signal,
550+
cleanupOnAbort: (db) => {
551+
db.close();
552+
}
553+
}).finally(() => {
554+
// We can remove the close listener here since we no longer need it past this point.
555+
removeCloseListener();
556+
});
559557

560-
clearTimeout(timeout);
558+
clearTimeout(timeout);
561559

562-
const wrapped = new WorkerWrappedAsyncDatabaseConnection({
563-
remote,
564-
baseConnection: db,
565-
identifier,
566-
// It's possible for this worker to outlive the client hosting the database for us. We need to be prepared for
567-
// that and ensure pending requests are aborted when the tab is closed.
568-
remoteCanCloseUnexpectedly: true
569-
});
570-
client.closeListeners.push(async () => {
571-
this.logger.info('Aborting open connection because associated tab closed.');
572-
/**
573-
* Don't await this close operation. It might never resolve if the tab is closed.
574-
* We mark the remote as closed first, this will reject any pending requests.
575-
* We then call close. The close operation is configured to fire-and-forget, the main promise will reject immediately.
576-
*/
577-
wrapped.markRemoteClosed();
578-
wrapped.close().catch((ex) => this.logger.warn('error closing database connection', ex));
579-
});
560+
const wrapped = new WorkerWrappedAsyncDatabaseConnection({
561+
remote,
562+
baseConnection: db,
563+
identifier,
564+
// It's possible for this worker to outlive the client hosting the database for us. We need to be prepared for
565+
// that and ensure pending requests are aborted when the tab is closed.
566+
remoteCanCloseUnexpectedly: true
567+
});
568+
client.closeListeners.push(async () => {
569+
this.logger.info('Aborting open connection because associated tab closed.');
570+
/**
571+
* Don't await this close operation. It might never resolve if the tab is closed.
572+
* We mark the remote as closed first, this will reject any pending requests.
573+
* We then call close. The close operation is configured to fire-and-forget, the main promise will reject immediately.
574+
*/
575+
wrapped.markRemoteClosed();
576+
wrapped.close().catch((ex) => this.logger.warn('error closing database connection', ex));
577+
});
580578

581-
return wrapped;
582-
} catch (ex) {
583-
this.logger.warn('Error opening internal DB', ex);
584-
await new Promise((resolve) => setTimeout(resolve, 1000));
585-
}
586-
}
579+
return wrapped;
587580
}
588581

589582
/**

0 commit comments

Comments
 (0)