From c0e38bb9906c6406703916809a906d355880ccfe Mon Sep 17 00:00:00 2001 From: Claudia Date: Sun, 1 Feb 2026 11:31:44 +0100 Subject: [PATCH] fix: serialize dynamic imports to prevent race condition on parallel account startup Problem: When multiple Matrix accounts start in parallel, they all trigger dynamic imports simultaneously. The Rust native crypto module (@matrix-org/matrix-sdk-crypto-nodejs) crashes when loaded in parallel. Solution: Add import-mutex.ts that caches import promises. Concurrent callers now await the same promise instead of triggering parallel imports. Files changed: - NEW: src/matrix/import-mutex.ts - serializedImport() utility - src/matrix/client/create-client.ts - use importCryptoNodejs() - src/matrix/client/config.ts - use importCredentials() --- src/matrix/client/config.ts | 4 +- src/matrix/client/create-client.ts | 5 ++- src/matrix/import-mutex.ts | 70 ++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 src/matrix/import-mutex.ts diff --git a/src/matrix/client/config.ts b/src/matrix/client/config.ts index cb9cc38b3..1cc28cc2b 100644 --- a/src/matrix/client/config.ts +++ b/src/matrix/client/config.ts @@ -5,6 +5,7 @@ import type { CoreConfig, MatrixAccountConfig, MatrixConfig } from "../types.js" import { getMatrixRuntime } from "../../runtime.js"; import { ensureMatrixSdkLoggingConfigured } from "./logging.js"; import type { MatrixAuth, MatrixResolvedConfig } from "./types.js"; +import { importCredentials } from "../import-mutex.js"; function clean(value?: string): string { return value?.trim() ?? ""; @@ -93,12 +94,13 @@ export async function resolveMatrixAuth(params?: { const isDefaultAccount = normalizedAccountId === DEFAULT_ACCOUNT_ID || normalizedAccountId === "default"; // Only use cached credentials for default account + // Use serialized import to prevent race conditions during parallel account startup const { loadMatrixCredentials, saveMatrixCredentials, credentialsMatchConfig, touchMatrixCredentials, - } = await import("../credentials.js"); + } = await importCredentials(); const cached = isDefaultAccount ? loadMatrixCredentials(env) : null; const cachedCredentials = diff --git a/src/matrix/client/create-client.ts b/src/matrix/client/create-client.ts index 874da7e92..3f8d11040 100644 --- a/src/matrix/client/create-client.ts +++ b/src/matrix/client/create-client.ts @@ -6,6 +6,8 @@ import { SimpleFsStorageProvider, RustSdkCryptoStorageProvider, } from "@vector-im/matrix-bot-sdk"; + +import { importCryptoNodejs } from "../import-mutex.js"; import type { IStorageProvider, ICryptoStorageProvider } from "@vector-im/matrix-bot-sdk"; import { ensureMatrixSdkLoggingConfigured } from "./logging.js"; @@ -65,7 +67,8 @@ export async function createMatrixClient(params: { fs.mkdirSync(storagePaths.cryptoPath, { recursive: true }); try { - const { StoreType } = await import("@matrix-org/matrix-sdk-crypto-nodejs"); + // Use serialized import to prevent race conditions with native Rust module + const { StoreType } = await importCryptoNodejs(); cryptoStorage = new RustSdkCryptoStorageProvider( storagePaths.cryptoPath, StoreType.Sqlite, diff --git a/src/matrix/import-mutex.ts b/src/matrix/import-mutex.ts new file mode 100644 index 000000000..caf145b8a --- /dev/null +++ b/src/matrix/import-mutex.ts @@ -0,0 +1,70 @@ +/** + * Import Mutex - Serializes dynamic imports to prevent race conditions + * + * Problem: When multiple Matrix accounts start in parallel, they all call + * dynamic imports simultaneously. Native modules (like @matrix-org/matrix-sdk-crypto-nodejs) + * can crash when loaded in parallel from multiple promises. + * + * Solution: Cache the import promise so that concurrent callers await the same promise + * instead of triggering parallel imports. + */ + +// Cache for import promises - key is module specifier +const importCache = new Map>(); + +/** + * Safely import a module with deduplication. + * If an import is already in progress, returns the existing promise. + * Once resolved, the result is cached for future calls. + */ +export async function serializedImport( + moduleSpecifier: string, + importFn: () => Promise +): Promise { + const existing = importCache.get(moduleSpecifier); + if (existing) { + return existing as Promise; + } + + const importPromise = importFn().catch((err) => { + // On failure, remove from cache to allow retry + importCache.delete(moduleSpecifier); + throw err; + }); + + importCache.set(moduleSpecifier, importPromise); + return importPromise; +} + +// Pre-cached imports for critical modules +let cryptoNodejsModule: typeof import("@matrix-org/matrix-sdk-crypto-nodejs") | null = null; +let credentialsModule: typeof import("./credentials.js") | null = null; + +/** + * Safely import the crypto-nodejs module (Rust native). + * This is the most critical one - parallel imports of native modules crash. + */ +export async function importCryptoNodejs(): Promise { + if (cryptoNodejsModule) return cryptoNodejsModule; + + const mod = await serializedImport( + "@matrix-org/matrix-sdk-crypto-nodejs", + () => import("@matrix-org/matrix-sdk-crypto-nodejs") + ); + cryptoNodejsModule = mod; + return mod; +} + +/** + * Safely import the credentials module. + */ +export async function importCredentials(): Promise { + if (credentialsModule) return credentialsModule; + + const mod = await serializedImport( + "../credentials.js", + () => import("./credentials.js") + ); + credentialsModule = mod; + return mod; +}