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 { getMatrixRuntime } from "../../runtime.js";
|
||||||
import { ensureMatrixSdkLoggingConfigured } from "./logging.js";
|
import { ensureMatrixSdkLoggingConfigured } from "./logging.js";
|
||||||
import type { MatrixAuth, MatrixResolvedConfig } from "./types.js";
|
import type { MatrixAuth, MatrixResolvedConfig } from "./types.js";
|
||||||
|
import { importCredentials } from "../import-mutex.js";
|
||||||
|
|
||||||
function clean(value?: string): string {
|
function clean(value?: string): string {
|
||||||
return value?.trim() ?? "";
|
return value?.trim() ?? "";
|
||||||
|
|
@ -93,12 +94,13 @@ export async function resolveMatrixAuth(params?: {
|
||||||
const isDefaultAccount = normalizedAccountId === DEFAULT_ACCOUNT_ID || normalizedAccountId === "default";
|
const isDefaultAccount = normalizedAccountId === DEFAULT_ACCOUNT_ID || normalizedAccountId === "default";
|
||||||
|
|
||||||
// Only use cached credentials for default account
|
// Only use cached credentials for default account
|
||||||
|
// Use serialized import to prevent race conditions during parallel account startup
|
||||||
const {
|
const {
|
||||||
loadMatrixCredentials,
|
loadMatrixCredentials,
|
||||||
saveMatrixCredentials,
|
saveMatrixCredentials,
|
||||||
credentialsMatchConfig,
|
credentialsMatchConfig,
|
||||||
touchMatrixCredentials,
|
touchMatrixCredentials,
|
||||||
} = await import("../credentials.js");
|
} = await importCredentials();
|
||||||
|
|
||||||
const cached = isDefaultAccount ? loadMatrixCredentials(env) : null;
|
const cached = isDefaultAccount ? loadMatrixCredentials(env) : null;
|
||||||
const cachedCredentials =
|
const cachedCredentials =
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import {
|
||||||
SimpleFsStorageProvider,
|
SimpleFsStorageProvider,
|
||||||
RustSdkCryptoStorageProvider,
|
RustSdkCryptoStorageProvider,
|
||||||
} from "@vector-im/matrix-bot-sdk";
|
} from "@vector-im/matrix-bot-sdk";
|
||||||
|
|
||||||
|
import { importCryptoNodejs } from "../import-mutex.js";
|
||||||
import type { IStorageProvider, ICryptoStorageProvider } from "@vector-im/matrix-bot-sdk";
|
import type { IStorageProvider, ICryptoStorageProvider } from "@vector-im/matrix-bot-sdk";
|
||||||
|
|
||||||
import { ensureMatrixSdkLoggingConfigured } from "./logging.js";
|
import { ensureMatrixSdkLoggingConfigured } from "./logging.js";
|
||||||
|
|
@ -65,7 +67,8 @@ export async function createMatrixClient(params: {
|
||||||
fs.mkdirSync(storagePaths.cryptoPath, { recursive: true });
|
fs.mkdirSync(storagePaths.cryptoPath, { recursive: true });
|
||||||
|
|
||||||
try {
|
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(
|
cryptoStorage = new RustSdkCryptoStorageProvider(
|
||||||
storagePaths.cryptoPath,
|
storagePaths.cryptoPath,
|
||||||
StoreType.Sqlite,
|
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