@namzu/sdk 20.3.0 → 20.4.0

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.
Files changed (50) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/dist/public-runtime.d.ts +1 -0
  3. package/dist/public-runtime.d.ts.map +1 -1
  4. package/dist/public-runtime.js +5 -0
  5. package/dist/public-runtime.js.map +1 -1
  6. package/dist/runtime/query/checkpoint.d.ts +27 -1
  7. package/dist/runtime/query/checkpoint.d.ts.map +1 -1
  8. package/dist/runtime/query/checkpoint.js +34 -4
  9. package/dist/runtime/query/checkpoint.js.map +1 -1
  10. package/dist/runtime/query/index.d.ts +21 -1
  11. package/dist/runtime/query/index.d.ts.map +1 -1
  12. package/dist/runtime/query/index.js +4 -0
  13. package/dist/runtime/query/index.js.map +1 -1
  14. package/dist/runtime/query/resume-run.d.ts +9 -1
  15. package/dist/runtime/query/resume-run.d.ts.map +1 -1
  16. package/dist/runtime/query/resume-run.js +2 -1
  17. package/dist/runtime/query/resume-run.js.map +1 -1
  18. package/dist/store/index.d.ts +1 -1
  19. package/dist/store/index.d.ts.map +1 -1
  20. package/dist/store/index.js +1 -1
  21. package/dist/store/index.js.map +1 -1
  22. package/dist/store/run/checkpoint-disk.d.ts +18 -2
  23. package/dist/store/run/checkpoint-disk.d.ts.map +1 -1
  24. package/dist/store/run/checkpoint-disk.js +50 -5
  25. package/dist/store/run/checkpoint-disk.js.map +1 -1
  26. package/dist/store/run/checkpoint-memory.d.ts +22 -2
  27. package/dist/store/run/checkpoint-memory.d.ts.map +1 -1
  28. package/dist/store/run/checkpoint-memory.js +99 -4
  29. package/dist/store/run/checkpoint-memory.js.map +1 -1
  30. package/dist/store/run/claim-disk.d.ts +130 -0
  31. package/dist/store/run/claim-disk.d.ts.map +1 -0
  32. package/dist/store/run/claim-disk.js +550 -0
  33. package/dist/store/run/claim-disk.js.map +1 -0
  34. package/dist/store/run/listing.d.ts +44 -1
  35. package/dist/store/run/listing.d.ts.map +1 -1
  36. package/dist/store/run/listing.js +92 -1
  37. package/dist/store/run/listing.js.map +1 -1
  38. package/dist/types/run/checkpoint-store.d.ts +178 -2
  39. package/dist/types/run/checkpoint-store.d.ts.map +1 -1
  40. package/package.json +1 -1
  41. package/src/public-runtime.ts +5 -0
  42. package/src/runtime/query/checkpoint.ts +42 -5
  43. package/src/runtime/query/index.ts +26 -1
  44. package/src/runtime/query/resume-run.ts +12 -2
  45. package/src/store/index.ts +4 -0
  46. package/src/store/run/checkpoint-disk.ts +70 -5
  47. package/src/store/run/checkpoint-memory.ts +118 -3
  48. package/src/store/run/claim-disk.ts +593 -0
  49. package/src/store/run/listing.ts +116 -1
  50. package/src/types/run/checkpoint-store.ts +189 -2
@@ -7,14 +7,24 @@ import type {
7
7
  CheckpointListingScope,
8
8
  CheckpointRunScope,
9
9
  CheckpointStore,
10
+ ClaimFence,
11
+ ClaimRunOptions,
10
12
  DurableRunEntry,
11
13
  DurableRunPage,
12
14
  ListDurableRunsOptions,
15
+ RunClaim,
13
16
  } from '../../types/run/checkpoint-store.js'
14
17
  import type { RunStoreConfig } from '../../types/run/index.js'
15
18
  import type { ProjectId } from '../../types/session/ids.js'
19
+ import { acquireClaim, currentFence, readClaim, releaseClaim } from './claim-disk.js'
16
20
  import { RunDiskStore, readCheckpointsIn } from './disk.js'
17
- import { assertContiguousListingScope, paginateDurableRuns, toDurableRunEntry } from './listing.js'
21
+ import {
22
+ assertContiguousListingScope,
23
+ fencedOut,
24
+ paginateDurableRuns,
25
+ toClaimSummary,
26
+ toDurableRunEntry,
27
+ } from './listing.js'
18
28
 
19
29
  /**
20
30
  * The attribution a disk store's own layout does not record.
@@ -84,11 +94,47 @@ export class DiskCheckpointStore implements CheckpointStore {
84
94
  return promise
85
95
  }
86
96
 
87
- async writeCheckpoint(scope: CheckpointRunScope, checkpoint: IterationCheckpoint): Promise<void> {
97
+ async writeCheckpoint(
98
+ scope: CheckpointRunScope,
99
+ checkpoint: IterationCheckpoint,
100
+ fence?: ClaimFence,
101
+ ): Promise<void> {
88
102
  const store = await this.bind(scope)
103
+ if (fence !== undefined) {
104
+ // Read at the moment of the write, not at the start of the run.
105
+ // A holder that stalled past its lease believes it still holds,
106
+ // and this is the only point at which it can be told otherwise.
107
+ // Names only, no parsing. The fence is the file name, so a corrupt
108
+ // body cannot make this check skip itself — which the previous shape
109
+ // did, at the one site whose entire job is refusing.
110
+ const current = await currentFence(this.runDir(scope))
111
+ if (fence < current) throw fencedOut(scope, fence, current)
112
+ }
89
113
  await store.writeCheckpoint(checkpoint)
90
114
  }
91
115
 
116
+ async claimRun(scope: CheckpointRunScope, options: ClaimRunOptions): Promise<RunClaim | null> {
117
+ return acquireClaim(this.runDir(scope), options)
118
+ }
119
+
120
+ async releaseRun(scope: CheckpointRunScope, fence: ClaimFence): Promise<void> {
121
+ await releaseClaim(this.runDir(scope), fence)
122
+ }
123
+
124
+ /**
125
+ * The run's directory, resolved the same way `RunDiskStore.initRun` does.
126
+ *
127
+ * Duplicated rather than shared because the claim path must be derivable
128
+ * WITHOUT binding a store — binding creates the directory, and a claim
129
+ * read is a read. Kept beside the layout comment on `listDurableRuns` so
130
+ * the two stay together if the layout ever moves.
131
+ */
132
+ private runDir(scope: CheckpointRunScope): string {
133
+ return scope.parentRunId
134
+ ? join(this.config.baseDir, scope.parentRunId, 'children', scope.runId)
135
+ : join(this.config.baseDir, scope.runId)
136
+ }
137
+
92
138
  async readCheckpoint(
93
139
  scope: CheckpointRunScope,
94
140
  checkpointId: CheckpointId,
@@ -163,21 +209,40 @@ export class DiskCheckpointStore implements CheckpointStore {
163
209
  const runDir = join(this.config.baseDir, runId)
164
210
 
165
211
  const own = toDurableRunEntry({ ...attribution, runId }, await readCheckpointsIn(runDir), now)
166
- if (own) entries.push(own)
212
+ if (own) entries.push(await this.withClaim(own, runDir, now))
167
213
 
168
214
  for (const childId of await this.readRunDirs(join(runDir, 'children'))) {
215
+ const childDir = join(runDir, 'children', childId)
169
216
  const child = toDurableRunEntry(
170
217
  { ...attribution, runId: childId, parentRunId: runId },
171
- await readCheckpointsIn(join(runDir, 'children', childId)),
218
+ await readCheckpointsIn(childDir),
172
219
  now,
173
220
  )
174
- if (child) entries.push(child)
221
+ if (child) entries.push(await this.withClaim(child, childDir, now))
175
222
  }
176
223
  }
177
224
 
178
225
  return paginateDurableRuns(entries, options)
179
226
  }
180
227
 
228
+ /**
229
+ * Attach the run's claim to its listing row, judged against the page's
230
+ * own clock so one page cannot disagree with itself about availability.
231
+ */
232
+ private async withClaim(
233
+ entry: DurableRunEntry,
234
+ runDir: string,
235
+ now: number,
236
+ ): Promise<DurableRunEntry> {
237
+ const claim = await readClaim(runDir)
238
+ // A holding whose body could not be read still appears on the row,
239
+ // carrying an expiry of 0 so it reads as available. Dropping the field
240
+ // entirely — which is what happened before — put the run under
241
+ // `claimed: false` by looking unclaimed rather than by being
242
+ // reclaimable, so a queue reader was told a wedged run was free work.
243
+ return claim ? { ...entry, claim: toClaimSummary(claim, now) } : entry
244
+ }
245
+
181
246
  /** Directory names under `dir`, or none when `dir` does not exist. */
182
247
  private async readRunDirs(dir: string): Promise<RunId[]> {
183
248
  try {
@@ -3,11 +3,20 @@ import type {
3
3
  CheckpointListingScope,
4
4
  CheckpointRunScope,
5
5
  CheckpointStore,
6
+ ClaimFence,
7
+ ClaimRunOptions,
6
8
  DurableRunEntry,
7
9
  DurableRunPage,
8
10
  ListDurableRunsOptions,
11
+ RunClaim,
9
12
  } from '../../types/run/checkpoint-store.js'
10
- import { assertContiguousListingScope, paginateDurableRuns, toDurableRunEntry } from './listing.js'
13
+ import {
14
+ assertContiguousListingScope,
15
+ fencedOut,
16
+ paginateDurableRuns,
17
+ toClaimSummary,
18
+ toDurableRunEntry,
19
+ } from './listing.js'
11
20
 
12
21
  /**
13
22
  * Process-local {@link CheckpointStore}, keyed by the full five-layer scope.
@@ -35,7 +44,111 @@ export class InMemoryCheckpointStore implements CheckpointStore {
35
44
  return [scope.tenantId, scope.projectId, scope.sessionId, scope.runId].join('/')
36
45
  }
37
46
 
38
- async writeCheckpoint(scope: CheckpointRunScope, checkpoint: IterationCheckpoint): Promise<void> {
47
+ /** `tenant/project/session/run` → the run's current holding, if any. */
48
+ private readonly claims = new Map<string, RunClaim>()
49
+
50
+ /**
51
+ * The highest fence ever issued per run, kept separately from the claim.
52
+ *
53
+ * The claim is removed on release; this is not. That separation is the
54
+ * whole point: the first version deleted the claim and then computed the
55
+ * next fence from it, so releasing rewound the counter to 1 and a worker
56
+ * stalled at fence 1 could write beside a new holder also at fence 1 —
57
+ * and the documented `finally { releaseRun() }` did it on every pass.
58
+ *
59
+ * The disk store gets this property from file names that persist. In
60
+ * memory the equivalent is a high-water mark nothing clears, and the two
61
+ * must agree, because this class is what a host reads when writing a
62
+ * backend of its own.
63
+ */
64
+ private readonly highWater = new Map<string, ClaimFence>()
65
+
66
+ async claimRun(scope: CheckpointRunScope, options: ClaimRunOptions): Promise<RunClaim | null> {
67
+ const key = this.key(scope)
68
+ const now = options.now ?? Date.now()
69
+ const held = this.claims.get(key)
70
+
71
+ // Held by somebody else and still live. Not an error: two readers on
72
+ // one queue is the ordinary case.
73
+ if (held && now < held.expiresAt && held.holder !== options.holder) return null
74
+
75
+ // A reclaim of an expired holding and a renewal by the current holder
76
+ // are the same write. The fence advances either way, so a previous
77
+ // holder that wakes up is fenced out in both cases — a renewal that
78
+ // kept the fence would leave a stalled twin able to write.
79
+ // Counted from the high-water mark, never from the live claim. A
80
+ // released run has no claim, and computing from that absence is what
81
+ // rewound the counter to 1 on every release.
82
+ const fence = (this.highWater.get(key) ?? 0) + 1
83
+ const claim: RunClaim = { holder: options.holder, fence, expiresAt: now + options.ttlMs }
84
+ this.highWater.set(key, fence)
85
+ this.claims.set(key, claim)
86
+ return claim
87
+ }
88
+
89
+ async releaseRun(scope: CheckpointRunScope, fence: ClaimFence): Promise<void> {
90
+ const key = this.key(scope)
91
+ const held = this.claims.get(key)
92
+ // A stale fence releases nothing. A worker that stalled past its lease
93
+ // must not be able to hand away a run somebody else now holds.
94
+ //
95
+ // The high-water mark deliberately survives this. Dropping the claim
96
+ // returns the run to the queue; forgetting the number it reached would
97
+ // re-issue a fence a stalled worker still believes it holds.
98
+ if (!held || held.fence !== fence) return
99
+ this.claims.delete(key)
100
+ // And the counter steps past the released holding, because the disk
101
+ // store's release does: it appends a TOMBSTONE at `fence + 1`, which
102
+ // raises the maximum name and therefore the fence every later write is
103
+ // checked against. Without this step a holder that released could still
104
+ // write with the fence it just gave up — refused on disk, accepted here.
105
+ //
106
+ // The consequence is visible in the numbering: a released run's next
107
+ // claim is `fence + 2` in both stores, because the tombstone consumed
108
+ // one. That is parity, not an off-by-one.
109
+ this.highWater.set(key, fence + 1)
110
+ }
111
+
112
+ async writeCheckpoint(
113
+ scope: CheckpointRunScope,
114
+ checkpoint: IterationCheckpoint,
115
+ fence?: ClaimFence,
116
+ ): Promise<void> {
117
+ const key = this.key(scope)
118
+ // An unfenced write is allowed even on a claimed run: a host adopting
119
+ // claims on one worker must not break the workers that have not
120
+ // adopted them. A fenced write is checked, and that check is what
121
+ // makes the lease real.
122
+ if (fence !== undefined) {
123
+ // Against the HIGH-WATER MARK, not the live claim, and the difference
124
+ // is a silent loss rather than a duplicate.
125
+ //
126
+ // `releaseRun` deletes the claim. Reading `claims` here meant that
127
+ // between a release and the next take there was no holding to
128
+ // compare against, so `held` was `undefined` and every fence was
129
+ // accepted however stale. w1 stalls at fence 1; w2 reclaims at 2,
130
+ // finishes the work, releases cleanly; w1 wakes and writes with
131
+ // fence 1 — accepted — and its checkpoint carries a fresh
132
+ // `createdAt`, so it sorts newest and the next resume restores w1's
133
+ // stale history. w2's completed work is gone, with no error
134
+ // anywhere.
135
+ //
136
+ // The disk store refuses that write, because `currentFence` reads
137
+ // file names and the release tombstone raised the maximum. The
138
+ // minting side here already counted from the high-water mark; only
139
+ // the enforcement side was left reading the claim, so the two
140
+ // shipped stores disagreed at the one point that decides whether a
141
+ // lease is real.
142
+ const current = this.highWater.get(key) ?? 0
143
+ if (fence < current) throw fencedOut(scope, fence, current)
144
+ }
145
+ return this.writeUnchecked(scope, checkpoint)
146
+ }
147
+
148
+ private async writeUnchecked(
149
+ scope: CheckpointRunScope,
150
+ checkpoint: IterationCheckpoint,
151
+ ): Promise<void> {
39
152
  const key = this.key(scope)
40
153
  let run = this.runs.get(key)
41
154
  if (!run) {
@@ -93,7 +206,9 @@ export class InMemoryCheckpointStore implements CheckpointStore {
93
206
  if (scope.sessionId !== undefined && runScope.sessionId !== scope.sessionId) continue
94
207
 
95
208
  const entry = toDurableRunEntry(runScope, [...checkpoints.values()], now)
96
- if (entry) entries.push(entry)
209
+ if (!entry) continue
210
+ const claim = this.claims.get(key)
211
+ entries.push(claim ? { ...entry, claim: toClaimSummary(claim, now) } : entry)
97
212
  }
98
213
 
99
214
  return paginateDurableRuns(entries, options)