@aztec/simulator 6.0.0-nightly.20260809 → 6.0.0-nightly.20260812
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.
|
@@ -8,13 +8,47 @@ export interface AvmSimulatorPoolOptions {
|
|
|
8
8
|
wsdbIpcPath: string;
|
|
9
9
|
/** Optional logger function for AVM process output. */
|
|
10
10
|
logger?: (msg: string) => void;
|
|
11
|
+
/**
|
|
12
|
+
* Flat delay between environmental spawn failures (default 1s). No backoff: sequencers live on
|
|
13
|
+
* ~6s slots, so sleeping longer than this after a failure costs whole blocks while the machine may
|
|
14
|
+
* have recovered — and a spawn attempt is cheap. Spawning never gives up on its own; the caller's
|
|
15
|
+
* deadline (abort signal) is the bound.
|
|
16
|
+
*/
|
|
17
|
+
spawnRetryIntervalMs?: number;
|
|
18
|
+
/** Process spawner override. Test hook; defaults to spawning a real bb-avm-sim via the generated AvmService. */
|
|
19
|
+
spawnProcess?: (options: AvmProcessSpawnOptions) => Promise<AvmProcessHandle>;
|
|
20
|
+
}
|
|
21
|
+
/** Options handed to the process spawner for each new pool slot. */
|
|
22
|
+
export interface AvmProcessSpawnOptions {
|
|
23
|
+
binaryPath?: string;
|
|
24
|
+
wsdbIpcPath: string;
|
|
25
|
+
cdbIpcPath: string;
|
|
26
|
+
logger?: (msg: string) => void;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A handle to a single bb-avm-sim service: it runs serialized simulations and connects back to the
|
|
30
|
+
* shared CDB/WSDB servers for state, but is unaware of which fork's contract data it is reading — that is
|
|
31
|
+
* routed by the fork id baked into the input buffer. The underlying service owns its process lifecycle
|
|
32
|
+
* (including respawn-on-death), so a handle stays usable for the life of the pool.
|
|
33
|
+
*/
|
|
34
|
+
export interface AvmProcessHandle {
|
|
35
|
+
simulate(inputBuffer: Uint8Array, signal?: AbortSignal): Promise<Uint8Array>;
|
|
36
|
+
simulateWithHints(inputBuffer: Uint8Array): Promise<Uint8Array>;
|
|
37
|
+
destroy(): Promise<void>;
|
|
11
38
|
}
|
|
12
39
|
/**
|
|
13
|
-
* The public-execution AVM backend: a lazily-grown pool of bb-avm-sim
|
|
40
|
+
* The public-execution AVM backend: a lazily-grown pool of bb-avm-sim services plus the CDB server that
|
|
14
41
|
* answers those processes' contract-data callbacks. Callers hold this as an {@link AvmSimulator}; the pool,
|
|
15
42
|
* the CDB server, its IPC path, and fork-id routing are all hidden behind that interface. Each `simulate`
|
|
16
43
|
* registers the call's contracts DB on the CDB server for the duration of the simulation (keyed by fork id
|
|
17
44
|
* so concurrent simulations on different forks don't collide) and unregisters it once the call returns.
|
|
45
|
+
*
|
|
46
|
+
* Process lifecycle is invisible to callers, exactly as when the simulator ran in-process: a simulation
|
|
47
|
+
* either produces a result, fails on its own merits, or runs until the caller's deadline aborts it.
|
|
48
|
+
* Environmental trouble is absorbed here — spawn failures retry indefinitely on a backoff ladder (bounded
|
|
49
|
+
* only by the caller's abort signal), and a simulation whose process dies is re-issued on the respawned
|
|
50
|
+
* process. The one deliberate exception: an input that kills the process twice is treated as a failing
|
|
51
|
+
* transaction, so a simulator-crashing tx gets evicted instead of burning a process per block forever.
|
|
18
52
|
*/
|
|
19
53
|
export declare class AvmSimulatorPool implements AvmSimulator {
|
|
20
54
|
private options;
|
|
@@ -22,24 +56,29 @@ export declare class AvmSimulatorPool implements AvmSimulator {
|
|
|
22
56
|
private available;
|
|
23
57
|
private waiters;
|
|
24
58
|
private createdCount;
|
|
59
|
+
private destroyed;
|
|
25
60
|
private log;
|
|
26
61
|
private maxSize;
|
|
27
62
|
private cdbServer;
|
|
63
|
+
private readonly spawnRetryIntervalMs;
|
|
64
|
+
private readonly spawnProcess;
|
|
28
65
|
constructor(options: AvmSimulatorPoolOptions);
|
|
29
66
|
static spawn(options: AvmSimulatorPoolOptions): Promise<AvmSimulatorPool>;
|
|
30
67
|
[Symbol.asyncDispose](): Promise<void>;
|
|
31
68
|
simulate(inputBuffer: Uint8Array, context: AvmContractsDBContext, signal?: AbortSignal): Promise<Uint8Array>;
|
|
32
69
|
simulateWithHints(inputBuffer: Uint8Array): Promise<Uint8Array>;
|
|
33
|
-
/** Destroy all AVM
|
|
70
|
+
/** Destroy all AVM services in the pool and close the CDB server. */
|
|
34
71
|
destroy(): Promise<void>;
|
|
35
72
|
/**
|
|
36
73
|
* Eagerly spawn up to `count` AVM processes (capped at maxSize) and leave them available, so the
|
|
37
74
|
* first simulate() doesn't pay process spawn/connect cost. Idempotent.
|
|
38
75
|
*/
|
|
39
76
|
prewarm(count?: number): Promise<void>;
|
|
77
|
+
private runOnPool;
|
|
40
78
|
private checkout;
|
|
41
|
-
/** Return an AVM
|
|
79
|
+
/** Return an AVM service to the pool after use. */
|
|
42
80
|
private return;
|
|
43
81
|
private createSlot;
|
|
82
|
+
private spawnUntilUp;
|
|
44
83
|
}
|
|
45
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
84
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXZtX3NpbXVsYXRvcl9wb29sLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvcHVibGljL2F2bV9zaW11bGF0b3JfcG9vbC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFLQSxPQUFPLEtBQUssRUFBRSxxQkFBcUIsRUFBRSxZQUFZLEVBQUUsTUFBTSxvQkFBb0IsQ0FBQztBQUc5RSxNQUFNLFdBQVcsdUJBQXVCO0lBQ3RDLHVIQUF1SDtJQUN2SCxPQUFPLENBQUMsRUFBRSxNQUFNLENBQUM7SUFDakIsb0ZBQW9GO0lBQ3BGLGFBQWEsQ0FBQyxFQUFFLE1BQU0sQ0FBQztJQUN2QiwyQ0FBMkM7SUFDM0MsV0FBVyxFQUFFLE1BQU0sQ0FBQztJQUNwQix1REFBdUQ7SUFDdkQsTUFBTSxDQUFDLEVBQUUsQ0FBQyxHQUFHLEVBQUUsTUFBTSxLQUFLLElBQUksQ0FBQztJQUMvQjs7Ozs7T0FLRztJQUNILG9CQUFvQixDQUFDLEVBQUUsTUFBTSxDQUFDO0lBQzlCLGdIQUFnSDtJQUNoSCxZQUFZLENBQUMsRUFBRSxDQUFDLE9BQU8sRUFBRSxzQkFBc0IsS0FBSyxPQUFPLENBQUMsZ0JBQWdCLENBQUMsQ0FBQztDQUMvRTtBQUVELG9FQUFvRTtBQUNwRSxNQUFNLFdBQVcsc0JBQXNCO0lBQ3JDLFVBQVUsQ0FBQyxFQUFFLE1BQU0sQ0FBQztJQUNwQixXQUFXLEVBQUUsTUFBTSxDQUFDO0lBQ3BCLFVBQVUsRUFBRSxNQUFNLENBQUM7SUFDbkIsTUFBTSxDQUFDLEVBQUUsQ0FBQyxHQUFHLEVBQUUsTUFBTSxLQUFLLElBQUksQ0FBQztDQUNoQztBQUVEOzs7OztHQUtHO0FBQ0gsTUFBTSxXQUFXLGdCQUFnQjtJQUMvQixRQUFRLENBQUMsV0FBVyxFQUFFLFVBQVUsRUFBRSxNQUFNLENBQUMsRUFBRSxXQUFXLEdBQUcsT0FBTyxDQUFDLFVBQVUsQ0FBQyxDQUFDO0lBQzdFLGlCQUFpQixDQUFDLFdBQVcsRUFBRSxVQUFVLEdBQUcsT0FBTyxDQUFDLFVBQVUsQ0FBQyxDQUFDO0lBQ2hFLE9BQU8sSUFBSSxPQUFPLENBQUMsSUFBSSxDQUFDLENBQUM7Q0FDMUI7QUFpQ0Q7Ozs7Ozs7Ozs7Ozs7R0FhRztBQUNILHFCQUFhLGdCQUFpQixZQUFXLFlBQVk7SUFZdkMsT0FBTyxDQUFDLE9BQU87SUFYM0IsT0FBTyxDQUFDLEtBQUssQ0FBMEI7SUFDdkMsT0FBTyxDQUFDLFNBQVMsQ0FBZ0I7SUFDakMsT0FBTyxDQUFDLE9BQU8sQ0FBaUc7SUFDaEgsT0FBTyxDQUFDLFlBQVksQ0FBSztJQUN6QixPQUFPLENBQUMsU0FBUyxDQUFTO0lBQzFCLE9BQU8sQ0FBQyxHQUFHLENBQVM7SUFDcEIsT0FBTyxDQUFDLE9BQU8sQ0FBUztJQUN4QixPQUFPLENBQUMsU0FBUyxDQUFlO0lBQ2hDLE9BQU8sQ0FBQyxRQUFRLENBQUMsb0JBQW9CLENBQVM7SUFDOUMsT0FBTyxDQUFDLFFBQVEsQ0FBQyxZQUFZLENBQWlFO0lBRTlGLFlBQW9CLE9BQU8sRUFBRSx1QkFBdUIsRUFNbkQ7SUFFRCxPQUFhLEtBQUssQ0FBQyxPQUFPLEVBQUUsdUJBQXVCLEdBQUcsT0FBTyxDQUFDLGdCQUFnQixDQUFDLENBTTlFO0lBRUssQ0FBQyxNQUFNLENBQUMsWUFBWSxDQUFDLElBQUksT0FBTyxDQUFDLElBQUksQ0FBQyxDQUUzQztJQUVLLFFBQVEsQ0FBQyxXQUFXLEVBQUUsVUFBVSxFQUFFLE9BQU8sRUFBRSxxQkFBcUIsRUFBRSxNQUFNLENBQUMsRUFBRSxXQUFXLEdBQUcsT0FBTyxDQUFDLFVBQVUsQ0FBQyxDQVNqSDtJQUVLLGlCQUFpQixDQUFDLFdBQVcsRUFBRSxVQUFVLEdBQUcsT0FBTyxDQUFDLFVBQVUsQ0FBQyxDQUdwRTtJQUVELHFFQUFxRTtJQUMvRCxPQUFPLElBQUksT0FBTyxDQUFDLElBQUksQ0FBQyxDQWE3QjtJQUVEOzs7T0FHRztJQUNHLE9BQU8sQ0FBQyxLQUFLLFNBQUksR0FBRyxPQUFPLENBQUMsSUFBSSxDQUFDLENBVXRDO1lBUWEsU0FBUztZQXdCVCxRQUFRO0lBcUN0QixtREFBbUQ7SUFDbkQsT0FBTyxDQUFDLE1BQU07WUFZQSxVQUFVO1lBeUJWLFlBQVk7Q0EyQjNCIn0=
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"avm_simulator_pool.d.ts","sourceRoot":"","sources":["../../src/public/avm_simulator_pool.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"avm_simulator_pool.d.ts","sourceRoot":"","sources":["../../src/public/avm_simulator_pool.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAG9E,MAAM,WAAW,uBAAuB;IACtC,uHAAuH;IACvH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oFAAoF;IACpF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,2CAA2C;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,uDAAuD;IACvD,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,gHAAgH;IAChH,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,sBAAsB,KAAK,OAAO,CAAC,gBAAgB,CAAC,CAAC;CAC/E;AAED,oEAAoE;AACpE,MAAM,WAAW,sBAAsB;IACrC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAChC;AAED;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,WAAW,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAC7E,iBAAiB,CAAC,WAAW,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAChE,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAiCD;;;;;;;;;;;;;GAaG;AACH,qBAAa,gBAAiB,YAAW,YAAY;IAYvC,OAAO,CAAC,OAAO;IAX3B,OAAO,CAAC,KAAK,CAA0B;IACvC,OAAO,CAAC,SAAS,CAAgB;IACjC,OAAO,CAAC,OAAO,CAAiG;IAChH,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,GAAG,CAAS;IACpB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAAe;IAChC,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAS;IAC9C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAiE;IAE9F,YAAoB,OAAO,EAAE,uBAAuB,EAMnD;IAED,OAAa,KAAK,CAAC,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAM9E;IAEK,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAE3C;IAEK,QAAQ,CAAC,WAAW,EAAE,UAAU,EAAE,OAAO,EAAE,qBAAqB,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CASjH;IAEK,iBAAiB,CAAC,WAAW,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAGpE;IAED,qEAAqE;IAC/D,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAa7B;IAED;;;OAGG;IACG,OAAO,CAAC,KAAK,SAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAUtC;YAQa,SAAS;YAwBT,QAAQ;IAqCtB,mDAAmD;IACnD,OAAO,CAAC,MAAM;YAYA,UAAU;YAyBV,YAAY;CA2B3B"}
|
|
@@ -1,36 +1,83 @@
|
|
|
1
1
|
var _computedKey;
|
|
2
2
|
import { AvmService } from '@aztec/bb-avm-sim';
|
|
3
|
+
import { AbortError } from '@aztec/foundation/error';
|
|
3
4
|
import { createLogger } from '@aztec/foundation/log';
|
|
5
|
+
import { sleep } from '@aztec/foundation/sleep';
|
|
4
6
|
import { CdbIpcServer } from './cdb_ipc_server.js';
|
|
7
|
+
/**
|
|
8
|
+
* The generated service flags errors caused by the death of the underlying process (rather than by the
|
|
9
|
+
* request itself) with `retry: true`. That distinction is interpreted here and goes no further: process
|
|
10
|
+
* lifecycle is invisible to the pool's callers.
|
|
11
|
+
*/ function isProcessFailure(err) {
|
|
12
|
+
return err instanceof Error && err.retry === true;
|
|
13
|
+
}
|
|
14
|
+
/** Sleep that wakes early (without throwing) when the signal aborts; callers re-check the signal. */ async function abortableSleep(ms, signal) {
|
|
15
|
+
if (!signal) {
|
|
16
|
+
return sleep(ms);
|
|
17
|
+
}
|
|
18
|
+
let onAbort;
|
|
19
|
+
const aborted = new Promise((resolve)=>{
|
|
20
|
+
onAbort = resolve;
|
|
21
|
+
signal.addEventListener('abort', onAbort, {
|
|
22
|
+
once: true
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
try {
|
|
26
|
+
await Promise.race([
|
|
27
|
+
sleep(ms),
|
|
28
|
+
aborted
|
|
29
|
+
]);
|
|
30
|
+
} finally{
|
|
31
|
+
signal.removeEventListener('abort', onAbort);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// After a cancellation is requested, how long the C++ process gets to cancel cooperatively (SIGUSR1
|
|
35
|
+
// checkpoint) before it is SIGKILLed. Killing is safe: the service respawns its process lazily, so the
|
|
36
|
+
// pool slot recovers instead of being leaked to a wedged simulation.
|
|
37
|
+
const CANCEL_KILL_GRACE_MS = 5_000;
|
|
5
38
|
_computedKey = Symbol.asyncDispose;
|
|
6
39
|
/**
|
|
7
|
-
* The public-execution AVM backend: a lazily-grown pool of bb-avm-sim
|
|
40
|
+
* The public-execution AVM backend: a lazily-grown pool of bb-avm-sim services plus the CDB server that
|
|
8
41
|
* answers those processes' contract-data callbacks. Callers hold this as an {@link AvmSimulator}; the pool,
|
|
9
42
|
* the CDB server, its IPC path, and fork-id routing are all hidden behind that interface. Each `simulate`
|
|
10
43
|
* registers the call's contracts DB on the CDB server for the duration of the simulation (keyed by fork id
|
|
11
44
|
* so concurrent simulations on different forks don't collide) and unregisters it once the call returns.
|
|
45
|
+
*
|
|
46
|
+
* Process lifecycle is invisible to callers, exactly as when the simulator ran in-process: a simulation
|
|
47
|
+
* either produces a result, fails on its own merits, or runs until the caller's deadline aborts it.
|
|
48
|
+
* Environmental trouble is absorbed here — spawn failures retry indefinitely on a backoff ladder (bounded
|
|
49
|
+
* only by the caller's abort signal), and a simulation whose process dies is re-issued on the respawned
|
|
50
|
+
* process. The one deliberate exception: an input that kills the process twice is treated as a failing
|
|
51
|
+
* transaction, so a simulator-crashing tx gets evicted instead of burning a process per block forever.
|
|
12
52
|
*/ export class AvmSimulatorPool {
|
|
13
53
|
options;
|
|
14
54
|
slots;
|
|
15
55
|
available;
|
|
16
56
|
waiters;
|
|
17
57
|
createdCount;
|
|
58
|
+
destroyed;
|
|
18
59
|
log;
|
|
19
60
|
maxSize;
|
|
20
61
|
cdbServer;
|
|
62
|
+
spawnRetryIntervalMs;
|
|
63
|
+
spawnProcess;
|
|
21
64
|
constructor(options){
|
|
22
65
|
this.options = options;
|
|
23
66
|
this.slots = [];
|
|
24
67
|
this.available = [];
|
|
25
68
|
this.waiters = [];
|
|
26
69
|
this.createdCount = 0;
|
|
70
|
+
this.destroyed = false;
|
|
27
71
|
this.log = createLogger('simulator:avm-pool');
|
|
28
72
|
this.maxSize = options.maxSize ?? parseInt(process.env.AVM_MAX_CONCURRENT_SIMULATIONS ?? '4', 10);
|
|
29
73
|
this.cdbServer = new CdbIpcServer();
|
|
74
|
+
this.spawnRetryIntervalMs = options.spawnRetryIntervalMs ?? 1_000;
|
|
75
|
+
this.spawnProcess = options.spawnProcess ?? ((spawnOptions)=>AvmSimulatorProcess.spawn(spawnOptions));
|
|
30
76
|
}
|
|
31
77
|
static async spawn(options) {
|
|
32
78
|
const pool = new AvmSimulatorPool(options);
|
|
33
79
|
// Always start one process up front so the first simulate() doesn't pay process spawn/connect cost.
|
|
80
|
+
// This is also where configuration errors (missing binary) surface, fast and fatally.
|
|
34
81
|
await pool.prewarm();
|
|
35
82
|
return pool;
|
|
36
83
|
}
|
|
@@ -42,37 +89,21 @@ _computedKey = Symbol.asyncDispose;
|
|
|
42
89
|
// and unregister once the simulation returns — registration is only needed while the call is running.
|
|
43
90
|
this.cdbServer.registerFork(context.forkId, context.contractsDB, context.timestamp);
|
|
44
91
|
try {
|
|
45
|
-
|
|
46
|
-
try {
|
|
47
|
-
return await simulator.simulate(inputBuffer, signal);
|
|
48
|
-
} finally{
|
|
49
|
-
this.return(simulator);
|
|
50
|
-
}
|
|
92
|
+
return await this.runOnPool((simulator)=>simulator.simulate(inputBuffer, signal), signal);
|
|
51
93
|
} finally{
|
|
52
94
|
this.cdbServer.unregisterFork(context.forkId);
|
|
53
95
|
}
|
|
54
96
|
}
|
|
55
97
|
async simulateWithHints(inputBuffer) {
|
|
56
98
|
// The hinted path makes no contract-data callbacks, so no CDB registration is needed.
|
|
57
|
-
|
|
58
|
-
try {
|
|
59
|
-
return await simulator.simulateWithHints(inputBuffer);
|
|
60
|
-
} finally{
|
|
61
|
-
this.return(simulator);
|
|
62
|
-
}
|
|
99
|
+
return await this.runOnPool((simulator)=>simulator.simulateWithHints(inputBuffer));
|
|
63
100
|
}
|
|
64
|
-
/** Destroy all AVM
|
|
65
|
-
|
|
101
|
+
/** Destroy all AVM services in the pool and close the CDB server. */ async destroy() {
|
|
102
|
+
this.destroyed = true;
|
|
103
|
+
for (const waiter of this.waiters.splice(0)){
|
|
66
104
|
waiter.reject(new Error('AVM simulator pool destroyed'));
|
|
67
105
|
}
|
|
68
|
-
this.
|
|
69
|
-
const destroyPromises = [];
|
|
70
|
-
for (const slot of this.slots){
|
|
71
|
-
if (slot) {
|
|
72
|
-
destroyPromises.push(slot.destroy());
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
await Promise.all(destroyPromises);
|
|
106
|
+
await Promise.all(this.slots.map((slot)=>slot.destroy()));
|
|
76
107
|
this.slots = [];
|
|
77
108
|
this.available = [];
|
|
78
109
|
this.createdCount = 0;
|
|
@@ -86,29 +117,81 @@ _computedKey = Symbol.asyncDispose;
|
|
|
86
117
|
const target = Math.min(count, this.maxSize);
|
|
87
118
|
const created = [];
|
|
88
119
|
while(this.createdCount < target){
|
|
89
|
-
created.push(await this.
|
|
120
|
+
created.push(await this.checkout());
|
|
90
121
|
}
|
|
91
|
-
// Hand the
|
|
122
|
+
// Hand the checked-out processes back to the pool so checkout() reuses them.
|
|
92
123
|
for (const simulator of created){
|
|
93
124
|
this.return(simulator);
|
|
94
125
|
}
|
|
95
126
|
}
|
|
96
|
-
/**
|
|
127
|
+
/**
|
|
128
|
+
* Run a call on a pooled service. A call that fails because its process died is re-issued on the
|
|
129
|
+
* respawned process; a second death for the same input is attributed to the input and surfaces as an
|
|
130
|
+
* ordinary error (the pre-IPC equivalent — a native crash — took down the whole node, so a failed tx
|
|
131
|
+
* is strictly gentler). Non-process failures surface as-is: they are the simulation's own verdict.
|
|
132
|
+
*/ async runOnPool(fn, signal) {
|
|
133
|
+
for(let attempt = 0;; attempt++){
|
|
134
|
+
const simulator = await this.checkout(signal);
|
|
135
|
+
try {
|
|
136
|
+
return await fn(simulator);
|
|
137
|
+
} catch (err) {
|
|
138
|
+
if (!isProcessFailure(err) || signal?.aborted) {
|
|
139
|
+
throw err;
|
|
140
|
+
}
|
|
141
|
+
if (attempt > 0) {
|
|
142
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
143
|
+
throw new Error(`AVM simulator process died twice running this simulation; attributing the failure to the input: ${message}`, {
|
|
144
|
+
cause: err
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
this.log.warn(`AVM process died during simulation; re-issuing once on the respawned process`, {
|
|
148
|
+
err
|
|
149
|
+
});
|
|
150
|
+
} finally{
|
|
151
|
+
this.return(simulator);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/** Check out an AVM service from the pool, blocking until one is free. Caller must return() it when done. */ async checkout(signal) {
|
|
156
|
+
if (this.destroyed) {
|
|
157
|
+
throw new Error('AVM simulator pool destroyed');
|
|
158
|
+
}
|
|
97
159
|
const idx = this.available.pop();
|
|
98
|
-
if (idx !== undefined
|
|
160
|
+
if (idx !== undefined) {
|
|
99
161
|
return this.slots[idx];
|
|
100
162
|
}
|
|
101
|
-
if (this.createdCount < this.maxSize
|
|
102
|
-
return await this.createSlot(
|
|
163
|
+
if (this.createdCount < this.maxSize) {
|
|
164
|
+
return await this.createSlot(signal);
|
|
103
165
|
}
|
|
104
166
|
return new Promise((resolve, reject)=>{
|
|
105
|
-
|
|
167
|
+
const waiter = {
|
|
106
168
|
resolve,
|
|
107
169
|
reject
|
|
108
|
-
}
|
|
170
|
+
};
|
|
171
|
+
if (signal) {
|
|
172
|
+
const onAbort = ()=>{
|
|
173
|
+
const at = this.waiters.indexOf(waiter);
|
|
174
|
+
if (at >= 0) {
|
|
175
|
+
this.waiters.splice(at, 1);
|
|
176
|
+
reject(new AbortError('AVM checkout aborted'));
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
signal.addEventListener('abort', onAbort, {
|
|
180
|
+
once: true
|
|
181
|
+
});
|
|
182
|
+
waiter.resolve = (simulator)=>{
|
|
183
|
+
signal.removeEventListener('abort', onAbort);
|
|
184
|
+
resolve(simulator);
|
|
185
|
+
};
|
|
186
|
+
waiter.reject = (err)=>{
|
|
187
|
+
signal.removeEventListener('abort', onAbort);
|
|
188
|
+
reject(err);
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
this.waiters.push(waiter);
|
|
109
192
|
});
|
|
110
193
|
}
|
|
111
|
-
/** Return an AVM
|
|
194
|
+
/** Return an AVM service to the pool after use. */ return(simulator) {
|
|
112
195
|
const waiter = this.waiters.shift();
|
|
113
196
|
if (waiter) {
|
|
114
197
|
waiter.resolve(simulator);
|
|
@@ -119,35 +202,54 @@ _computedKey = Symbol.asyncDispose;
|
|
|
119
202
|
}
|
|
120
203
|
}
|
|
121
204
|
}
|
|
122
|
-
async createSlot(
|
|
123
|
-
const reuse = reuseIdx !== undefined && reuseIdx < this.slots.length;
|
|
205
|
+
async createSlot(signal) {
|
|
124
206
|
// Reserve the slot count synchronously, before the async spawn, so concurrent checkouts can't all
|
|
125
207
|
// observe `createdCount < maxSize` and overshoot the pool (the count is only bumped once control has
|
|
126
|
-
// yielded on the await). Roll back the reservation if the spawn
|
|
127
|
-
|
|
128
|
-
this.createdCount++;
|
|
129
|
-
}
|
|
130
|
-
let simulator;
|
|
208
|
+
// yielded on the await). Roll back the reservation if the spawn is abandoned.
|
|
209
|
+
this.createdCount++;
|
|
131
210
|
try {
|
|
132
|
-
simulator = await
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
logger: this.options.logger
|
|
137
|
-
});
|
|
211
|
+
const simulator = await this.spawnUntilUp(signal);
|
|
212
|
+
this.slots.push(simulator);
|
|
213
|
+
this.log.debug(`Created AVM pool slot (${this.createdCount}/${this.maxSize})`);
|
|
214
|
+
return simulator;
|
|
138
215
|
} catch (err) {
|
|
139
|
-
|
|
140
|
-
this.createdCount--;
|
|
141
|
-
}
|
|
216
|
+
this.createdCount--;
|
|
142
217
|
throw err;
|
|
143
218
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Spawn a service, retrying environmental failures on a flat cadence indefinitely — the bound is
|
|
222
|
+
* the caller's own deadline (abort signal), matching the pre-IPC contract where a simulation either
|
|
223
|
+
* completed or was deadlined out. The cadence is deliberately fast and constant: an attempt is cheap,
|
|
224
|
+
* a slow one self-paces inside the backend's connect backstop, and backing off would cost a
|
|
225
|
+
* ~6s-slot sequencer whole blocks after the machine has already recovered. Configuration errors
|
|
226
|
+
* (missing binary, flagged non-retryable by the generated service) throw immediately; the boot-time
|
|
227
|
+
* prewarm is where those are meant to surface.
|
|
228
|
+
*/ async spawnUntilUp(signal) {
|
|
229
|
+
for(let failures = 0;; failures++){
|
|
230
|
+
if (this.destroyed) {
|
|
231
|
+
throw new Error('AVM simulator pool destroyed');
|
|
232
|
+
}
|
|
233
|
+
if (signal?.aborted) {
|
|
234
|
+
throw new AbortError('AVM process spawn aborted');
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
return await this.spawnProcess({
|
|
238
|
+
binaryPath: this.options.avmBinaryPath,
|
|
239
|
+
wsdbIpcPath: this.options.wsdbIpcPath,
|
|
240
|
+
cdbIpcPath: this.cdbServer.ipcPath,
|
|
241
|
+
logger: this.options.logger
|
|
242
|
+
});
|
|
243
|
+
} catch (err) {
|
|
244
|
+
if (!isProcessFailure(err)) {
|
|
245
|
+
throw err;
|
|
246
|
+
}
|
|
247
|
+
this.log.warn(`Failed to spawn AVM process (attempt ${failures + 1}); retrying in ${this.spawnRetryIntervalMs}ms`, {
|
|
248
|
+
err
|
|
249
|
+
});
|
|
250
|
+
await abortableSleep(this.spawnRetryIntervalMs, signal);
|
|
251
|
+
}
|
|
148
252
|
}
|
|
149
|
-
this.log.debug(`Created AVM pool slot (${this.createdCount}/${this.maxSize})`);
|
|
150
|
-
return simulator;
|
|
151
253
|
}
|
|
152
254
|
}
|
|
153
255
|
class AvmSimulatorProcess {
|
|
@@ -165,13 +267,22 @@ class AvmSimulatorProcess {
|
|
|
165
267
|
options.wsdbIpcPath,
|
|
166
268
|
'--cdb',
|
|
167
269
|
options.cdbIpcPath
|
|
168
|
-
]
|
|
270
|
+
],
|
|
271
|
+
// Each simulation is self-contained (state comes from the WSDB/CDB servers, routed by fork id),
|
|
272
|
+
// so a fresh process can safely serve the next call after a death.
|
|
273
|
+
respawn: true
|
|
169
274
|
});
|
|
170
275
|
return new AvmSimulatorProcess(service);
|
|
171
276
|
}
|
|
172
277
|
async simulate(inputBuffer, signal) {
|
|
173
|
-
|
|
174
|
-
|
|
278
|
+
let killTimer;
|
|
279
|
+
// Cooperative cancellation first: SIGUSR1 makes the C++ process stop at its next cancellation
|
|
280
|
+
// checkpoint. If it doesn't respond within the grace (wedged in a slow op), SIGKILL it — the
|
|
281
|
+
// service respawns lazily, so this reclaims the pool slot rather than leaking it.
|
|
282
|
+
const onAbort = ()=>{
|
|
283
|
+
this.service.sendProcessSignal('SIGUSR1');
|
|
284
|
+
killTimer = setTimeout(()=>this.service.sendProcessSignal('SIGKILL'), CANCEL_KILL_GRACE_MS);
|
|
285
|
+
};
|
|
175
286
|
if (signal?.aborted) {
|
|
176
287
|
onAbort();
|
|
177
288
|
}
|
|
@@ -184,6 +295,9 @@ class AvmSimulatorProcess {
|
|
|
184
295
|
})).result;
|
|
185
296
|
} finally{
|
|
186
297
|
signal?.removeEventListener('abort', onAbort);
|
|
298
|
+
if (killTimer !== undefined) {
|
|
299
|
+
clearTimeout(killTimer);
|
|
300
|
+
}
|
|
187
301
|
}
|
|
188
302
|
}
|
|
189
303
|
async simulateWithHints(inputBuffer) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aztec/simulator",
|
|
3
|
-
"version": "6.0.0-nightly.
|
|
3
|
+
"version": "6.0.0-nightly.20260812",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./server": "./dest/server.js",
|
|
@@ -63,29 +63,29 @@
|
|
|
63
63
|
]
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
|
-
"@aztec/bb-avm-sim": "6.0.0-nightly.
|
|
67
|
-
"@aztec/cdb": "6.0.0-nightly.
|
|
68
|
-
"@aztec/constants": "6.0.0-nightly.
|
|
69
|
-
"@aztec/foundation": "6.0.0-nightly.
|
|
70
|
-
"@aztec/ipc-runtime": "6.0.0-nightly.
|
|
71
|
-
"@aztec/native": "6.0.0-nightly.
|
|
72
|
-
"@aztec/noir-acvm_js": "6.0.0-nightly.
|
|
73
|
-
"@aztec/noir-noirc_abi": "6.0.0-nightly.
|
|
74
|
-
"@aztec/noir-types": "6.0.0-nightly.
|
|
75
|
-
"@aztec/protocol-contracts": "6.0.0-nightly.
|
|
76
|
-
"@aztec/standard-contracts": "6.0.0-nightly.
|
|
77
|
-
"@aztec/stdlib": "6.0.0-nightly.
|
|
78
|
-
"@aztec/telemetry-client": "6.0.0-nightly.
|
|
79
|
-
"@aztec/world-state": "6.0.0-nightly.
|
|
66
|
+
"@aztec/bb-avm-sim": "6.0.0-nightly.20260812",
|
|
67
|
+
"@aztec/cdb": "6.0.0-nightly.20260812",
|
|
68
|
+
"@aztec/constants": "6.0.0-nightly.20260812",
|
|
69
|
+
"@aztec/foundation": "6.0.0-nightly.20260812",
|
|
70
|
+
"@aztec/ipc-runtime": "6.0.0-nightly.20260812",
|
|
71
|
+
"@aztec/native": "6.0.0-nightly.20260812",
|
|
72
|
+
"@aztec/noir-acvm_js": "6.0.0-nightly.20260812",
|
|
73
|
+
"@aztec/noir-noirc_abi": "6.0.0-nightly.20260812",
|
|
74
|
+
"@aztec/noir-types": "6.0.0-nightly.20260812",
|
|
75
|
+
"@aztec/protocol-contracts": "6.0.0-nightly.20260812",
|
|
76
|
+
"@aztec/standard-contracts": "6.0.0-nightly.20260812",
|
|
77
|
+
"@aztec/stdlib": "6.0.0-nightly.20260812",
|
|
78
|
+
"@aztec/telemetry-client": "6.0.0-nightly.20260812",
|
|
79
|
+
"@aztec/world-state": "6.0.0-nightly.20260812",
|
|
80
80
|
"lodash.clonedeep": "^4.5.0",
|
|
81
81
|
"lodash.merge": "^4.6.2",
|
|
82
82
|
"msgpackr": "^1.11.2",
|
|
83
83
|
"tslib": "^2.4.0"
|
|
84
84
|
},
|
|
85
85
|
"devDependencies": {
|
|
86
|
-
"@aztec/kv-store": "6.0.0-nightly.
|
|
87
|
-
"@aztec/noir-contracts.js": "6.0.0-nightly.
|
|
88
|
-
"@aztec/noir-test-contracts.js": "6.0.0-nightly.
|
|
86
|
+
"@aztec/kv-store": "6.0.0-nightly.20260812",
|
|
87
|
+
"@aztec/noir-contracts.js": "6.0.0-nightly.20260812",
|
|
88
|
+
"@aztec/noir-test-contracts.js": "6.0.0-nightly.20260812",
|
|
89
89
|
"@jest/globals": "^30.0.0",
|
|
90
90
|
"@types/jest": "^30.0.0",
|
|
91
91
|
"@types/lodash.clonedeep": "^4.5.7",
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { AvmService } from '@aztec/bb-avm-sim';
|
|
2
|
+
import { AbortError } from '@aztec/foundation/error';
|
|
2
3
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
4
|
+
import { sleep } from '@aztec/foundation/sleep';
|
|
3
5
|
|
|
4
6
|
import type { AvmContractsDBContext, AvmSimulator } from './avm_simulator.js';
|
|
5
7
|
import { CdbIpcServer } from './cdb_ipc_server.js';
|
|
@@ -13,44 +15,106 @@ export interface AvmSimulatorPoolOptions {
|
|
|
13
15
|
wsdbIpcPath: string;
|
|
14
16
|
/** Optional logger function for AVM process output. */
|
|
15
17
|
logger?: (msg: string) => void;
|
|
18
|
+
/**
|
|
19
|
+
* Flat delay between environmental spawn failures (default 1s). No backoff: sequencers live on
|
|
20
|
+
* ~6s slots, so sleeping longer than this after a failure costs whole blocks while the machine may
|
|
21
|
+
* have recovered — and a spawn attempt is cheap. Spawning never gives up on its own; the caller's
|
|
22
|
+
* deadline (abort signal) is the bound.
|
|
23
|
+
*/
|
|
24
|
+
spawnRetryIntervalMs?: number;
|
|
25
|
+
/** Process spawner override. Test hook; defaults to spawning a real bb-avm-sim via the generated AvmService. */
|
|
26
|
+
spawnProcess?: (options: AvmProcessSpawnOptions) => Promise<AvmProcessHandle>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Options handed to the process spawner for each new pool slot. */
|
|
30
|
+
export interface AvmProcessSpawnOptions {
|
|
31
|
+
binaryPath?: string;
|
|
32
|
+
wsdbIpcPath: string;
|
|
33
|
+
cdbIpcPath: string;
|
|
34
|
+
logger?: (msg: string) => void;
|
|
16
35
|
}
|
|
17
36
|
|
|
18
37
|
/**
|
|
19
|
-
* A
|
|
38
|
+
* A handle to a single bb-avm-sim service: it runs serialized simulations and connects back to the
|
|
20
39
|
* shared CDB/WSDB servers for state, but is unaware of which fork's contract data it is reading — that is
|
|
21
|
-
* routed by the fork id baked into the input buffer.
|
|
40
|
+
* routed by the fork id baked into the input buffer. The underlying service owns its process lifecycle
|
|
41
|
+
* (including respawn-on-death), so a handle stays usable for the life of the pool.
|
|
22
42
|
*/
|
|
23
|
-
interface AvmProcessHandle {
|
|
43
|
+
export interface AvmProcessHandle {
|
|
24
44
|
simulate(inputBuffer: Uint8Array, signal?: AbortSignal): Promise<Uint8Array>;
|
|
25
45
|
simulateWithHints(inputBuffer: Uint8Array): Promise<Uint8Array>;
|
|
26
46
|
destroy(): Promise<void>;
|
|
27
47
|
}
|
|
28
48
|
|
|
29
49
|
/**
|
|
30
|
-
* The
|
|
50
|
+
* The generated service flags errors caused by the death of the underlying process (rather than by the
|
|
51
|
+
* request itself) with `retry: true`. That distinction is interpreted here and goes no further: process
|
|
52
|
+
* lifecycle is invisible to the pool's callers.
|
|
53
|
+
*/
|
|
54
|
+
function isProcessFailure(err: unknown): boolean {
|
|
55
|
+
return err instanceof Error && (err as Error & { retry?: unknown }).retry === true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Sleep that wakes early (without throwing) when the signal aborts; callers re-check the signal. */
|
|
59
|
+
async function abortableSleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
60
|
+
if (!signal) {
|
|
61
|
+
return sleep(ms);
|
|
62
|
+
}
|
|
63
|
+
let onAbort: () => void;
|
|
64
|
+
const aborted = new Promise<void>(resolve => {
|
|
65
|
+
onAbort = resolve;
|
|
66
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
67
|
+
});
|
|
68
|
+
try {
|
|
69
|
+
await Promise.race([sleep(ms), aborted]);
|
|
70
|
+
} finally {
|
|
71
|
+
signal.removeEventListener('abort', onAbort!);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// After a cancellation is requested, how long the C++ process gets to cancel cooperatively (SIGUSR1
|
|
76
|
+
// checkpoint) before it is SIGKILLed. Killing is safe: the service respawns its process lazily, so the
|
|
77
|
+
// pool slot recovers instead of being leaked to a wedged simulation.
|
|
78
|
+
const CANCEL_KILL_GRACE_MS = 5_000;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The public-execution AVM backend: a lazily-grown pool of bb-avm-sim services plus the CDB server that
|
|
31
82
|
* answers those processes' contract-data callbacks. Callers hold this as an {@link AvmSimulator}; the pool,
|
|
32
83
|
* the CDB server, its IPC path, and fork-id routing are all hidden behind that interface. Each `simulate`
|
|
33
84
|
* registers the call's contracts DB on the CDB server for the duration of the simulation (keyed by fork id
|
|
34
85
|
* so concurrent simulations on different forks don't collide) and unregisters it once the call returns.
|
|
86
|
+
*
|
|
87
|
+
* Process lifecycle is invisible to callers, exactly as when the simulator ran in-process: a simulation
|
|
88
|
+
* either produces a result, fails on its own merits, or runs until the caller's deadline aborts it.
|
|
89
|
+
* Environmental trouble is absorbed here — spawn failures retry indefinitely on a backoff ladder (bounded
|
|
90
|
+
* only by the caller's abort signal), and a simulation whose process dies is re-issued on the respawned
|
|
91
|
+
* process. The one deliberate exception: an input that kills the process twice is treated as a failing
|
|
92
|
+
* transaction, so a simulator-crashing tx gets evicted instead of burning a process per block forever.
|
|
35
93
|
*/
|
|
36
94
|
export class AvmSimulatorPool implements AvmSimulator {
|
|
37
|
-
private slots:
|
|
95
|
+
private slots: AvmProcessHandle[] = [];
|
|
38
96
|
private available: number[] = [];
|
|
39
97
|
private waiters: Array<{ resolve: (simulator: AvmProcessHandle) => void; reject: (error: Error) => void }> = [];
|
|
40
98
|
private createdCount = 0;
|
|
99
|
+
private destroyed = false;
|
|
41
100
|
private log: Logger;
|
|
42
101
|
private maxSize: number;
|
|
43
102
|
private cdbServer: CdbIpcServer;
|
|
103
|
+
private readonly spawnRetryIntervalMs: number;
|
|
104
|
+
private readonly spawnProcess: (options: AvmProcessSpawnOptions) => Promise<AvmProcessHandle>;
|
|
44
105
|
|
|
45
106
|
constructor(private options: AvmSimulatorPoolOptions) {
|
|
46
107
|
this.log = createLogger('simulator:avm-pool');
|
|
47
108
|
this.maxSize = options.maxSize ?? parseInt(process.env.AVM_MAX_CONCURRENT_SIMULATIONS ?? '4', 10);
|
|
48
109
|
this.cdbServer = new CdbIpcServer();
|
|
110
|
+
this.spawnRetryIntervalMs = options.spawnRetryIntervalMs ?? 1_000;
|
|
111
|
+
this.spawnProcess = options.spawnProcess ?? (spawnOptions => AvmSimulatorProcess.spawn(spawnOptions));
|
|
49
112
|
}
|
|
50
113
|
|
|
51
114
|
static async spawn(options: AvmSimulatorPoolOptions): Promise<AvmSimulatorPool> {
|
|
52
115
|
const pool = new AvmSimulatorPool(options);
|
|
53
116
|
// Always start one process up front so the first simulate() doesn't pay process spawn/connect cost.
|
|
117
|
+
// This is also where configuration errors (missing binary) surface, fast and fatally.
|
|
54
118
|
await pool.prewarm();
|
|
55
119
|
return pool;
|
|
56
120
|
}
|
|
@@ -64,12 +128,7 @@ export class AvmSimulatorPool implements AvmSimulator {
|
|
|
64
128
|
// and unregister once the simulation returns — registration is only needed while the call is running.
|
|
65
129
|
this.cdbServer.registerFork(context.forkId, context.contractsDB, context.timestamp);
|
|
66
130
|
try {
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
return await simulator.simulate(inputBuffer, signal);
|
|
70
|
-
} finally {
|
|
71
|
-
this.return(simulator);
|
|
72
|
-
}
|
|
131
|
+
return await this.runOnPool(simulator => simulator.simulate(inputBuffer, signal), signal);
|
|
73
132
|
} finally {
|
|
74
133
|
this.cdbServer.unregisterFork(context.forkId);
|
|
75
134
|
}
|
|
@@ -77,28 +136,17 @@ export class AvmSimulatorPool implements AvmSimulator {
|
|
|
77
136
|
|
|
78
137
|
async simulateWithHints(inputBuffer: Uint8Array): Promise<Uint8Array> {
|
|
79
138
|
// The hinted path makes no contract-data callbacks, so no CDB registration is needed.
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
return await simulator.simulateWithHints(inputBuffer);
|
|
83
|
-
} finally {
|
|
84
|
-
this.return(simulator);
|
|
85
|
-
}
|
|
139
|
+
return await this.runOnPool(simulator => simulator.simulateWithHints(inputBuffer));
|
|
86
140
|
}
|
|
87
141
|
|
|
88
|
-
/** Destroy all AVM
|
|
142
|
+
/** Destroy all AVM services in the pool and close the CDB server. */
|
|
89
143
|
async destroy(): Promise<void> {
|
|
90
|
-
|
|
144
|
+
this.destroyed = true;
|
|
145
|
+
for (const waiter of this.waiters.splice(0)) {
|
|
91
146
|
waiter.reject(new Error('AVM simulator pool destroyed'));
|
|
92
147
|
}
|
|
93
|
-
this.waiters = [];
|
|
94
148
|
|
|
95
|
-
|
|
96
|
-
for (const slot of this.slots) {
|
|
97
|
-
if (slot) {
|
|
98
|
-
destroyPromises.push(slot.destroy());
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
await Promise.all(destroyPromises);
|
|
149
|
+
await Promise.all(this.slots.map(slot => slot.destroy()));
|
|
102
150
|
|
|
103
151
|
this.slots = [];
|
|
104
152
|
this.available = [];
|
|
@@ -115,31 +163,82 @@ export class AvmSimulatorPool implements AvmSimulator {
|
|
|
115
163
|
const target = Math.min(count, this.maxSize);
|
|
116
164
|
const created: AvmProcessHandle[] = [];
|
|
117
165
|
while (this.createdCount < target) {
|
|
118
|
-
created.push(await this.
|
|
166
|
+
created.push(await this.checkout());
|
|
119
167
|
}
|
|
120
|
-
// Hand the
|
|
168
|
+
// Hand the checked-out processes back to the pool so checkout() reuses them.
|
|
121
169
|
for (const simulator of created) {
|
|
122
170
|
this.return(simulator);
|
|
123
171
|
}
|
|
124
172
|
}
|
|
125
173
|
|
|
126
|
-
/**
|
|
127
|
-
|
|
174
|
+
/**
|
|
175
|
+
* Run a call on a pooled service. A call that fails because its process died is re-issued on the
|
|
176
|
+
* respawned process; a second death for the same input is attributed to the input and surfaces as an
|
|
177
|
+
* ordinary error (the pre-IPC equivalent — a native crash — took down the whole node, so a failed tx
|
|
178
|
+
* is strictly gentler). Non-process failures surface as-is: they are the simulation's own verdict.
|
|
179
|
+
*/
|
|
180
|
+
private async runOnPool<T>(fn: (simulator: AvmProcessHandle) => Promise<T>, signal?: AbortSignal): Promise<T> {
|
|
181
|
+
for (let attempt = 0; ; attempt++) {
|
|
182
|
+
const simulator = await this.checkout(signal);
|
|
183
|
+
try {
|
|
184
|
+
return await fn(simulator);
|
|
185
|
+
} catch (err) {
|
|
186
|
+
if (!isProcessFailure(err) || signal?.aborted) {
|
|
187
|
+
throw err;
|
|
188
|
+
}
|
|
189
|
+
if (attempt > 0) {
|
|
190
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
191
|
+
throw new Error(
|
|
192
|
+
`AVM simulator process died twice running this simulation; attributing the failure to the input: ${message}`,
|
|
193
|
+
{ cause: err },
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
this.log.warn(`AVM process died during simulation; re-issuing once on the respawned process`, { err });
|
|
197
|
+
} finally {
|
|
198
|
+
this.return(simulator);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Check out an AVM service from the pool, blocking until one is free. Caller must return() it when done. */
|
|
204
|
+
private async checkout(signal?: AbortSignal): Promise<AvmProcessHandle> {
|
|
205
|
+
if (this.destroyed) {
|
|
206
|
+
throw new Error('AVM simulator pool destroyed');
|
|
207
|
+
}
|
|
128
208
|
const idx = this.available.pop();
|
|
129
|
-
if (idx !== undefined
|
|
130
|
-
return this.slots[idx]
|
|
209
|
+
if (idx !== undefined) {
|
|
210
|
+
return this.slots[idx];
|
|
131
211
|
}
|
|
132
212
|
|
|
133
|
-
if (this.createdCount < this.maxSize
|
|
134
|
-
return await this.createSlot(
|
|
213
|
+
if (this.createdCount < this.maxSize) {
|
|
214
|
+
return await this.createSlot(signal);
|
|
135
215
|
}
|
|
136
216
|
|
|
137
217
|
return new Promise<AvmProcessHandle>((resolve, reject) => {
|
|
138
|
-
|
|
218
|
+
const waiter = { resolve, reject };
|
|
219
|
+
if (signal) {
|
|
220
|
+
const onAbort = () => {
|
|
221
|
+
const at = this.waiters.indexOf(waiter);
|
|
222
|
+
if (at >= 0) {
|
|
223
|
+
this.waiters.splice(at, 1);
|
|
224
|
+
reject(new AbortError('AVM checkout aborted'));
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
228
|
+
waiter.resolve = simulator => {
|
|
229
|
+
signal.removeEventListener('abort', onAbort);
|
|
230
|
+
resolve(simulator);
|
|
231
|
+
};
|
|
232
|
+
waiter.reject = err => {
|
|
233
|
+
signal.removeEventListener('abort', onAbort);
|
|
234
|
+
reject(err);
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
this.waiters.push(waiter);
|
|
139
238
|
});
|
|
140
239
|
}
|
|
141
240
|
|
|
142
|
-
/** Return an AVM
|
|
241
|
+
/** Return an AVM service to the pool after use. */
|
|
143
242
|
private return(simulator: AvmProcessHandle): void {
|
|
144
243
|
const waiter = this.waiters.shift();
|
|
145
244
|
if (waiter) {
|
|
@@ -152,59 +251,85 @@ export class AvmSimulatorPool implements AvmSimulator {
|
|
|
152
251
|
}
|
|
153
252
|
}
|
|
154
253
|
|
|
155
|
-
private async createSlot(
|
|
156
|
-
const reuse = reuseIdx !== undefined && reuseIdx < this.slots.length;
|
|
254
|
+
private async createSlot(signal?: AbortSignal): Promise<AvmProcessHandle> {
|
|
157
255
|
// Reserve the slot count synchronously, before the async spawn, so concurrent checkouts can't all
|
|
158
256
|
// observe `createdCount < maxSize` and overshoot the pool (the count is only bumped once control has
|
|
159
|
-
// yielded on the await). Roll back the reservation if the spawn
|
|
160
|
-
|
|
161
|
-
this.createdCount++;
|
|
162
|
-
}
|
|
163
|
-
let simulator: AvmProcessHandle;
|
|
257
|
+
// yielded on the await). Roll back the reservation if the spawn is abandoned.
|
|
258
|
+
this.createdCount++;
|
|
164
259
|
try {
|
|
165
|
-
simulator = await
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
logger: this.options.logger,
|
|
170
|
-
});
|
|
260
|
+
const simulator = await this.spawnUntilUp(signal);
|
|
261
|
+
this.slots.push(simulator);
|
|
262
|
+
this.log.debug(`Created AVM pool slot (${this.createdCount}/${this.maxSize})`);
|
|
263
|
+
return simulator;
|
|
171
264
|
} catch (err) {
|
|
172
|
-
|
|
173
|
-
this.createdCount--;
|
|
174
|
-
}
|
|
265
|
+
this.createdCount--;
|
|
175
266
|
throw err;
|
|
176
267
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Spawn a service, retrying environmental failures on a flat cadence indefinitely — the bound is
|
|
272
|
+
* the caller's own deadline (abort signal), matching the pre-IPC contract where a simulation either
|
|
273
|
+
* completed or was deadlined out. The cadence is deliberately fast and constant: an attempt is cheap,
|
|
274
|
+
* a slow one self-paces inside the backend's connect backstop, and backing off would cost a
|
|
275
|
+
* ~6s-slot sequencer whole blocks after the machine has already recovered. Configuration errors
|
|
276
|
+
* (missing binary, flagged non-retryable by the generated service) throw immediately; the boot-time
|
|
277
|
+
* prewarm is where those are meant to surface.
|
|
278
|
+
*/
|
|
279
|
+
private async spawnUntilUp(signal?: AbortSignal): Promise<AvmProcessHandle> {
|
|
280
|
+
for (let failures = 0; ; failures++) {
|
|
281
|
+
if (this.destroyed) {
|
|
282
|
+
throw new Error('AVM simulator pool destroyed');
|
|
283
|
+
}
|
|
284
|
+
if (signal?.aborted) {
|
|
285
|
+
throw new AbortError('AVM process spawn aborted');
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
return await this.spawnProcess({
|
|
289
|
+
binaryPath: this.options.avmBinaryPath,
|
|
290
|
+
wsdbIpcPath: this.options.wsdbIpcPath,
|
|
291
|
+
cdbIpcPath: this.cdbServer.ipcPath,
|
|
292
|
+
logger: this.options.logger,
|
|
293
|
+
});
|
|
294
|
+
} catch (err) {
|
|
295
|
+
if (!isProcessFailure(err)) {
|
|
296
|
+
throw err;
|
|
297
|
+
}
|
|
298
|
+
this.log.warn(
|
|
299
|
+
`Failed to spawn AVM process (attempt ${failures + 1}); retrying in ${this.spawnRetryIntervalMs}ms`,
|
|
300
|
+
{ err },
|
|
301
|
+
);
|
|
302
|
+
await abortableSleep(this.spawnRetryIntervalMs, signal);
|
|
303
|
+
}
|
|
181
304
|
}
|
|
182
|
-
this.log.debug(`Created AVM pool slot (${this.createdCount}/${this.maxSize})`);
|
|
183
|
-
return simulator;
|
|
184
305
|
}
|
|
185
306
|
}
|
|
186
307
|
|
|
187
308
|
class AvmSimulatorProcess implements AvmProcessHandle {
|
|
188
309
|
private constructor(private service: AvmService) {}
|
|
189
310
|
|
|
190
|
-
static async spawn(options: {
|
|
191
|
-
binaryPath?: string;
|
|
192
|
-
wsdbIpcPath: string;
|
|
193
|
-
cdbIpcPath: string;
|
|
194
|
-
logger?: (msg: string) => void;
|
|
195
|
-
}): Promise<AvmSimulatorProcess> {
|
|
311
|
+
static async spawn(options: AvmProcessSpawnOptions): Promise<AvmSimulatorProcess> {
|
|
196
312
|
const service = await AvmService.spawn({
|
|
197
313
|
binaryPath: options.binaryPath,
|
|
198
314
|
transport: 'uds',
|
|
199
315
|
logger: options.logger,
|
|
200
316
|
extraArgs: ['--wsdb', options.wsdbIpcPath, '--cdb', options.cdbIpcPath],
|
|
317
|
+
// Each simulation is self-contained (state comes from the WSDB/CDB servers, routed by fork id),
|
|
318
|
+
// so a fresh process can safely serve the next call after a death.
|
|
319
|
+
respawn: true,
|
|
201
320
|
});
|
|
202
321
|
return new AvmSimulatorProcess(service);
|
|
203
322
|
}
|
|
204
323
|
|
|
205
324
|
public async simulate(inputBuffer: Uint8Array, signal?: AbortSignal): Promise<Uint8Array> {
|
|
206
|
-
|
|
207
|
-
|
|
325
|
+
let killTimer: NodeJS.Timeout | undefined;
|
|
326
|
+
// Cooperative cancellation first: SIGUSR1 makes the C++ process stop at its next cancellation
|
|
327
|
+
// checkpoint. If it doesn't respond within the grace (wedged in a slow op), SIGKILL it — the
|
|
328
|
+
// service respawns lazily, so this reclaims the pool slot rather than leaking it.
|
|
329
|
+
const onAbort = () => {
|
|
330
|
+
this.service.sendProcessSignal('SIGUSR1');
|
|
331
|
+
killTimer = setTimeout(() => this.service.sendProcessSignal('SIGKILL'), CANCEL_KILL_GRACE_MS);
|
|
332
|
+
};
|
|
208
333
|
if (signal?.aborted) {
|
|
209
334
|
onAbort();
|
|
210
335
|
}
|
|
@@ -213,6 +338,9 @@ class AvmSimulatorProcess implements AvmProcessHandle {
|
|
|
213
338
|
return (await this.service.simulate({ inputs: inputBuffer })).result;
|
|
214
339
|
} finally {
|
|
215
340
|
signal?.removeEventListener('abort', onAbort);
|
|
341
|
+
if (killTimer !== undefined) {
|
|
342
|
+
clearTimeout(killTimer);
|
|
343
|
+
}
|
|
216
344
|
}
|
|
217
345
|
}
|
|
218
346
|
|