@opengeni/worker-bundle 0.28.3-canary.0 → 0.28.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,4 @@
1
- import { uploadWorkspaceArchiveSpool, type ObjectStorage } from "@opengeni/storage";
2
- import { randomUUID } from "node:crypto";
3
- import type { VerifiedHostWorkspaceArchive } from "@opengeni/runtime/sandbox";
1
+ import type { ObjectStorage } from "@opengeni/storage";
4
2
  import {
5
3
  parseWorkspaceArchiveObjectRef,
6
4
  workspaceArchiveObjectKey,
@@ -37,102 +35,43 @@ export async function putTarWorkspaceArchiveObject(input: {
37
35
  accountId: string;
38
36
  workspaceId: string;
39
37
  sandboxGroupId: string;
40
- archive:
41
- | { bytes: Uint8Array; descriptor: WorkspaceArchiveDescriptor }
42
- | VerifiedHostWorkspaceArchive;
38
+ archive: { bytes: Uint8Array; descriptor: WorkspaceArchiveDescriptor };
43
39
  }): Promise<WorkspaceArchiveObjectRef> {
44
- // Snapshot caller-owned metadata before any provider callback can mutate it.
45
- const descriptor = structuredClone(input.archive.descriptor);
46
- if (descriptor.version !== 1) {
40
+ if (input.archive.descriptor.version !== 1) {
47
41
  throw new Error("Object-storage workspace archives are portable tar only");
48
42
  }
49
43
  const key = workspaceArchiveObjectKey({
50
44
  accountId: input.accountId,
51
45
  workspaceId: input.workspaceId,
52
46
  sandboxGroupId: input.sandboxGroupId,
53
- revision: descriptor.revision,
54
- uploadId: randomUUID(),
47
+ revision: input.archive.descriptor.revision,
48
+ });
49
+ await input.objectStorage.putObject({
50
+ key,
51
+ contentType: "application/x-tar",
52
+ body: input.archive.bytes,
53
+ sha256: input.archive.descriptor.archiveSha256,
55
54
  });
56
- const archive = input.archive;
57
- if ("spool" in archive) {
58
- if (
59
- archive.spool.byteSize !== descriptor.archiveBytes ||
60
- archive.spool.sha256 !== descriptor.archiveSha256
61
- ) {
62
- throw new Error("Workspace archive spool does not match its verified descriptor");
63
- }
64
- }
65
- const source =
66
- "spool" in archive
67
- ? archive.spool
68
- : {
69
- path: "",
70
- byteSize: archive.bytes.byteLength,
71
- sha256: descriptor.archiveSha256,
72
- async *open() {
73
- for (let offset = 0; offset < archive.bytes.length; offset += 1024 * 1024)
74
- yield archive.bytes.subarray(offset, offset + 1024 * 1024);
75
- },
76
- async dispose() {},
77
- };
78
- try {
79
- await uploadWorkspaceArchiveSpool(input.objectStorage, key, {
80
- ...source,
81
- byteSize: descriptor.archiveBytes,
82
- sha256: descriptor.archiveSha256,
83
- open: source.open.bind(source),
84
- });
85
- } catch (error) {
86
- // This fresh locator has never been offered to database publication. An
87
- // ambiguous provider upload can leave an orphan, never overwrite a retained
88
- // archive or justify deleting another attempt's object.
89
- await input.objectStorage.deleteObject(key).catch(() => undefined);
90
- throw error;
91
- }
92
55
  return {
93
56
  schema: "sandbox_archive_object_v1",
94
57
  key,
95
- sha256: descriptor.archiveSha256,
96
- bytes: descriptor.archiveBytes,
58
+ sha256: input.archive.descriptor.archiveSha256,
59
+ bytes: input.archive.descriptor.archiveBytes,
97
60
  backend: input.objectStorage.backend,
98
61
  };
99
62
  }
100
63
 
101
- /** The caller owns a fresh candidate returned by this module. A thrown or lost
102
- * database response is UNKNOWN: it may have committed, so never delete on catch.
103
- * Only the transaction's exact candidate disposition can authorize cleanup. */
104
- export async function persistWorkspaceArchiveCandidate<
105
- T extends {
106
- wrote: boolean;
107
- candidateDisposition?: "adopted" | "already_referenced" | "unused";
108
- },
109
- >(input: {
110
- objectStorage?: ObjectStorage | null;
111
- ref?: WorkspaceArchiveObjectRef;
112
- persist: () => Promise<T>;
113
- }): Promise<T> {
114
- const objectStorage = input.objectStorage;
115
- const ref = input.ref ? { ...input.ref } : undefined;
116
- const result = await input.persist();
117
- if (objectStorage && ref && result.candidateDisposition === "unused") {
118
- await deleteUnpublishedWorkspaceArchiveObject(objectStorage, ref);
119
- }
120
- return result;
121
- }
122
-
123
64
  export async function putVersion1TarArchiveOrInline(input: {
124
65
  backend: string;
125
66
  objectStorage?: ObjectStorage | null | undefined;
126
67
  accountId: string;
127
68
  workspaceId: string;
128
69
  sandboxGroupId: string;
129
- archive:
130
- | VerifiedHostWorkspaceArchive
131
- | {
132
- bytes: Uint8Array;
133
- descriptor: WorkspaceArchiveDescriptor;
134
- base64: string;
135
- };
70
+ archive: {
71
+ bytes: Uint8Array;
72
+ descriptor: WorkspaceArchiveDescriptor;
73
+ base64: string;
74
+ };
136
75
  metrics?: {
137
76
  onWorkspaceArchiveObject?: (input: {
138
77
  outcome: "put" | "put_failed" | "deleted_unpublished";
@@ -144,15 +83,12 @@ export async function putVersion1TarArchiveOrInline(input: {
144
83
  workspaceArchiveRef?: WorkspaceArchiveObjectRef;
145
84
  }> {
146
85
  if (input.archive.descriptor.version !== 1) {
147
- if ("spool" in input.archive) throw new Error("Native snapshots cannot use a portable spool");
148
86
  return { workspaceArchive: input.archive.base64 };
149
87
  }
150
88
  if (input.backend === "opensandbox" && !input.objectStorage) {
151
89
  throw new WorkspaceArchiveObjectStorageRequiredError("opensandbox");
152
90
  }
153
91
  if (!input.objectStorage) {
154
- if ("spool" in input.archive)
155
- throw new WorkspaceArchiveObjectStorageRequiredError(input.backend);
156
92
  return { workspaceArchive: input.archive.base64 };
157
93
  }
158
94
  try {
@@ -161,7 +97,7 @@ export async function putVersion1TarArchiveOrInline(input: {
161
97
  accountId: input.accountId,
162
98
  workspaceId: input.workspaceId,
163
99
  sandboxGroupId: input.sandboxGroupId,
164
- archive: input.archive,
100
+ archive: { bytes: input.archive.bytes, descriptor: input.archive.descriptor },
165
101
  });
166
102
  try {
167
103
  input.metrics?.onWorkspaceArchiveObject?.({
@@ -55,9 +55,7 @@ import {
55
55
  type LeaseHolderKind,
56
56
  } from "@opengeni/db";
57
57
  import {
58
- captureWorkspaceArchiveForStorage,
59
- disposeWorkspaceArchive,
60
- type VerifiedWorkspaceArchivePayload,
58
+ captureVerifiedWorkspaceArchive,
61
59
  describeLegacyNativeSnapshotArchive,
62
60
  inlineWorkspaceArchiveForRestore,
63
61
  MODAL_EXEC_READINESS_TIMEOUT_MS,
@@ -81,15 +79,13 @@ import {
81
79
  type RuntimeMetricsHooks,
82
80
  type WorkspaceArchiveDescriptor,
83
81
  } from "@opengeni/runtime";
84
- import {
85
- downloadWorkspaceArchiveSpool,
86
- WorkspaceArchiveStorageError,
87
- type ObjectStorage,
88
- } from "@opengeni/storage";
82
+ import type { ObjectStorage } from "@opengeni/storage";
89
83
  import type { Observability } from "@opengeni/observability";
90
84
  import { parseWorkspaceArchiveObjectRef } from "@opengeni/contracts";
91
85
  import {
92
- persistWorkspaceArchiveCandidate,
86
+ collectWorkspaceArchiveObjectKeys,
87
+ deleteUnpublishedWorkspaceArchiveObject,
88
+ deleteWorkspaceArchiveObjectKeys,
93
89
  putVersion1TarArchiveOrInline,
94
90
  } from "./sandbox-archive-storage";
95
91
 
@@ -586,14 +582,6 @@ async function materializeSpawnEnvelopeArchive(
586
582
  "workspace archive object storage is not configured",
587
583
  );
588
584
  }
589
- const descriptor = parseWorkspaceArchiveDescriptor(sessionState.workspaceArchiveMeta);
590
- if (
591
- process.platform === "linux" &&
592
- descriptor?.version === 1 &&
593
- descriptor.workspace.projection === "sdk_local_archive_v1"
594
- ) {
595
- return envelope;
596
- }
597
585
  return {
598
586
  ...record,
599
587
  sessionState: await inlineWorkspaceArchiveForRestore(sessionState, (key) =>
@@ -910,10 +898,9 @@ async function persistWarmWorkspaceSnapshot(
910
898
  // first (the bounded-wait race Bugbot flagged).
911
899
  const capturedAtMs = Date.now();
912
900
  const registerCandidate = async (
913
- archive: VerifiedWorkspaceArchivePayload,
901
+ archive: Awaited<ReturnType<typeof captureVerifiedWorkspaceArchive>>,
914
902
  ): Promise<{ id: string } | null> => {
915
903
  if (
916
- archive.kind === "host_spool" ||
917
904
  archive.descriptor.version !== 2 ||
918
905
  (archive.descriptor.provider !== "modal_snapshot_filesystem" &&
919
906
  archive.descriptor.provider !== "modal_snapshot_directory")
@@ -951,65 +938,82 @@ async function persistWarmWorkspaceSnapshot(
951
938
  // turn signal resolves first. Its finally block is the only normal release
952
939
  // of the exact admission gate; a late callback cannot release a successor.
953
940
  const captureAndPublish = (async (): Promise<boolean> => {
954
- let archive: VerifiedWorkspaceArchivePayload | undefined;
955
941
  let candidate: { id: string } | null = null;
956
- let publicationAttempted = false;
942
+ let workspaceArchiveRef: Awaited<
943
+ ReturnType<typeof putVersion1TarArchiveOrInline>
944
+ >["workspaceArchiveRef"];
957
945
  try {
958
- archive = await captureWorkspaceArchiveForStorage(
959
- session,
960
- capturedAtMs,
961
- {
962
- requestId: claimed.claim.providerRequestId,
963
- strategy: capturePolicy.strategy,
964
- },
965
- Boolean(services.objectStorage),
966
- );
946
+ const archive = await captureVerifiedWorkspaceArchive(session, capturedAtMs, {
947
+ requestId: claimed.claim.providerRequestId,
948
+ strategy: capturePolicy.strategy,
949
+ });
967
950
  candidate = await registerCandidate(archive);
968
- const archiveDescriptor = archive.descriptor;
951
+ const priorLease = await readLease(db, ids.workspaceId, ids.sandboxGroupId);
952
+ const priorKeys = collectWorkspaceArchiveObjectKeys(
953
+ (priorLease?.resumeState as Record<string, unknown> | null | undefined) ?? null,
954
+ );
969
955
  const published = await putVersion1TarArchiveOrInline({
970
956
  backend: lease.backend,
971
957
  objectStorage: services.objectStorage,
972
958
  accountId: ids.accountId,
973
959
  workspaceId: ids.workspaceId,
974
960
  sandboxGroupId: ids.sandboxGroupId,
975
- archive,
961
+ archive: {
962
+ bytes: archive.bytes,
963
+ descriptor: archive.descriptor,
964
+ base64: archive.base64,
965
+ },
976
966
  ...(services.sandboxMetrics ? { metrics: services.sandboxMetrics } : {}),
977
967
  });
978
- publicationAttempted = true;
979
- const { wrote } = await persistWorkspaceArchiveCandidate({
980
- ...(services.objectStorage ? { objectStorage: services.objectStorage } : {}),
981
- ...(published.workspaceArchiveRef ? { ref: published.workspaceArchiveRef } : {}),
982
- persist: () =>
983
- persistWarmSnapshot(db, {
984
- accountId: ids.accountId,
985
- workspaceId: ids.workspaceId,
986
- sessionId: ids.sessionId,
987
- turnId: ids.turnId,
988
- attemptId: ids.attemptId,
989
- sandboxGroupId: ids.sandboxGroupId,
990
- expectedEpoch: leaseEpoch,
991
- expectedInstanceId: instanceId,
992
- expectedWorkspaceGeneration: claimed.claim.workspaceGeneration,
993
- captureId,
994
- workspaceArchiveMeta: archiveDescriptor,
995
- ...published,
996
- checkpointArtifactId: candidate?.id ?? null,
997
- minIntervalMs: force ? 0 : intervalMs,
998
- capturedAtMs,
999
- }),
968
+ workspaceArchiveRef = published.workspaceArchiveRef;
969
+ const { wrote } = await persistWarmSnapshot(db, {
970
+ accountId: ids.accountId,
971
+ workspaceId: ids.workspaceId,
972
+ sessionId: ids.sessionId,
973
+ turnId: ids.turnId,
974
+ attemptId: ids.attemptId,
975
+ sandboxGroupId: ids.sandboxGroupId,
976
+ expectedEpoch: leaseEpoch,
977
+ expectedInstanceId: instanceId,
978
+ expectedWorkspaceGeneration: claimed.claim.workspaceGeneration,
979
+ captureId,
980
+ workspaceArchiveMeta: archive.descriptor,
981
+ ...published,
982
+ checkpointArtifactId: candidate?.id ?? null,
983
+ minIntervalMs: force ? 0 : intervalMs,
984
+ capturedAtMs,
1000
985
  });
986
+ if (published.workspaceArchiveRef && services.objectStorage) {
987
+ if (!wrote) {
988
+ await deleteUnpublishedWorkspaceArchiveObject(
989
+ services.objectStorage,
990
+ published.workspaceArchiveRef,
991
+ services.sandboxMetrics,
992
+ );
993
+ } else {
994
+ const afterLease = await readLease(db, ids.workspaceId, ids.sandboxGroupId);
995
+ const afterKeys = collectWorkspaceArchiveObjectKeys(
996
+ (afterLease?.resumeState as Record<string, unknown> | null | undefined) ?? null,
997
+ );
998
+ await deleteWorkspaceArchiveObjectKeys(
999
+ services.objectStorage,
1000
+ [...priorKeys].filter((key) => !afterKeys.has(key)),
1001
+ ).catch(() => undefined);
1002
+ }
1003
+ }
1001
1004
  if (!wrote && candidate) {
1002
1005
  await abandonCandidate(candidate.id, "snapshot_publication_fenced");
1003
1006
  }
1004
1007
  return wrote;
1005
1008
  } catch (error) {
1006
- if (candidate && !publicationAttempted)
1007
- await abandonCandidate(candidate.id, "snapshot_capture_failed");
1009
+ await deleteUnpublishedWorkspaceArchiveObject(
1010
+ services.objectStorage,
1011
+ workspaceArchiveRef,
1012
+ services.sandboxMetrics,
1013
+ );
1014
+ if (candidate) await abandonCandidate(candidate.id, "snapshot_capture_failed");
1008
1015
  throw error;
1009
1016
  } finally {
1010
- await disposeWorkspaceArchive(archive).catch(() => {
1011
- console.warn("workspace archive spool cleanup failed");
1012
- });
1013
1017
  await releaseWorkspaceArchiveCapture(db, {
1014
1018
  accountId: ids.accountId,
1015
1019
  workspaceId: ids.workspaceId,
@@ -1500,26 +1504,6 @@ export async function resumeBoxForTurn(
1500
1504
  const established = await establishSandboxSessionFromEnvelope(settings, hydrateEnvelope, {
1501
1505
  sessionId: ids.sessionId,
1502
1506
  recovery: "create-or-restore",
1503
- ...(services.objectStorage
1504
- ? {
1505
- loadHostWorkspaceArchive: async (ref) => {
1506
- try {
1507
- return await downloadWorkspaceArchiveSpool(services.objectStorage!, ref.key, {
1508
- bytes: ref.bytes,
1509
- sha256: ref.sha256,
1510
- });
1511
- } catch (error) {
1512
- if (error instanceof WorkspaceArchiveStorageError) {
1513
- throw new WorkspaceArchiveIntegrityError(error.code, error.message, {
1514
- retryable: error.retryable,
1515
- cause: error,
1516
- });
1517
- }
1518
- throw error;
1519
- }
1520
- },
1521
- }
1522
- : {}),
1523
1507
  backendOverride: ids.backend as never,
1524
1508
  ...(ids.environment ? { environment: ids.environment } : {}),
1525
1509
  ...(services.sandboxMetrics ? { metrics: services.sandboxMetrics } : {}),