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()
This commit is contained in:
parent
8790ba5dad
commit
c0e38bb990
3 changed files with 77 additions and 2 deletions
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
70
src/matrix/import-mutex.ts
Normal file
70
src/matrix/import-mutex.ts
Normal file
|
|
@ -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<string, Promise<unknown>>();
|
||||
|
||||
/**
|
||||
* 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<T>(
|
||||
moduleSpecifier: string,
|
||||
importFn: () => Promise<T>
|
||||
): Promise<T> {
|
||||
const existing = importCache.get(moduleSpecifier);
|
||||
if (existing) {
|
||||
return existing as Promise<T>;
|
||||
}
|
||||
|
||||
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<typeof import("@matrix-org/matrix-sdk-crypto-nodejs")> {
|
||||
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<typeof import("./credentials.js")> {
|
||||
if (credentialsModule) return credentialsModule;
|
||||
|
||||
const mod = await serializedImport(
|
||||
"../credentials.js",
|
||||
() => import("./credentials.js")
|
||||
);
|
||||
credentialsModule = mod;
|
||||
return mod;
|
||||
}
|
||||
Loading…
Reference in a new issue