Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion components/legacy/scope-api/lib/put.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export type ComponentObjectsInput = {
};

export type PushOptions = {
clientId?: string; // timestamp in ms when the client started the request.
clientId?: string; // opaque export identifier (timestamp-prefixed unique string), used as the pending-dir name and queue/lock key.
persist?: boolean; // persist the objects immediately with no validation. (for legacy and bit-sign).
};

Expand Down
2 changes: 1 addition & 1 deletion components/legacy/scope/exceptions/client-id-in-use.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export default class ClientIdInUse extends BitError {
code: number;
constructor(public clientId: string) {
super(
`fatal: another client started exporting to the same scopes as yours within the exact same millisecond (${clientId}), please try again.`
`fatal: another client is already exporting to the same scopes as yours using the same export id (${clientId}), please try again.`
);
this.code = 136;
}
Expand Down
10 changes: 9 additions & 1 deletion scopes/scope/export/export.main.runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ComponentsList } from '@teambit/legacy.component-list';
import type { RemoveMain } from '@teambit/remove';
import { RemoveAspect } from '@teambit/remove';
import { hasWildcard } from '@teambit/legacy.utils';
import { generateRandomStr } from '@teambit/toolbox.string.random';
import type { Workspace } from '@teambit/workspace';
import { WorkspaceAspect, OutsideWorkspaceError } from '@teambit/workspace';
import type { Logger, LoggerMain } from '@teambit/logger';
Expand Down Expand Up @@ -678,7 +679,14 @@ if the scope name is wrong and you've already snapped/tagged, run "bit reset" to

async pushToRemotesCarefully(manyObjectsPerRemote: ObjectsPerRemote[], resumeExportId?: string) {
const remotes = manyObjectsPerRemote.map((o) => o.remote);
const clientId = resumeExportId || Date.now().toString();
// The clientId is both the pending-dir name AND the cross-client export lock: `export-validate`'s
// waitIfNeeded queue sorts pending-dir names and lets only the first proceed to validate+persist.
// A pure `Date.now()` is not collision-safe — two exports to the same remote within the same
// millisecond (e.g. concurrent CI runners pushing the same lane) get the same clientId, share one
// pending-dir, collapse the queue to a single entry, and both validate against the pre-persist
// state, silently losing one runner's update. A random suffix keeps the timestamp prefix (so the
// sorted queue still roughly preserves arrival order) while guaranteeing uniqueness.
const clientId = resumeExportId || `${Date.now()}-${generateRandomStr()}`;
await this.pushRemotesPendingDir(clientId, manyObjectsPerRemote, resumeExportId);
await validateRemotes(remotes, clientId, Boolean(resumeExportId));
// Intentionally no cleanup on `persistRemotes` failure: pending dirs are the substrate for
Expand Down