Skip to content

Commit 6993ed1

Browse files
committed
fix(export): make export clientId collision-safe to prevent concurrent lost-update
Generate clientId as `-` instead of a bare Date.now(). The clientId is the pending-dir name and the cross-client export lock (export-validate's waitIfNeeded queue); a bare millisecond timestamp collides when two runners export to the same remote in the same ms, sharing one pending-dir and silently dropping one runner's update. Uses node's crypto so this core aspect gains no new component dependency.
1 parent d1789a8 commit 6993ed1

1 file changed

Lines changed: 11 additions & 1 deletion

File tree

scopes/scope/export/export.main.runtime.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import crypto from 'crypto';
12
import fs from 'fs-extra';
23
import type { CLIMain } from '@teambit/cli';
34
import { CLIAspect, MainRuntime } from '@teambit/cli';
@@ -678,7 +679,16 @@ if the scope name is wrong and you've already snapped/tagged, run "bit reset" to
678679

679680
async pushToRemotesCarefully(manyObjectsPerRemote: ObjectsPerRemote[], resumeExportId?: string) {
680681
const remotes = manyObjectsPerRemote.map((o) => o.remote);
681-
const clientId = resumeExportId || Date.now().toString();
682+
// The clientId is both the pending-dir name AND the cross-client export lock: `export-validate`'s
683+
// waitIfNeeded queue sorts pending-dir names and lets only the first proceed to validate+persist.
684+
// A pure `Date.now()` is not collision-safe — two exports to the same remote within the same
685+
// millisecond (e.g. concurrent CI runners pushing the same lane) get the same clientId, share one
686+
// pending-dir, collapse the queue to a single entry, and both validate against the pre-persist
687+
// state, silently losing one runner's update. A random suffix keeps the timestamp prefix (so the
688+
// sorted queue still roughly preserves arrival order) while making a same-millisecond collision
689+
// vanishingly unlikely (64 bits of randomness). Use node's built-in `crypto` rather than a
690+
// component helper so this core aspect doesn't gain a new component dependency.
691+
const clientId = resumeExportId || `${Date.now()}-${crypto.randomBytes(8).toString('hex')}`;
682692
await this.pushRemotesPendingDir(clientId, manyObjectsPerRemote, resumeExportId);
683693
await validateRemotes(remotes, clientId, Boolean(resumeExportId));
684694
// Intentionally no cleanup on `persistRemotes` failure: pending dirs are the substrate for

0 commit comments

Comments
 (0)