@aztec/prover-client 0.0.1-commit.993d240 → 0.0.1-commit.9a89641
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.
- package/dest/light/lightweight_checkpoint_builder.d.ts +1 -1
- package/dest/light/lightweight_checkpoint_builder.d.ts.map +1 -1
- package/dest/light/lightweight_checkpoint_builder.js +3 -1
- package/dest/mocks/fixtures.d.ts +1 -1
- package/dest/mocks/fixtures.d.ts.map +1 -1
- package/dest/mocks/fixtures.js +3 -3
- package/dest/mocks/test_context.d.ts +9 -11
- package/dest/mocks/test_context.d.ts.map +1 -1
- package/dest/mocks/test_context.js +15 -23
- package/dest/orchestrator/block-building-helpers.d.ts +1 -1
- package/dest/orchestrator/checkpoint-proving-state.d.ts +13 -36
- package/dest/orchestrator/checkpoint-proving-state.d.ts.map +1 -1
- package/dest/orchestrator/checkpoint-proving-state.js +18 -125
- package/dest/orchestrator/checkpoint-sub-tree-orchestrator.d.ts +116 -62
- package/dest/orchestrator/checkpoint-sub-tree-orchestrator.d.ts.map +1 -1
- package/dest/orchestrator/checkpoint-sub-tree-orchestrator.js +1005 -90
- package/dest/orchestrator/chonk-cache.d.ts +39 -0
- package/dest/orchestrator/chonk-cache.d.ts.map +1 -0
- package/dest/orchestrator/chonk-cache.js +80 -0
- package/dest/orchestrator/index.d.ts +2 -3
- package/dest/orchestrator/index.d.ts.map +1 -1
- package/dest/orchestrator/index.js +1 -2
- package/dest/orchestrator/proving-scheduler.d.ts +26 -17
- package/dest/orchestrator/proving-scheduler.d.ts.map +1 -1
- package/dest/orchestrator/proving-scheduler.js +28 -25
- package/dest/orchestrator/top-tree-orchestrator.d.ts +11 -6
- package/dest/orchestrator/top-tree-orchestrator.d.ts.map +1 -1
- package/dest/orchestrator/top-tree-orchestrator.js +71 -21
- package/dest/orchestrator/top-tree-proving-state.d.ts +1 -1
- package/dest/orchestrator/top-tree-proving-state.d.ts.map +1 -1
- package/dest/orchestrator/tx-proving-state.d.ts +3 -3
- package/dest/orchestrator/tx-proving-state.d.ts.map +1 -1
- package/dest/prover-client/prover-client.d.ts +17 -19
- package/dest/prover-client/prover-client.d.ts.map +1 -1
- package/dest/prover-client/prover-client.js +24 -25
- package/dest/proving_broker/broker_prover_facade.d.ts +3 -3
- package/dest/proving_broker/broker_prover_facade.d.ts.map +1 -1
- package/dest/proving_broker/broker_prover_facade.js +13 -1
- package/dest/proving_broker/proving_broker.d.ts +2 -1
- package/dest/proving_broker/proving_broker.d.ts.map +1 -1
- package/dest/proving_broker/proving_broker.js +195 -77
- package/dest/proving_broker/proving_broker_database/memory.d.ts +5 -2
- package/dest/proving_broker/proving_broker_database/memory.d.ts.map +1 -1
- package/dest/proving_broker/proving_broker_database/memory.js +14 -1
- package/dest/proving_broker/proving_broker_database/persisted.d.ts +15 -2
- package/dest/proving_broker/proving_broker_database/persisted.d.ts.map +1 -1
- package/dest/proving_broker/proving_broker_database/persisted.js +55 -4
- package/dest/proving_broker/proving_broker_database.d.ts +27 -1
- package/dest/proving_broker/proving_broker_database.d.ts.map +1 -1
- package/dest/proving_broker/rpc.d.ts +13 -4
- package/dest/proving_broker/rpc.d.ts.map +1 -1
- package/dest/proving_broker/rpc.js +14 -3
- package/dest/test/epoch_settlement.d.ts +36 -0
- package/dest/test/epoch_settlement.d.ts.map +1 -0
- package/dest/test/epoch_settlement.js +52 -0
- package/dest/test/index.d.ts +2 -0
- package/dest/test/index.d.ts.map +1 -0
- package/dest/test/index.js +1 -0
- package/dest/test/mock_prover.d.ts +1 -1
- package/dest/test/mock_prover.js +2 -2
- package/package.json +17 -16
- package/src/light/lightweight_checkpoint_builder.ts +2 -0
- package/src/mocks/fixtures.ts +2 -4
- package/src/mocks/test_context.ts +16 -35
- package/src/orchestrator/checkpoint-proving-state.ts +14 -168
- package/src/orchestrator/checkpoint-sub-tree-orchestrator.ts +902 -129
- package/src/orchestrator/chonk-cache.ts +99 -0
- package/src/orchestrator/index.ts +1 -2
- package/src/orchestrator/proving-scheduler.ts +29 -25
- package/src/orchestrator/top-tree-orchestrator.ts +114 -44
- package/src/orchestrator/top-tree-proving-state.ts +0 -1
- package/src/orchestrator/tx-proving-state.ts +3 -3
- package/src/prover-client/prover-client.ts +35 -49
- package/src/proving_broker/broker_prover_facade.ts +12 -3
- package/src/proving_broker/proving_broker.ts +186 -77
- package/src/proving_broker/proving_broker_database/memory.ts +16 -2
- package/src/proving_broker/proving_broker_database/persisted.ts +69 -6
- package/src/proving_broker/proving_broker_database.ts +30 -0
- package/src/proving_broker/rpc.ts +16 -0
- package/src/test/epoch_settlement.ts +82 -0
- package/src/test/index.ts +1 -0
- package/src/test/mock_prover.ts +2 -2
- package/dest/orchestrator/epoch-proving-context.d.ts +0 -51
- package/dest/orchestrator/epoch-proving-context.d.ts.map +0 -1
- package/dest/orchestrator/epoch-proving-context.js +0 -81
- package/dest/orchestrator/epoch-proving-state.d.ts +0 -75
- package/dest/orchestrator/epoch-proving-state.d.ts.map +0 -1
- package/dest/orchestrator/epoch-proving-state.js +0 -269
- package/dest/orchestrator/orchestrator.d.ts +0 -118
- package/dest/orchestrator/orchestrator.d.ts.map +0 -1
- package/dest/orchestrator/orchestrator.js +0 -1113
- package/dest/orchestrator/top-tree-proving-scheduler.d.ts +0 -62
- package/dest/orchestrator/top-tree-proving-scheduler.d.ts.map +0 -1
- package/dest/orchestrator/top-tree-proving-scheduler.js +0 -73
- package/dest/prover-client/server-epoch-prover.d.ts +0 -32
- package/dest/prover-client/server-epoch-prover.d.ts.map +0 -1
- package/dest/prover-client/server-epoch-prover.js +0 -40
- package/src/orchestrator/epoch-proving-context.ts +0 -101
- package/src/orchestrator/epoch-proving-state.ts +0 -380
- package/src/orchestrator/orchestrator.ts +0 -1111
- package/src/orchestrator/top-tree-proving-scheduler.ts +0 -154
- package/src/prover-client/server-epoch-prover.ts +0 -69
|
@@ -33,6 +33,23 @@ type InProgressMetadata = {
|
|
|
33
33
|
|
|
34
34
|
type EnqueuedProvingJob = Pick<ProvingJob, 'id' | 'epochNumber'>;
|
|
35
35
|
|
|
36
|
+
/** In-memory scheduling metadata for a job. The large `inputsUri` is deliberately excluded — it lives
|
|
37
|
+
* in the database and is read on demand when dispatching to an agent. */
|
|
38
|
+
type ProvingJobMetadata = Pick<ProvingJob, 'id' | 'type' | 'epochNumber'>;
|
|
39
|
+
|
|
40
|
+
/** Settled state kept in memory: the status plus the small payloads (rejected reason, aborted). The
|
|
41
|
+
* large fulfilled proof `value` is NOT kept here — it lives in the database (read on demand) and,
|
|
42
|
+
* transiently, in `pendingResults` while its database write is in flight. */
|
|
43
|
+
type SettledRecord = { status: 'fulfilled' } | { status: 'rejected'; reason: string } | { status: 'aborted' };
|
|
44
|
+
|
|
45
|
+
function toJobMetadata(job: ProvingJob): ProvingJobMetadata {
|
|
46
|
+
return { id: job.id, type: job.type, epochNumber: job.epochNumber };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function toSettledRecord(result: ProvingJobSettledResult): SettledRecord {
|
|
50
|
+
return result.status === 'fulfilled' ? { status: 'fulfilled' } : result;
|
|
51
|
+
}
|
|
52
|
+
|
|
36
53
|
/**
|
|
37
54
|
* A broker that manages proof requests and distributes them to workers based on their priority.
|
|
38
55
|
* It takes a backend that is responsible for storing and retrieving proof requests and results.
|
|
@@ -69,11 +86,17 @@ export class ProvingBroker implements ProvingJobProducer, ProvingJobConsumer, Pr
|
|
|
69
86
|
[ProvingRequestType.PARITY_ROOT]: new PriorityMemoryQueue<EnqueuedProvingJob>(provingJobComparator),
|
|
70
87
|
};
|
|
71
88
|
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
private jobsCache = new Map<ProvingJobId,
|
|
75
|
-
//
|
|
76
|
-
|
|
89
|
+
// Scheduling metadata for every known job (id, type, epochNumber). The large `inputsUri` is NOT held
|
|
90
|
+
// here — it lives in the database and is read on demand when a job is dispatched to an agent.
|
|
91
|
+
private jobsCache = new Map<ProvingJobId, ProvingJobMetadata>();
|
|
92
|
+
// Settled status for every settled job. The large fulfilled proof `value` is NOT held here — it lives
|
|
93
|
+
// in the database (read on demand in getProvingJobStatus) and, transiently, in `pendingResults` while
|
|
94
|
+
// its database write is in flight. Small payloads (rejected reason, aborted) are kept inline.
|
|
95
|
+
private resultsCache = new Map<ProvingJobId, SettledRecord>();
|
|
96
|
+
// Transient hold of a fulfilled result's full value, from the moment it settles until its database
|
|
97
|
+
// write commits, so status reads are read-your-writes without retaining every proof. Evicted once the
|
|
98
|
+
// value is durable in the database; kept only if that write failed (preserving the result in memory).
|
|
99
|
+
private pendingResults = new Map<ProvingJobId, ProvingJobSettledResult>();
|
|
77
100
|
|
|
78
101
|
// tracks when each job was enqueued
|
|
79
102
|
private enqueuedAt = new Map<ProvingJobId, Timer>();
|
|
@@ -87,8 +110,9 @@ export class ProvingBroker implements ProvingJobProducer, ProvingJobConsumer, Pr
|
|
|
87
110
|
// keep track of which proving job has been retried
|
|
88
111
|
private retries = new Map<ProvingJobId, number>();
|
|
89
112
|
|
|
90
|
-
// a map of promises that
|
|
91
|
-
|
|
113
|
+
// a map of promises that are resolved when a job settles. Carries no payload (settled status/results
|
|
114
|
+
// are read from resultsCache/pendingResults/DB) so it never pins a proof value in memory.
|
|
115
|
+
private promises = new Map<ProvingJobId, PromiseWithResolvers<void>>();
|
|
92
116
|
|
|
93
117
|
private cleanupPromise: RunningPromise;
|
|
94
118
|
private msTimeSource = () => Date.now();
|
|
@@ -175,12 +199,12 @@ export class ProvingBroker implements ProvingJobProducer, ProvingJobConsumer, Pr
|
|
|
175
199
|
status: result ? result.status : 'pending',
|
|
176
200
|
});
|
|
177
201
|
|
|
178
|
-
this.jobsCache.set(item.id, item);
|
|
202
|
+
this.jobsCache.set(item.id, toJobMetadata(item));
|
|
179
203
|
this.promises.set(item.id, promiseWithResolvers());
|
|
180
204
|
|
|
181
205
|
if (result) {
|
|
182
|
-
this.promises.get(item.id)!.resolve(
|
|
183
|
-
this.resultsCache.set(item.id, result);
|
|
206
|
+
this.promises.get(item.id)!.resolve();
|
|
207
|
+
this.resultsCache.set(item.id, toSettledRecord(result));
|
|
184
208
|
} else {
|
|
185
209
|
this.enqueueJobInternal(item);
|
|
186
210
|
}
|
|
@@ -211,7 +235,7 @@ export class ProvingBroker implements ProvingJobProducer, ProvingJobConsumer, Pr
|
|
|
211
235
|
}
|
|
212
236
|
|
|
213
237
|
public getProvingJobStatus(id: ProvingJobId): Promise<ProvingJobStatus> {
|
|
214
|
-
return
|
|
238
|
+
return this.#getProvingJobStatus(id);
|
|
215
239
|
}
|
|
216
240
|
|
|
217
241
|
public getCompletedJobs(ids: ProvingJobId[]): Promise<ProvingJobId[]> {
|
|
@@ -219,7 +243,7 @@ export class ProvingBroker implements ProvingJobProducer, ProvingJobConsumer, Pr
|
|
|
219
243
|
}
|
|
220
244
|
|
|
221
245
|
public getProvingJob(filter?: ProvingJobFilter): Promise<GetProvingJobResponse | undefined> {
|
|
222
|
-
return
|
|
246
|
+
return this.#getProvingJob(filter);
|
|
223
247
|
}
|
|
224
248
|
|
|
225
249
|
public reportProvingJobSuccess(
|
|
@@ -244,7 +268,7 @@ export class ProvingBroker implements ProvingJobProducer, ProvingJobConsumer, Pr
|
|
|
244
268
|
startedAt: number,
|
|
245
269
|
filter?: ProvingJobFilter,
|
|
246
270
|
): Promise<{ job: ProvingJob; time: number } | undefined> {
|
|
247
|
-
return
|
|
271
|
+
return this.#reportProvingJobProgress(id, startedAt, filter);
|
|
248
272
|
}
|
|
249
273
|
|
|
250
274
|
public async replayProvingJob(
|
|
@@ -263,7 +287,7 @@ export class ProvingBroker implements ProvingJobProducer, ProvingJobConsumer, Pr
|
|
|
263
287
|
this.cleanUpProvingJobState([jobId]);
|
|
264
288
|
|
|
265
289
|
const job: ProvingJob = { id: jobId, type, epochNumber, inputsUri };
|
|
266
|
-
this.jobsCache.set(jobId, job);
|
|
290
|
+
this.jobsCache.set(jobId, toJobMetadata(job));
|
|
267
291
|
await this.database.addProvingJob(job);
|
|
268
292
|
this.enqueueJobInternal(job);
|
|
269
293
|
|
|
@@ -271,16 +295,52 @@ export class ProvingBroker implements ProvingJobProducer, ProvingJobConsumer, Pr
|
|
|
271
295
|
}
|
|
272
296
|
|
|
273
297
|
async #enqueueProvingJob(job: ProvingJob): Promise<ProvingJobStatus> {
|
|
274
|
-
//
|
|
275
|
-
|
|
298
|
+
// The status returned to the caller reflects the job's state at the start of this call: `not-found`
|
|
299
|
+
// for a brand-new job (it did not exist yet), or the cached status for one already known. Crucially
|
|
300
|
+
// this must NOT `await` before the synchronous `jobsCache`/`resultsCache` gate below, or the
|
|
301
|
+
// enqueue/revive lock would no longer hold. So the not-found default is set synchronously here and
|
|
302
|
+
// the cached branch computes the real status (which may read the DB) only when it returns.
|
|
303
|
+
let jobStatus: ProvingJobStatus = { status: 'not-found' };
|
|
276
304
|
if (this.jobsCache.has(job.id)) {
|
|
277
305
|
const existing = this.jobsCache.get(job.id);
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
this.
|
|
283
|
-
|
|
306
|
+
// Identity check is metadata-only: `inputsUri` now lives in the database, not memory, and job ids
|
|
307
|
+
// are content-addressed (same id ⇒ same inputs), so a mismatch shows up in id/type/epochNumber.
|
|
308
|
+
assert.deepStrictEqual(toJobMetadata(job), existing, 'Duplicate proving job ID');
|
|
309
|
+
|
|
310
|
+
if (this.resultsCache.get(job.id)?.status === 'aborted') {
|
|
311
|
+
// The producer is re-requesting a job it previously cancelled: revive it rather than
|
|
312
|
+
// returning the cached abort, clearing the aborted state in memory and in the database so the
|
|
313
|
+
// revival survives a restart.
|
|
314
|
+
//
|
|
315
|
+
// Concurrency model: `jobsCache` is the enqueue lock. Every path that puts a job on the queue
|
|
316
|
+
// populates `jobsCache` *synchronously, before its first await* (see the "New proving job"
|
|
317
|
+
// block below), so a second concurrent enqueue of the same id observes the entry at the top
|
|
318
|
+
// of this method and takes a cached, no-op branch instead of enqueuing a duplicate. The revive
|
|
319
|
+
// must keep holding that lock: we tear down the settled state and re-set `jobsCache` in a
|
|
320
|
+
// single synchronous span (no await in between), and only then await the database. Because a
|
|
321
|
+
// concurrent re-request can only interleave at that await — by which point `jobsCache` is
|
|
322
|
+
// populated again and the aborted result is gone — it falls into the cached branch and no-ops,
|
|
323
|
+
// so the job is enqueued exactly once. (`cleanUpProvingJobState` also drops the promise, so
|
|
324
|
+
// `enqueueJobInternal` below mints a fresh one for the retry.) This holds unchanged with the
|
|
325
|
+
// slimmer caches: `jobsCache` (now metadata) and `resultsCache` (now settled status) are still
|
|
326
|
+
// synchronous in-memory maps, so the tear-down + re-set span still contains no await.
|
|
327
|
+
this.logger.info(`Reviving aborted proving job id=${job.id} epochNumber=${job.epochNumber}`, {
|
|
328
|
+
provingJobId: job.id,
|
|
329
|
+
});
|
|
330
|
+
this.cleanUpProvingJobState([job.id]);
|
|
331
|
+
this.jobsCache.set(job.id, toJobMetadata(job));
|
|
332
|
+
await this.database.deleteProvingJobResult(job.id);
|
|
333
|
+
// The job is re-set in the cache and about to be re-enqueued below: its start status is in-queue.
|
|
334
|
+
jobStatus = { status: 'in-queue' };
|
|
335
|
+
} else {
|
|
336
|
+
this.logger.warn(`Cached proving job id=${job.id} epochNumber=${job.epochNumber}. Not enqueuing again`, {
|
|
337
|
+
provingJobId: job.id,
|
|
338
|
+
});
|
|
339
|
+
this.instrumentation.incCachedJobs(job.type);
|
|
340
|
+
// Return the job's current status. This reads the fulfilled proof value from the DB when needed;
|
|
341
|
+
// the await is fine here because this branch does not enqueue, so it is outside the lock span.
|
|
342
|
+
return await this.#getProvingJobStatus(job.id);
|
|
343
|
+
}
|
|
284
344
|
}
|
|
285
345
|
|
|
286
346
|
if (this.isJobStale(job)) {
|
|
@@ -293,7 +353,7 @@ export class ProvingBroker implements ProvingJobProducer, ProvingJobConsumer, Pr
|
|
|
293
353
|
this.logger.info(`New proving job id=${job.id} epochNumber=${job.epochNumber}`, { provingJobId: job.id });
|
|
294
354
|
try {
|
|
295
355
|
// do this first so it acts as a "lock". If this job is enqueued again while we're saving it the if at the top will catch it.
|
|
296
|
-
this.jobsCache.set(job.id, job);
|
|
356
|
+
this.jobsCache.set(job.id, toJobMetadata(job));
|
|
297
357
|
await this.database.addProvingJob(job);
|
|
298
358
|
this.enqueueJobInternal(job);
|
|
299
359
|
this.instrumentation.incTotalJobs(job.type);
|
|
@@ -306,15 +366,34 @@ export class ProvingBroker implements ProvingJobProducer, ProvingJobConsumer, Pr
|
|
|
306
366
|
}
|
|
307
367
|
|
|
308
368
|
async #cancelProvingJob(id: ProvingJobId): Promise<void> {
|
|
309
|
-
|
|
369
|
+
const job = this.jobsCache.get(id);
|
|
370
|
+
if (!job) {
|
|
310
371
|
this.logger.warn(`Can't cancel a job that doesn't exist id=${id}`, { provingJobId: id });
|
|
311
372
|
return;
|
|
312
373
|
}
|
|
313
374
|
|
|
314
|
-
//
|
|
315
|
-
if (
|
|
316
|
-
|
|
317
|
-
|
|
375
|
+
// Leave jobs that have already settled (completed or failed) alone: those results are terminal.
|
|
376
|
+
if (this.resultsCache.has(id)) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
this.logger.info(`Cancelling job id=${id}`, { provingJobId: id });
|
|
381
|
+
this.inProgress.delete(id);
|
|
382
|
+
|
|
383
|
+
// Record the cancellation as its own settled state and persist it, so it survives a restart and
|
|
384
|
+
// notifies the current waiter. Unlike a completion or failure this is not terminal: re-enqueuing
|
|
385
|
+
// the same job id revives it (see #enqueueProvingJob), so the abort never permanently blocks the
|
|
386
|
+
// proof.
|
|
387
|
+
this.resultsCache.set(id, { status: 'aborted' });
|
|
388
|
+
this.promises.get(id)?.resolve();
|
|
389
|
+
this.completedJobNotifications.push(id);
|
|
390
|
+
this.instrumentation.incAbortedJobs(job.type);
|
|
391
|
+
|
|
392
|
+
try {
|
|
393
|
+
await this.database.setProvingJobAborted(id);
|
|
394
|
+
} catch (saveErr) {
|
|
395
|
+
this.logger.error(`Failed to save proving job aborted status id=${id}`, saveErr, { provingJobId: id });
|
|
396
|
+
throw saveErr;
|
|
318
397
|
}
|
|
319
398
|
}
|
|
320
399
|
|
|
@@ -324,10 +403,11 @@ export class ProvingBroker implements ProvingJobProducer, ProvingJobConsumer, Pr
|
|
|
324
403
|
this.jobsCache.delete(id);
|
|
325
404
|
const deferred = this.promises.get(id);
|
|
326
405
|
if (deferred) {
|
|
327
|
-
deferred.resolve(
|
|
406
|
+
deferred.resolve();
|
|
328
407
|
}
|
|
329
408
|
this.promises.delete(id);
|
|
330
409
|
this.resultsCache.delete(id);
|
|
410
|
+
this.pendingResults.delete(id);
|
|
331
411
|
this.inProgress.delete(id);
|
|
332
412
|
this.retries.delete(id);
|
|
333
413
|
this.enqueuedAt.delete(id);
|
|
@@ -335,20 +415,31 @@ export class ProvingBroker implements ProvingJobProducer, ProvingJobConsumer, Pr
|
|
|
335
415
|
this.completedJobNotifications = this.completedJobNotifications.filter(id => !idsToClean.has(id));
|
|
336
416
|
}
|
|
337
417
|
|
|
338
|
-
#getProvingJobStatus(id: ProvingJobId): ProvingJobStatus {
|
|
339
|
-
const
|
|
340
|
-
if (
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
const item = this.jobsCache.get(id);
|
|
345
|
-
|
|
346
|
-
if (!item) {
|
|
347
|
-
return { status: 'not-found' };
|
|
418
|
+
async #getProvingJobStatus(id: ProvingJobId): Promise<ProvingJobStatus> {
|
|
419
|
+
const settled = this.resultsCache.get(id);
|
|
420
|
+
if (settled) {
|
|
421
|
+
if (settled.status !== 'fulfilled') {
|
|
422
|
+
// rejected/aborted carry their (small) payload inline.
|
|
423
|
+
return settled;
|
|
348
424
|
}
|
|
425
|
+
// A fulfilled job's proof value is not held in memory: serve it from the transient hold if its
|
|
426
|
+
// database write is still in flight, otherwise read it back from the database.
|
|
427
|
+
const full = this.pendingResults.get(id) ?? (await this.database.getProvingJobResult(id));
|
|
428
|
+
if (full?.status === 'fulfilled') {
|
|
429
|
+
return full;
|
|
430
|
+
}
|
|
431
|
+
// Settled as fulfilled but the value is nowhere to be found — should not happen (it is held in
|
|
432
|
+
// pendingResults until its DB write commits). Report not-found rather than a torn result.
|
|
433
|
+
this.logger.error(`Fulfilled proving job id=${id} has no retrievable result value`, { provingJobId: id });
|
|
434
|
+
return { status: 'not-found' };
|
|
435
|
+
}
|
|
349
436
|
|
|
350
|
-
|
|
437
|
+
// no result yet, check if we know the item
|
|
438
|
+
const item = this.jobsCache.get(id);
|
|
439
|
+
if (!item) {
|
|
440
|
+
return { status: 'not-found' };
|
|
351
441
|
}
|
|
442
|
+
return { status: this.inProgress.has(id) ? 'in-progress' : 'in-queue' };
|
|
352
443
|
}
|
|
353
444
|
|
|
354
445
|
#getCompletedJobs(ids: ProvingJobId[]): Promise<ProvingJobId[]> {
|
|
@@ -358,7 +449,9 @@ export class ProvingBroker implements ProvingJobProducer, ProvingJobConsumer, Pr
|
|
|
358
449
|
return Promise.resolve(notifications.concat(completedJobs));
|
|
359
450
|
}
|
|
360
451
|
|
|
361
|
-
#getProvingJob(
|
|
452
|
+
async #getProvingJob(
|
|
453
|
+
filter: ProvingJobFilter = { allowList: [] },
|
|
454
|
+
): Promise<{ job: ProvingJob; time: number } | undefined> {
|
|
362
455
|
const allowedProofs: ProvingRequestType[] =
|
|
363
456
|
Array.isArray(filter.allowList) && filter.allowList.length > 0
|
|
364
457
|
? [...filter.allowList]
|
|
@@ -373,21 +466,40 @@ export class ProvingBroker implements ProvingJobProducer, ProvingJobConsumer, Pr
|
|
|
373
466
|
// this can happen if the broker crashes and restarts
|
|
374
467
|
// it's possible agents will report progress or results for jobs that are in the queue (after the restart)
|
|
375
468
|
while ((enqueuedJob = queue.getImmediate())) {
|
|
376
|
-
const
|
|
377
|
-
if (
|
|
469
|
+
const meta = this.jobsCache.get(enqueuedJob.id);
|
|
470
|
+
if (meta && !this.inProgress.has(enqueuedJob.id) && !this.resultsCache.has(enqueuedJob.id)) {
|
|
378
471
|
const time = this.msTimeSource();
|
|
379
|
-
|
|
380
|
-
|
|
472
|
+
// Claim the job synchronously (before the await below) so a concurrent dispatch can't re-pick
|
|
473
|
+
// it. The same id can sit in a queue more than once (an abort leaves a stale entry that a
|
|
474
|
+
// later revive re-enqueues alongside), so this in-progress claim — not the queue pop — is what
|
|
475
|
+
// dedups those copies across concurrently-polling agents. It must be set before the await.
|
|
476
|
+
this.inProgress.set(meta.id, {
|
|
477
|
+
id: meta.id,
|
|
381
478
|
startedAt: time,
|
|
382
479
|
lastUpdatedAt: time,
|
|
383
480
|
});
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
481
|
+
|
|
482
|
+
// The large inputs are not kept in memory; read them from the database. They were durably
|
|
483
|
+
// persisted (addProvingJob's write commits) before the job became dispatchable, so this hits.
|
|
484
|
+
const inputsUri = await this.database.getProvingJobInputs(meta.id);
|
|
485
|
+
if (!inputsUri) {
|
|
486
|
+
// The job was cleaned up (or its inputs lost) after we claimed it — release the claim and
|
|
487
|
+
// keep draining the queue for another candidate. Nothing else was mutated yet, so there is
|
|
488
|
+
// no wait metric or enqueued-at timer to unwind.
|
|
489
|
+
this.inProgress.delete(meta.id);
|
|
490
|
+
this.logger.warn(`No inputs found for proving job id=${meta.id}; skipping dispatch`, {
|
|
491
|
+
provingJobId: meta.id,
|
|
492
|
+
});
|
|
493
|
+
continue;
|
|
389
494
|
}
|
|
390
495
|
|
|
496
|
+
// The dispatch is now committed: record the queue wait and clear the enqueued-at timer.
|
|
497
|
+
const enqueuedAt = this.enqueuedAt.get(meta.id);
|
|
498
|
+
if (enqueuedAt) {
|
|
499
|
+
this.instrumentation.recordJobWait(meta.type, enqueuedAt);
|
|
500
|
+
this.enqueuedAt.delete(meta.id);
|
|
501
|
+
}
|
|
502
|
+
const job: ProvingJob = { id: meta.id, type: meta.type, epochNumber: meta.epochNumber, inputsUri };
|
|
391
503
|
return { job, time };
|
|
392
504
|
}
|
|
393
505
|
}
|
|
@@ -401,7 +513,6 @@ export class ProvingBroker implements ProvingJobProducer, ProvingJobConsumer, Pr
|
|
|
401
513
|
err: string,
|
|
402
514
|
retry = false,
|
|
403
515
|
filter?: ProvingJobFilter,
|
|
404
|
-
aborted = false,
|
|
405
516
|
): Promise<GetProvingJobResponse | undefined> {
|
|
406
517
|
const info = this.inProgress.get(id);
|
|
407
518
|
const item = this.jobsCache.get(id);
|
|
@@ -455,18 +566,13 @@ export class ProvingBroker implements ProvingJobProducer, ProvingJobConsumer, Pr
|
|
|
455
566
|
},
|
|
456
567
|
);
|
|
457
568
|
|
|
458
|
-
// save the
|
|
459
|
-
//
|
|
460
|
-
|
|
461
|
-
this.
|
|
462
|
-
this.promises.get(id)!.resolve(result);
|
|
569
|
+
// save the (small) rejection reason to the cache and notify clients of the job status. The reason
|
|
570
|
+
// is kept in memory, so status reads work even if the database write below fails.
|
|
571
|
+
this.resultsCache.set(id, { status: 'rejected', reason: String(err) });
|
|
572
|
+
this.promises.get(id)!.resolve();
|
|
463
573
|
this.completedJobNotifications.push(id);
|
|
464
574
|
|
|
465
|
-
|
|
466
|
-
this.instrumentation.incAbortedJobs(item.type);
|
|
467
|
-
} else {
|
|
468
|
-
this.instrumentation.incRejectedJobs(item.type);
|
|
469
|
-
}
|
|
575
|
+
this.instrumentation.incRejectedJobs(item.type);
|
|
470
576
|
if (info) {
|
|
471
577
|
const duration = this.msTimeSource() - info.startedAt;
|
|
472
578
|
this.instrumentation.recordJobDuration(item.type, duration);
|
|
@@ -485,20 +591,20 @@ export class ProvingBroker implements ProvingJobProducer, ProvingJobConsumer, Pr
|
|
|
485
591
|
return this.#getProvingJob(filter);
|
|
486
592
|
}
|
|
487
593
|
|
|
488
|
-
#reportProvingJobProgress(
|
|
594
|
+
async #reportProvingJobProgress(
|
|
489
595
|
id: ProvingJobId,
|
|
490
596
|
startedAt: number,
|
|
491
597
|
filter?: ProvingJobFilter,
|
|
492
|
-
): { job: ProvingJob; time: number } | undefined {
|
|
598
|
+
): Promise<{ job: ProvingJob; time: number } | undefined> {
|
|
493
599
|
const job = this.jobsCache.get(id);
|
|
494
600
|
if (!job) {
|
|
495
601
|
this.logger.warn(`Proving job id=${id} does not exist`, { provingJobId: id });
|
|
496
|
-
return this.#getProvingJob(filter);
|
|
602
|
+
return await this.#getProvingJob(filter);
|
|
497
603
|
}
|
|
498
604
|
|
|
499
605
|
if (this.resultsCache.has(id)) {
|
|
500
606
|
this.logger.warn(`Proving job id=${id} has already been completed`, { provingJobId: id });
|
|
501
|
-
return this.#getProvingJob(filter);
|
|
607
|
+
return await this.#getProvingJob(filter);
|
|
502
608
|
}
|
|
503
609
|
|
|
504
610
|
const metadata = this.inProgress.get(id);
|
|
@@ -538,7 +644,7 @@ export class ProvingBroker implements ProvingJobProducer, ProvingJobConsumer, Pr
|
|
|
538
644
|
{ provingJobId: id },
|
|
539
645
|
);
|
|
540
646
|
|
|
541
|
-
return this.#getProvingJob(filter);
|
|
647
|
+
return await this.#getProvingJob(filter);
|
|
542
648
|
}
|
|
543
649
|
|
|
544
650
|
async #reportProvingJobSuccess(
|
|
@@ -572,12 +678,12 @@ export class ProvingBroker implements ProvingJobProducer, ProvingJobConsumer, Pr
|
|
|
572
678
|
{ provingJobId: id },
|
|
573
679
|
);
|
|
574
680
|
|
|
575
|
-
//
|
|
576
|
-
//
|
|
577
|
-
//
|
|
578
|
-
|
|
579
|
-
this.
|
|
580
|
-
this.promises.get(id)!.resolve(
|
|
681
|
+
// Mark settled synchronously (the gate that makes concurrent settles no-op, unchanged from before),
|
|
682
|
+
// holding only the status in resultsCache. The large proof value is kept transiently in
|
|
683
|
+
// pendingResults so status reads are read-your-writes until the database write below commits.
|
|
684
|
+
this.resultsCache.set(id, { status: 'fulfilled' });
|
|
685
|
+
this.pendingResults.set(id, { status: 'fulfilled', value });
|
|
686
|
+
this.promises.get(id)!.resolve();
|
|
581
687
|
this.completedJobNotifications.push(id);
|
|
582
688
|
|
|
583
689
|
this.instrumentation.incResolvedJobs(item.type);
|
|
@@ -588,7 +694,11 @@ export class ProvingBroker implements ProvingJobProducer, ProvingJobConsumer, Pr
|
|
|
588
694
|
|
|
589
695
|
try {
|
|
590
696
|
await this.database.setProvingJobResult(id, value);
|
|
697
|
+
// The value is durable in the database now; drop the transient in-memory copy.
|
|
698
|
+
this.pendingResults.delete(id);
|
|
591
699
|
} catch (saveErr) {
|
|
700
|
+
// Keep the value in pendingResults so status/result reads still succeed despite the failed write —
|
|
701
|
+
// this preserves the previous "works even if the database breaks" behaviour for this edge.
|
|
592
702
|
this.logger.error(`Failed to save proving job result id=${id}`, saveErr, {
|
|
593
703
|
provingJobId: id,
|
|
594
704
|
});
|
|
@@ -652,9 +762,8 @@ export class ProvingBroker implements ProvingJobProducer, ProvingJobConsumer, Pr
|
|
|
652
762
|
this.logger.error(`Proving job id=${id} timed out after ${retries + 1} attempts. Marking as failed.`, {
|
|
653
763
|
provingJobId: id,
|
|
654
764
|
});
|
|
655
|
-
|
|
656
|
-
this.
|
|
657
|
-
this.promises.get(id)?.resolve(result);
|
|
765
|
+
this.resultsCache.set(id, { status: 'rejected', reason: 'Timed out' });
|
|
766
|
+
this.promises.get(id)?.resolve();
|
|
658
767
|
this.completedJobNotifications.push(id);
|
|
659
768
|
this.instrumentation.incRejectedJobs(item.type);
|
|
660
769
|
}
|
|
@@ -662,7 +771,7 @@ export class ProvingBroker implements ProvingJobProducer, ProvingJobConsumer, Pr
|
|
|
662
771
|
}
|
|
663
772
|
}
|
|
664
773
|
|
|
665
|
-
private enqueueJobInternal(job:
|
|
774
|
+
private enqueueJobInternal(job: ProvingJobMetadata): void {
|
|
666
775
|
if (!this.promises.has(job.id)) {
|
|
667
776
|
this.promises.set(job.id, promiseWithResolvers());
|
|
668
777
|
}
|
|
@@ -674,7 +783,7 @@ export class ProvingBroker implements ProvingJobProducer, ProvingJobConsumer, Pr
|
|
|
674
783
|
this.epochHeight = Math.max(this.epochHeight, job.epochNumber);
|
|
675
784
|
}
|
|
676
785
|
|
|
677
|
-
private isJobStale(job:
|
|
786
|
+
private isJobStale(job: ProvingJobMetadata) {
|
|
678
787
|
return job.epochNumber < this.oldestEpochToKeep();
|
|
679
788
|
}
|
|
680
789
|
|
|
@@ -17,8 +17,12 @@ export class InMemoryBrokerDatabase implements ProvingBrokerDatabase {
|
|
|
17
17
|
return this.jobs.get(id);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
return this.
|
|
20
|
+
getProvingJobInputs(id: ProvingJobId): Promise<ProofUri | undefined> {
|
|
21
|
+
return Promise.resolve(this.jobs.get(id)?.inputsUri);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
getProvingJobResult(id: ProvingJobId): Promise<ProvingJobSettledResult | undefined> {
|
|
25
|
+
return Promise.resolve(this.results.get(id));
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
addProvingJob(job: ProvingJob): Promise<void> {
|
|
@@ -36,6 +40,16 @@ export class InMemoryBrokerDatabase implements ProvingBrokerDatabase {
|
|
|
36
40
|
return Promise.resolve();
|
|
37
41
|
}
|
|
38
42
|
|
|
43
|
+
setProvingJobAborted(id: ProvingJobId): Promise<void> {
|
|
44
|
+
this.results.set(id, { status: 'aborted' });
|
|
45
|
+
return Promise.resolve();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
deleteProvingJobResult(id: ProvingJobId): Promise<void> {
|
|
49
|
+
this.results.delete(id);
|
|
50
|
+
return Promise.resolve();
|
|
51
|
+
}
|
|
52
|
+
|
|
39
53
|
deleteProvingJobs(ids: ProvingJobId[]): Promise<void> {
|
|
40
54
|
for (const id of ids) {
|
|
41
55
|
this.jobs.delete(id);
|
|
@@ -41,7 +41,11 @@ class SingleEpochDatabase {
|
|
|
41
41
|
return this.store.estimateSize();
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
async batchWrite(
|
|
44
|
+
async batchWrite(
|
|
45
|
+
jobs: ProvingJob[],
|
|
46
|
+
results: Array<[ProvingJobId, ProvingJobSettledResult]>,
|
|
47
|
+
deletedResults: ProvingJobId[] = [],
|
|
48
|
+
) {
|
|
45
49
|
await this.store.transactionAsync(async () => {
|
|
46
50
|
for (const job of jobs) {
|
|
47
51
|
await this.jobs.set(job.id, jsonStringify(job));
|
|
@@ -49,6 +53,11 @@ class SingleEpochDatabase {
|
|
|
49
53
|
for (const [id, result] of results) {
|
|
50
54
|
await this.jobResults.set(id, jsonStringify(result));
|
|
51
55
|
}
|
|
56
|
+
// Deletes are applied after sets so that, if a set and a delete for the same id land in one
|
|
57
|
+
// batch (e.g. an abort immediately followed by a revive's result-delete), the delete wins.
|
|
58
|
+
for (const id of deletedResults) {
|
|
59
|
+
await this.jobResults.delete(id);
|
|
60
|
+
}
|
|
52
61
|
});
|
|
53
62
|
}
|
|
54
63
|
|
|
@@ -61,11 +70,26 @@ class SingleEpochDatabase {
|
|
|
61
70
|
}
|
|
62
71
|
}
|
|
63
72
|
|
|
73
|
+
async getProvingJobInputs(id: ProvingJobId): Promise<ProofUri | undefined> {
|
|
74
|
+
const jobStr = await this.jobs.getAsync(id);
|
|
75
|
+
return jobStr ? jsonParseWithSchema(jobStr, ProvingJob).inputsUri : undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async getProvingJobResult(id: ProvingJobId): Promise<ProvingJobSettledResult | undefined> {
|
|
79
|
+
const resultStr = await this.jobResults.getAsync(id);
|
|
80
|
+
return resultStr ? jsonParseWithSchema(resultStr, ProvingJobSettledResult) : undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
64
83
|
async setProvingJobError(id: ProvingJobId, reason: string): Promise<void> {
|
|
65
84
|
const result: ProvingJobSettledResult = { status: 'rejected', reason };
|
|
66
85
|
await this.jobResults.set(id, jsonStringify(result));
|
|
67
86
|
}
|
|
68
87
|
|
|
88
|
+
async setProvingJobAborted(id: ProvingJobId): Promise<void> {
|
|
89
|
+
const result: ProvingJobSettledResult = { status: 'aborted' };
|
|
90
|
+
await this.jobResults.set(id, jsonStringify(result));
|
|
91
|
+
}
|
|
92
|
+
|
|
69
93
|
async setProvingJobResult(id: ProvingJobId, value: ProofUri): Promise<void> {
|
|
70
94
|
const result: ProvingJobSettledResult = { status: 'fulfilled', value };
|
|
71
95
|
await this.jobResults.set(id, jsonStringify(result));
|
|
@@ -80,10 +104,19 @@ class SingleEpochDatabase {
|
|
|
80
104
|
}
|
|
81
105
|
}
|
|
82
106
|
|
|
107
|
+
/**
|
|
108
|
+
* An item queued for a batched write. A {@link ProvingJob} adds/updates a job; a `[id, result]`
|
|
109
|
+
* tuple sets a job's settled result; a `[id, undefined]` tuple deletes a job's result. Deletes ride
|
|
110
|
+
* the same queue as writes so they stay FIFO-ordered against them: a revive's result-delete must
|
|
111
|
+
* land after the abort that preceded it, otherwise a still-queued abort could flush later and
|
|
112
|
+
* resurrect the cancelled state on disk.
|
|
113
|
+
*/
|
|
114
|
+
type BrokerWrite = ProvingJob | [ProvingJobId, ProvingJobSettledResult | undefined];
|
|
115
|
+
|
|
83
116
|
export class KVBrokerDatabase implements ProvingBrokerDatabase {
|
|
84
117
|
private metrics: LmdbMetrics;
|
|
85
118
|
|
|
86
|
-
private batchQueue: BatchQueue<
|
|
119
|
+
private batchQueue: BatchQueue<BrokerWrite, number>;
|
|
87
120
|
|
|
88
121
|
public readonly tracer: Tracer;
|
|
89
122
|
|
|
@@ -112,12 +145,16 @@ export class KVBrokerDatabase implements ProvingBrokerDatabase {
|
|
|
112
145
|
}
|
|
113
146
|
|
|
114
147
|
// exposed for testing
|
|
115
|
-
public async commitWrites(items: Array<
|
|
148
|
+
public async commitWrites(items: Array<BrokerWrite>, epochNumber: number) {
|
|
116
149
|
const jobsToAdd = items.filter((item): item is ProvingJob => 'id' in item);
|
|
117
|
-
const
|
|
150
|
+
const resultOps = items.filter((item): item is [ProvingJobId, ProvingJobSettledResult | undefined] =>
|
|
151
|
+
Array.isArray(item),
|
|
152
|
+
);
|
|
153
|
+
const resultsToSet = resultOps.filter((op): op is [ProvingJobId, ProvingJobSettledResult] => op[1] !== undefined);
|
|
154
|
+
const resultsToDelete = resultOps.filter(op => op[1] === undefined).map(([id]) => id);
|
|
118
155
|
|
|
119
156
|
const db = await this.getEpochDatabase(EpochNumber(epochNumber));
|
|
120
|
-
await db.batchWrite(jobsToAdd,
|
|
157
|
+
await db.batchWrite(jobsToAdd, resultsToSet, resultsToDelete);
|
|
121
158
|
}
|
|
122
159
|
|
|
123
160
|
private async estimateSize() {
|
|
@@ -192,8 +229,13 @@ export class KVBrokerDatabase implements ProvingBrokerDatabase {
|
|
|
192
229
|
}
|
|
193
230
|
}
|
|
194
231
|
|
|
232
|
+
// Key jobs by the id-derived epoch, the same epoch results are written under (setProvingJobResult et
|
|
233
|
+
// al.). This keeps a job's inputs and its result co-located in one epoch database so both can be read
|
|
234
|
+
// back with a single keyed lookup (getProvingJobInputs/getProvingJobResult) rather than a scan. The id
|
|
235
|
+
// embeds the epoch and equals `job.epochNumber` at every construction site, so this matches the prior
|
|
236
|
+
// storage epoch while making the id the single source of truth for placement.
|
|
195
237
|
addProvingJob(job: ProvingJob): Promise<void> {
|
|
196
|
-
return this.batchQueue.put(job, job.
|
|
238
|
+
return this.batchQueue.put(job, getEpochFromProvingJobId(job.id));
|
|
197
239
|
}
|
|
198
240
|
|
|
199
241
|
async *allProvingJobs(): AsyncIterableIterator<[ProvingJob, ProvingJobSettledResult | undefined]> {
|
|
@@ -203,10 +245,31 @@ export class KVBrokerDatabase implements ProvingBrokerDatabase {
|
|
|
203
245
|
}
|
|
204
246
|
}
|
|
205
247
|
|
|
248
|
+
// A job id is `${epochNumber}:${type}:${inputsHash}`, so its epoch is recoverable directly and maps to
|
|
249
|
+
// exactly one epoch database. Results are already written under this same id-derived epoch (see
|
|
250
|
+
// setProvingJobResult et al.), and a job's `epochNumber` equals its id-epoch at every construction site,
|
|
251
|
+
// so inputs live there too. Use `epochs.get` (not `getEpochDatabase`, which would create the store on a
|
|
252
|
+
// miss) so a read for an unknown id has no side effects.
|
|
253
|
+
getProvingJobInputs(id: ProvingJobId): Promise<ProofUri | undefined> {
|
|
254
|
+
return this.epochs.get(getEpochFromProvingJobId(id))?.getProvingJobInputs(id) ?? Promise.resolve(undefined);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
getProvingJobResult(id: ProvingJobId): Promise<ProvingJobSettledResult | undefined> {
|
|
258
|
+
return this.epochs.get(getEpochFromProvingJobId(id))?.getProvingJobResult(id) ?? Promise.resolve(undefined);
|
|
259
|
+
}
|
|
260
|
+
|
|
206
261
|
setProvingJobError(id: ProvingJobId, reason: string): Promise<void> {
|
|
207
262
|
return this.batchQueue.put([id, { status: 'rejected', reason }], getEpochFromProvingJobId(id));
|
|
208
263
|
}
|
|
209
264
|
|
|
265
|
+
setProvingJobAborted(id: ProvingJobId): Promise<void> {
|
|
266
|
+
return this.batchQueue.put([id, { status: 'aborted' }], getEpochFromProvingJobId(id));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
deleteProvingJobResult(id: ProvingJobId): Promise<void> {
|
|
270
|
+
return this.batchQueue.put([id, undefined], getEpochFromProvingJobId(id));
|
|
271
|
+
}
|
|
272
|
+
|
|
210
273
|
setProvingJobResult(id: ProvingJobId, value: ProofUri): Promise<void> {
|
|
211
274
|
return this.batchQueue.put([id, { status: 'fulfilled', value }], getEpochFromProvingJobId(id));
|
|
212
275
|
}
|