Skip to content

SQLite migration finalize: Promise.all([oldStorage.remove(), metaStorage.remove()]) bypasses TX_QUEUE_BY_DATABASE, causing "database is locked" on async single-connection adapters (expo-sqlite) #9024

Description

@silwalprabin

Our production RN sessions (Android and iOS) had schema migration block DB initialization: "Collection migration failed" and our app-level "Database initialization timed out". The native error was expo-sqlite finalizeAsync with SQLITE_LOCKED (code 6).
Version: RxDB/Premium Version 17.3.0
Our app migrates collections sequentially, but finalization appears concurrent:

  1. RxDB shares one SQLite connection per DB:
    export const DATABASE_STATE_BY_NAME: Map<string, DatabaseState> = new Map();
    export const RX_STORAGE_NAME_SQLITE = 'sqlite';
    /**
    * @link https://www.sqlite.org/inmemorydb.html
    */
    export const SQLITE_IN_MEMORY_DB_NAME = ':memory:';
    export function getDatabaseConnection(
    sqliteBasics: SQLiteBasics<any>,
    databaseName: string
    ): Promise<SQLiteDatabaseClass> {
    let state = DATABASE_STATE_BY_NAME.get(databaseName);
    if (!state) {
    state = {
    database: sqliteBasics.open(databaseName),
    sqliteBasics,
    openConnections: 1
    };
    DATABASE_STATE_BY_NAME.set(databaseName, state);
    } else {
    if (state.sqliteBasics !== sqliteBasics && databaseName !== SQLITE_IN_MEMORY_DB_NAME) {
    throw new Error('opened db with different creator method ' + databaseName + ' ' + state.sqliteBasics.debugId + ' ' + sqliteBasics.debugId);
    }
    state.openConnections = state.openConnections + 1;
    }
    return state.database;
    }
  2. The Expo adapter forwards all/run directly:
    export function getSQLiteBasicsExpoSQLiteAsync(
    openDB: any,
    options?: any,
    directory?: any
    ): SQLiteBasics<any> {
    return {
    open: async (name: string) => {
    return await openDB(name, options, directory);
    },
    all: async (db: any, queryWithParams: SQLiteQueryWithParams) => {
    const result = await db.getAllAsync(
    queryWithParams.query,
    queryWithParams.params
    );
    return result as any;
    },
    run: async (db: any, queryWithParams: SQLiteQueryWithParams) => {
    const ret = await db.runAsync(
    queryWithParams.query,
    queryWithParams.params
    );
    return ret as any;
    },
    async setPragma(db, key, value) {
    await db.execAsync('PRAGMA ' + key + ' = ' + value);
    },
    close: async (db: any) => {
    return await db.closeAsync();
    },
    journalMode: '',
    };
  3. Migration cleanup concurrently calls oldStorage.remove() and metaStorage.remove(), then cancels replication:
    await awaitRxStorageReplicationFirstInSync(replicationState);
    await awaitRxStorageReplicationInSync(replicationState);
    await this.updateStatusQueue;
    if (hasError) {
    await cancelRxStorageReplication(replicationState);
    await replicationMetaStorageInstance.close();
    throw hasError;
    }
    // cleanup old storages
    await Promise.all([
    oldStorage.remove(),
    replicationMetaStorageInstance.remove()
    ]);
    await cancelRxStorageReplication(replicationState);
    }

    In our reproduction, serializing SQLiteBasics operations per connection prevents the lock. We'd prefer an upstream-supported solution over a broad wrapper.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions