@effectstream/runtime 0.100.20 → 0.100.21
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/package.json +13 -13
- package/src/api/http-server.ts +4 -0
- package/src/main.ts +54 -58
- package/test/reproduction/buffering.test.ts +263 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effectstream/runtime",
|
|
3
|
-
"version": "0.100.
|
|
3
|
+
"version": "0.100.21",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"homepage": "https://effectstream.github.io/docs/",
|
|
6
6
|
"repository": {
|
|
@@ -13,20 +13,20 @@
|
|
|
13
13
|
},
|
|
14
14
|
"exports": "./src/mod.ts",
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@effectstream/concise": "0.100.
|
|
17
|
-
"@effectstream/config": "0.100.
|
|
18
|
-
"@effectstream/coroutine": "0.100.
|
|
19
|
-
"@effectstream/crypto": "0.100.
|
|
20
|
-
"@effectstream/db": "0.100.
|
|
21
|
-
"@effectstream/event-client": "0.100.
|
|
22
|
-
"@effectstream/event-server": "0.100.
|
|
23
|
-
"@effectstream/log": "0.100.
|
|
24
|
-
"@effectstream/sm": "0.100.
|
|
25
|
-
"@effectstream/sync": "0.100.
|
|
26
|
-
"@effectstream/utils": "0.100.
|
|
16
|
+
"@effectstream/concise": "0.100.21",
|
|
17
|
+
"@effectstream/config": "0.100.21",
|
|
18
|
+
"@effectstream/coroutine": "0.100.21",
|
|
19
|
+
"@effectstream/crypto": "0.100.21",
|
|
20
|
+
"@effectstream/db": "0.100.21",
|
|
21
|
+
"@effectstream/event-client": "0.100.21",
|
|
22
|
+
"@effectstream/event-server": "0.100.21",
|
|
23
|
+
"@effectstream/log": "0.100.21",
|
|
24
|
+
"@effectstream/sm": "0.100.21",
|
|
25
|
+
"@effectstream/sync": "0.100.21",
|
|
26
|
+
"@effectstream/utils": "0.100.21",
|
|
27
27
|
"@fastify/cors": "^11.0.1",
|
|
28
28
|
"@metamask/json-rpc-engine": "^10.0.3",
|
|
29
|
-
"pg": "8.
|
|
29
|
+
"pg": "8.21.0",
|
|
30
30
|
"effection": "^3.5.0",
|
|
31
31
|
"@opentelemetry/api": "^1.9.0",
|
|
32
32
|
"@opentelemetry/auto-instrumentations-node": "^0.54.0",
|
package/src/api/http-server.ts
CHANGED
|
@@ -504,6 +504,10 @@ export const startHttpServer = function* (
|
|
|
504
504
|
pausedNow: p.pausedNow,
|
|
505
505
|
pauses: p.backpressurePauses,
|
|
506
506
|
pausedMs: p.backpressurePausedMs,
|
|
507
|
+
// Merge-demand exemption: when true the merge is gated on this chain's
|
|
508
|
+
// page and the cap is lifted.
|
|
509
|
+
mergeWaiting: p.mergeWaitingForPage,
|
|
510
|
+
mergeDemandRoot: p.mergeDemandRoot ?? null,
|
|
507
511
|
})),
|
|
508
512
|
// Apply-stage lag: how old the last-applied block is. Unlike `buf` (fetch
|
|
509
513
|
// backlog) this stays high when the node is write/apply-bound.
|
package/src/main.ts
CHANGED
|
@@ -6,11 +6,13 @@ import {
|
|
|
6
6
|
import {
|
|
7
7
|
acquireDBMutex,
|
|
8
8
|
createDynamicTables,
|
|
9
|
+
detectCapabilities,
|
|
9
10
|
getConnection,
|
|
10
11
|
getLastNonEmptyBlockHash,
|
|
11
12
|
releaseDBMutex,
|
|
12
13
|
resetPublicTables,
|
|
13
14
|
runSnapshotLoop,
|
|
15
|
+
selectViewStrategy,
|
|
14
16
|
} from "@effectstream/db";
|
|
15
17
|
import { EventBroker } from "@effectstream/event-server";
|
|
16
18
|
import { ENV } from "@effectstream/utils/node-env";
|
|
@@ -103,12 +105,11 @@ export function* start(config: StartConfig): Operation<void> {
|
|
|
103
105
|
const finalizedBlockStream = createChannel<ChainBlock>();
|
|
104
106
|
|
|
105
107
|
// Subscribe BEFORE spawning the merge: effection channels drop sends with no
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
+
// subscriber, so a fast restart could emit blocks before we subscribe and
|
|
109
|
+
// stall at the pre-restart height.
|
|
108
110
|
const finalizedBlocks = yield* finalizedBlockStream;
|
|
109
|
-
// NOTE: this subscriber's queue is unbounded
|
|
110
|
-
// can outpace
|
|
111
|
-
// downstream side of the Fix C backpressure issue (see sync/CLAUDE.md).
|
|
111
|
+
// NOTE: this subscriber's queue is unbounded — during deep catch-up the merge
|
|
112
|
+
// can outpace block processing and grow it (Fix C backpressure, sync/CLAUDE.md).
|
|
112
113
|
|
|
113
114
|
yield* spawn(() => startMerge(syncProtocols, finalizedBlockStream));
|
|
114
115
|
|
|
@@ -182,10 +183,9 @@ export function* start(config: StartConfig): Operation<void> {
|
|
|
182
183
|
const value = yield* coalescer.advance();
|
|
183
184
|
if (value === undefined) break;
|
|
184
185
|
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
// the block's COMMIT.
|
|
186
|
+
// Owns connection checkout, the per-block DB mutex (PGLite), and
|
|
187
|
+
// transient-pg retry/backoff. App events flush below only after it
|
|
188
|
+
// returns — strictly after the block's COMMIT.
|
|
189
189
|
const result = yield* processFinalizedBlockWithRetry(
|
|
190
190
|
value,
|
|
191
191
|
config,
|
|
@@ -202,21 +202,14 @@ export function* start(config: StartConfig): Operation<void> {
|
|
|
202
202
|
// Used to emit & log the block range for each protocol.
|
|
203
203
|
const contentBlocksForProtocol = getRangesForSyncProtocols(value);
|
|
204
204
|
|
|
205
|
-
// Fire-and-forget:
|
|
205
|
+
// Fire-and-forget: don't stall the sync loop on broker acks. Ordering is
|
|
206
|
+
// safe without a wrapper queue — Opifex assigns packet IDs synchronously in
|
|
207
|
+
// publish() and net.Socket serializes writes in call order, so MQTT 3.1.1
|
|
208
|
+
// §4.6 in-order delivery holds per (publisher, topic, QoS).
|
|
206
209
|
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
// MQTT 3.1.1 §4.6 then guarantees in-order delivery to each subscriber
|
|
211
|
-
// per (publisher, topic, QoS). We don't need a wrapper queue.
|
|
212
|
-
//
|
|
213
|
-
// TODO(scaling): the broker runs in-process (see `new EventBroker(...)`
|
|
214
|
-
// above). Fan-out to N subscribers consumes the same event loop as block
|
|
215
|
-
// processing. At ~thousands of subscribers, consider:
|
|
216
|
-
// 1) moving the broker to a worker / separate process,
|
|
217
|
-
// 2) a publisher-side circuit breaker if ctx.unresolvedPublish.size > N
|
|
218
|
-
// (drop events with a counter for observability).
|
|
219
|
-
// Today: localhost, in-process, expected O(10²) subscribers — non-issue.
|
|
210
|
+
// TODO(scaling): the broker is in-process, so fan-out shares this event
|
|
211
|
+
// loop. At ~thousands of subscribers, move it to a worker and/or add a
|
|
212
|
+
// publisher-side circuit breaker. Today: localhost, O(10²) subs — non-issue.
|
|
220
213
|
emitLatestBlocks(
|
|
221
214
|
value.blockNumber,
|
|
222
215
|
value.timestamp,
|
|
@@ -268,13 +261,9 @@ function getRangesForSyncProtocols(value: ChainBlock): Record<string, [number, n
|
|
|
268
261
|
}
|
|
269
262
|
|
|
270
263
|
/**
|
|
271
|
-
* Publish built-in block-level events.
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
* (do not stall the sync loop; ordering preserved by Opifex + MQTT 3.1.1).
|
|
275
|
-
*
|
|
276
|
-
* Errors are caught locally so they don't escape to the global
|
|
277
|
-
* `unhandledrejection` handler without context.
|
|
264
|
+
* Publish built-in block-level events. Fire-and-forget like the app-event flush
|
|
265
|
+
* above (don't stall the sync loop; ordering held by Opifex + MQTT 3.1.1).
|
|
266
|
+
* Errors are caught locally so they don't reach the global handler contextless.
|
|
278
267
|
*/
|
|
279
268
|
function emitLatestBlocks(
|
|
280
269
|
rollUpBlockHeight: number,
|
|
@@ -325,41 +314,48 @@ function* startup(
|
|
|
325
314
|
});
|
|
326
315
|
});
|
|
327
316
|
|
|
328
|
-
|
|
329
317
|
yield* acquireDBMutex(`startup-node`);
|
|
318
|
+
// `finally` releases the mutex on error/cancellation too; a throw in any step
|
|
319
|
+
// below would otherwise leak it and deadlock every later DB operation.
|
|
320
|
+
try {
|
|
321
|
+
// Dev-only reset of user-owned public tables
|
|
322
|
+
if (config.dev?.resetPublicData) {
|
|
323
|
+
yield* resetPublicTables(dbConn as any); // Client,
|
|
324
|
+
}
|
|
330
325
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
yield*
|
|
334
|
-
|
|
326
|
+
// When the node is started, we apply system migrations.
|
|
327
|
+
// Either system initial migrations, or migrations given a Paima Engine Update.
|
|
328
|
+
yield* applySystemMigrations(
|
|
329
|
+
config.appVersion,
|
|
330
|
+
versionInfo,
|
|
331
|
+
lastBlockHeight,
|
|
332
|
+
dbConn,
|
|
333
|
+
config.migrations,
|
|
334
|
+
);
|
|
335
335
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
versionInfo,
|
|
341
|
-
lastBlockHeight,
|
|
342
|
-
dbConn,
|
|
343
|
-
config.migrations,
|
|
344
|
-
);
|
|
336
|
+
// Validate that immutable config fields (e.g. NTP startTime, startBlockHeight)
|
|
337
|
+
// have not changed since the last run. Persists a snapshot on first start.
|
|
338
|
+
// Must run after system migrations so the snapshot table exists.
|
|
339
|
+
yield* validateAndSnapshotConfig(syncInfo, dbConn);
|
|
345
340
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
// Must run after system migrations so the snapshot table exists.
|
|
349
|
-
yield* validateAndSnapshotConfig(syncInfo, dbConn);
|
|
341
|
+
const syncProtocols = yield* genSyncProtocols(dbConn as any, // Client,
|
|
342
|
+
syncInfo);
|
|
350
343
|
|
|
351
|
-
|
|
352
|
-
|
|
344
|
+
const capabilities = yield* until(detectCapabilities(dbConn as any));
|
|
345
|
+
const viewStrategy = selectViewStrategy(capabilities);
|
|
353
346
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
347
|
+
yield* createDynamicTables(
|
|
348
|
+
versionInfo,
|
|
349
|
+
lastBlockHeight,
|
|
350
|
+
dbConn as any, // Client,
|
|
351
|
+
syncProtocols,
|
|
352
|
+
viewStrategy,
|
|
353
|
+
);
|
|
360
354
|
|
|
361
|
-
|
|
362
|
-
|
|
355
|
+
return syncProtocols;
|
|
356
|
+
} finally {
|
|
357
|
+
releaseDBMutex(`startup-node`);
|
|
358
|
+
}
|
|
363
359
|
}
|
|
364
360
|
|
|
365
361
|
// Convert the primitive config to the final primitive instance
|
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
* 1b — head-of-line blocking: a stalled slow chain STILL halts block production
|
|
9
9
|
* (the merge gate is correct), but the fast chain's buffer is now bounded by
|
|
10
10
|
* the cap instead of ballooning behind the head-of-line block.
|
|
11
|
+
* 1c — skip-ahead: the merge-demand exemption lifts the cap while the merge is gated
|
|
12
|
+
* on a chain whose page is far behind the (skipped-ahead) root, so it advances
|
|
13
|
+
* instead of deadlocking.
|
|
14
|
+
* 1d — density: the same exemption when a parallel chain is finer-grained than the cap.
|
|
11
15
|
*
|
|
12
16
|
* Before the fix these buffers reached ~49k–50k (see
|
|
13
17
|
* e2e/perf/results/BACKPRESSURE-BASELINE.md); these tests assert they now
|
|
@@ -177,7 +181,7 @@ test("1a: backpressure caps the buffer while the node keeps applying blocks", as
|
|
|
177
181
|
let mainPauses = 0;
|
|
178
182
|
try {
|
|
179
183
|
const startMs = Date.now();
|
|
180
|
-
const deadline = startMs +
|
|
184
|
+
const deadline = startMs + 30_000;
|
|
181
185
|
while (Date.now() < deadline) {
|
|
182
186
|
const wall = Date.now();
|
|
183
187
|
const mainBuf = bufferSize(node, MAIN_1A);
|
|
@@ -327,7 +331,8 @@ test("1b: a stalled chain still halts production, but the fast chain's buffer st
|
|
|
327
331
|
let fastPauses = 0;
|
|
328
332
|
try {
|
|
329
333
|
const startMs = Date.now();
|
|
330
|
-
|
|
334
|
+
// Generous for slow CI; exits early once the stall + capped sibling are captured.
|
|
335
|
+
const deadline = startMs + 30_000;
|
|
331
336
|
let lastHeight = -1;
|
|
332
337
|
let lastChange = Date.now();
|
|
333
338
|
while (Date.now() < deadline) {
|
|
@@ -387,3 +392,259 @@ test("1b: a stalled chain still halts production, but the fast chain's buffer st
|
|
|
387
392
|
// ...and the cap actually fired on the fast chain (observability metric populated).
|
|
388
393
|
expect(fastPauses).toBeGreaterThan(0);
|
|
389
394
|
}, 60_000);
|
|
395
|
+
|
|
396
|
+
// ── 1c — skip-ahead: merge-demand exemption keeps the cap from deadlocking ────────
|
|
397
|
+
//
|
|
398
|
+
// The root (main clock) starts far ahead of a parallel chain that starts at block 1.
|
|
399
|
+
// To produce the first root block at τ (main page SKIP_START) the merge needs the
|
|
400
|
+
// parallel page PAST τ — so the fetcher must scan ~SKIP_START blocks, more than the
|
|
401
|
+
// cap. The exemption lifts the cap while the merge is gated on this chain, so it
|
|
402
|
+
// reaches τ and production proceeds (without it the node wedges on the first block).
|
|
403
|
+
|
|
404
|
+
const MAIN_1C = "mainClock1c";
|
|
405
|
+
const PAR_1C = "parallelP1c";
|
|
406
|
+
// Main genesis page (skip-ahead); cap far below it so the parallel chain must buffer
|
|
407
|
+
// past the cap to reach τ.
|
|
408
|
+
const SKIP_START = 300;
|
|
409
|
+
const SMALL_STEP = 50;
|
|
410
|
+
const SMALL_CAP = 100;
|
|
411
|
+
// Liveness: past the first non-empty block at SKIP_START.
|
|
412
|
+
const LIVE_TARGET_1C = SKIP_START + 10;
|
|
413
|
+
// Bounded by necessity: at most the skip window + one in-flight chunk.
|
|
414
|
+
const SKIP_BUFFER_BOUND = SKIP_START + SMALL_STEP + 200;
|
|
415
|
+
|
|
416
|
+
function buildConfig1c() {
|
|
417
|
+
return new ConfigBuilder()
|
|
418
|
+
.setNamespace((b) => b.setSecurityNamespace("sync-buffering-1c"))
|
|
419
|
+
.buildNetworks((b) =>
|
|
420
|
+
b
|
|
421
|
+
.addNetwork({
|
|
422
|
+
name: "clock",
|
|
423
|
+
type: ConfigNetworkType.TEST,
|
|
424
|
+
startTime: START_TIME,
|
|
425
|
+
blockTimeMS: BLOCK_TIME_MS,
|
|
426
|
+
})
|
|
427
|
+
.addNetwork({
|
|
428
|
+
name: "chainP",
|
|
429
|
+
type: ConfigNetworkType.TEST,
|
|
430
|
+
startTime: START_TIME,
|
|
431
|
+
blockTimeMS: BLOCK_TIME_MS,
|
|
432
|
+
})
|
|
433
|
+
)
|
|
434
|
+
.buildDeployments((b) => b)
|
|
435
|
+
.buildSyncProtocols((b) =>
|
|
436
|
+
b
|
|
437
|
+
.addMain(
|
|
438
|
+
(n) => n.clock,
|
|
439
|
+
() => ({
|
|
440
|
+
name: MAIN_1C,
|
|
441
|
+
type: ConfigSyncProtocolType.TEST_MAIN,
|
|
442
|
+
// Skipped far ahead of the parallel chain (the recovery skip-ahead).
|
|
443
|
+
startBlockHeight: SKIP_START,
|
|
444
|
+
pollingInterval: 30,
|
|
445
|
+
stepSize: SMALL_STEP,
|
|
446
|
+
maxBufferedPages: SMALL_CAP,
|
|
447
|
+
}),
|
|
448
|
+
)
|
|
449
|
+
.addParallel(
|
|
450
|
+
(n) => (n as any).chainP,
|
|
451
|
+
() => ({
|
|
452
|
+
name: PAR_1C,
|
|
453
|
+
type: ConfigSyncProtocolType.TEST_PARALLEL,
|
|
454
|
+
startBlockHeight: 1,
|
|
455
|
+
pollingInterval: 30,
|
|
456
|
+
stepSize: SMALL_STEP,
|
|
457
|
+
delayMs: 0,
|
|
458
|
+
events: [],
|
|
459
|
+
maxBufferedPages: SMALL_CAP,
|
|
460
|
+
}),
|
|
461
|
+
)
|
|
462
|
+
)
|
|
463
|
+
.buildPrimitives((b) =>
|
|
464
|
+
b.addPrimitive(
|
|
465
|
+
(sp) => (sp as any)[PAR_1C],
|
|
466
|
+
() => ({ name: "noEvt1c", type: TEST_PRIMITIVE_TYPE, startBlockHeight: 1 }),
|
|
467
|
+
)
|
|
468
|
+
)
|
|
469
|
+
.build();
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
test("1c: a skipped-ahead root does NOT deadlock the merge on the first non-empty block", async () => {
|
|
473
|
+
// Main skipped to SKIP_START; parallel starts at 1 with a high tip.
|
|
474
|
+
TestChainControl.setTip(MAIN_1C, SKIP_START + 100);
|
|
475
|
+
TestChainControl.setTip(PAR_1C, 10_000);
|
|
476
|
+
|
|
477
|
+
const node = await h.runNode({ config: buildConfig1c(), apiPort: 19143 });
|
|
478
|
+
const samples: BufSample[] = [];
|
|
479
|
+
let peakPar = 0;
|
|
480
|
+
let lastApplied = 0;
|
|
481
|
+
try {
|
|
482
|
+
const startMs = Date.now();
|
|
483
|
+
// Generous for slow CI; exits early once the liveness target is reached.
|
|
484
|
+
const deadline = startMs + 30_000;
|
|
485
|
+
while (Date.now() < deadline) {
|
|
486
|
+
const wall = Date.now();
|
|
487
|
+
const mainBuf = bufferSize(node, MAIN_1C);
|
|
488
|
+
const parBuf = bufferSize(node, PAR_1C);
|
|
489
|
+
const appliedBlock = await latestFinalizedHeight(h.pool);
|
|
490
|
+
samples.push({
|
|
491
|
+
t: wall - startMs,
|
|
492
|
+
wall,
|
|
493
|
+
mainBuf,
|
|
494
|
+
evmBuf: parBuf,
|
|
495
|
+
mainOwnBlock: ownBlock(node, MAIN_1C),
|
|
496
|
+
evmOwnBlock: ownBlock(node, PAR_1C),
|
|
497
|
+
rss: process.memoryUsage().rss,
|
|
498
|
+
appliedBlock,
|
|
499
|
+
});
|
|
500
|
+
peakPar = Math.max(peakPar, parBuf);
|
|
501
|
+
lastApplied = appliedBlock ?? lastApplied;
|
|
502
|
+
// Got past the first non-empty block → the deadlock is broken; stop early.
|
|
503
|
+
if (lastApplied >= LIVE_TARGET_1C) break;
|
|
504
|
+
await sleep(50);
|
|
505
|
+
}
|
|
506
|
+
} finally {
|
|
507
|
+
writeArtifact("buffering-1c", samples, {
|
|
508
|
+
mode: "1c-skip-ahead-deadlock",
|
|
509
|
+
stepSize: SMALL_STEP,
|
|
510
|
+
skipStart: SKIP_START,
|
|
511
|
+
cap: SMALL_CAP,
|
|
512
|
+
liveTarget: LIVE_TARGET_1C,
|
|
513
|
+
peakParBuf: peakPar,
|
|
514
|
+
lastApplied,
|
|
515
|
+
});
|
|
516
|
+
await node.stop();
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Liveness: production advanced past the first non-empty block (no wedge).
|
|
520
|
+
expect(lastApplied).toBeGreaterThanOrEqual(LIVE_TARGET_1C);
|
|
521
|
+
// Bounded by necessity: only the skip window + one chunk, not the whole tip.
|
|
522
|
+
expect(peakPar).toBeLessThanOrEqual(SKIP_BUFFER_BOUND);
|
|
523
|
+
}, 60_000);
|
|
524
|
+
|
|
525
|
+
// ── 1d — density: same exemption when a chain is finer-grained than the cap ───────
|
|
526
|
+
//
|
|
527
|
+
// Same circular wait without a skip-ahead: a parallel chain far finer than the root
|
|
528
|
+
// (100 parallel blocks per root slot). Each root block needs the parallel page past
|
|
529
|
+
// the slot boundary (~PER_SLOT buffered items), but the cap is below that. The
|
|
530
|
+
// exemption lets the per-slot window buffer and drain each slot (without it the node
|
|
531
|
+
// wedges on the first block).
|
|
532
|
+
|
|
533
|
+
const MAIN_1D = "mainClock1d";
|
|
534
|
+
const PAR_1D = "parallelP1d";
|
|
535
|
+
const DENSE_BLOCK_MS = 10; // 100 parallel blocks per 1000ms root slot
|
|
536
|
+
const PER_SLOT = BLOCK_TIME_MS / DENSE_BLOCK_MS; // 100 parallel items per root slot
|
|
537
|
+
// Step + cap small enough that cap + one overshoot chunk (~40) stays below PER_SLOT
|
|
538
|
+
// (100), so the fetcher pauses before its page can cross the slot boundary.
|
|
539
|
+
const DENSE_STEP = 10;
|
|
540
|
+
const CAP_1D = 30;
|
|
541
|
+
const LIVE_TARGET_1D = 20;
|
|
542
|
+
// Bounded by the per-slot need plus one chunk — not the whole tip.
|
|
543
|
+
const DENSE_BUFFER_BOUND = PER_SLOT + DENSE_STEP + 150;
|
|
544
|
+
|
|
545
|
+
function buildConfig1d() {
|
|
546
|
+
return new ConfigBuilder()
|
|
547
|
+
.setNamespace((b) => b.setSecurityNamespace("sync-buffering-1d"))
|
|
548
|
+
.buildNetworks((b) =>
|
|
549
|
+
b
|
|
550
|
+
.addNetwork({
|
|
551
|
+
name: "clock",
|
|
552
|
+
type: ConfigNetworkType.TEST,
|
|
553
|
+
startTime: START_TIME,
|
|
554
|
+
blockTimeMS: BLOCK_TIME_MS,
|
|
555
|
+
})
|
|
556
|
+
.addNetwork({
|
|
557
|
+
name: "chainDense",
|
|
558
|
+
type: ConfigNetworkType.TEST,
|
|
559
|
+
startTime: START_TIME,
|
|
560
|
+
blockTimeMS: DENSE_BLOCK_MS,
|
|
561
|
+
})
|
|
562
|
+
)
|
|
563
|
+
.buildDeployments((b) => b)
|
|
564
|
+
.buildSyncProtocols((b) =>
|
|
565
|
+
b
|
|
566
|
+
.addMain(
|
|
567
|
+
(n) => n.clock,
|
|
568
|
+
() => ({
|
|
569
|
+
name: MAIN_1D,
|
|
570
|
+
type: ConfigSyncProtocolType.TEST_MAIN,
|
|
571
|
+
startBlockHeight: 1,
|
|
572
|
+
pollingInterval: 30,
|
|
573
|
+
stepSize: DENSE_STEP,
|
|
574
|
+
maxBufferedPages: 200,
|
|
575
|
+
}),
|
|
576
|
+
)
|
|
577
|
+
.addParallel(
|
|
578
|
+
(n) => (n as any).chainDense,
|
|
579
|
+
() => ({
|
|
580
|
+
name: PAR_1D,
|
|
581
|
+
type: ConfigSyncProtocolType.TEST_PARALLEL,
|
|
582
|
+
startBlockHeight: 1,
|
|
583
|
+
pollingInterval: 30,
|
|
584
|
+
stepSize: DENSE_STEP,
|
|
585
|
+
delayMs: 0,
|
|
586
|
+
events: [],
|
|
587
|
+
maxBufferedPages: CAP_1D,
|
|
588
|
+
}),
|
|
589
|
+
)
|
|
590
|
+
)
|
|
591
|
+
.buildPrimitives((b) =>
|
|
592
|
+
b.addPrimitive(
|
|
593
|
+
(sp) => (sp as any)[PAR_1D],
|
|
594
|
+
() => ({ name: "noEvt1d", type: TEST_PRIMITIVE_TYPE, startBlockHeight: 1 }),
|
|
595
|
+
)
|
|
596
|
+
)
|
|
597
|
+
.build();
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
test("1d: a parallel chain denser than the cap does NOT deadlock the merge", async () => {
|
|
601
|
+
TestChainControl.setTip(MAIN_1D, 60);
|
|
602
|
+
TestChainControl.setTip(PAR_1D, 10_000);
|
|
603
|
+
|
|
604
|
+
const node = await h.runNode({ config: buildConfig1d(), apiPort: 19144 });
|
|
605
|
+
const samples: BufSample[] = [];
|
|
606
|
+
let peakPar = 0;
|
|
607
|
+
let lastApplied = 0;
|
|
608
|
+
try {
|
|
609
|
+
const startMs = Date.now();
|
|
610
|
+
// Generous for slow CI; exits early once the liveness target is reached.
|
|
611
|
+
const deadline = startMs + 30_000;
|
|
612
|
+
while (Date.now() < deadline) {
|
|
613
|
+
const wall = Date.now();
|
|
614
|
+
const mainBuf = bufferSize(node, MAIN_1D);
|
|
615
|
+
const parBuf = bufferSize(node, PAR_1D);
|
|
616
|
+
const appliedBlock = await latestFinalizedHeight(h.pool);
|
|
617
|
+
samples.push({
|
|
618
|
+
t: wall - startMs,
|
|
619
|
+
wall,
|
|
620
|
+
mainBuf,
|
|
621
|
+
evmBuf: parBuf,
|
|
622
|
+
mainOwnBlock: ownBlock(node, MAIN_1D),
|
|
623
|
+
evmOwnBlock: ownBlock(node, PAR_1D),
|
|
624
|
+
rss: process.memoryUsage().rss,
|
|
625
|
+
appliedBlock,
|
|
626
|
+
});
|
|
627
|
+
peakPar = Math.max(peakPar, parBuf);
|
|
628
|
+
lastApplied = appliedBlock ?? lastApplied;
|
|
629
|
+
if (lastApplied >= LIVE_TARGET_1D) break;
|
|
630
|
+
await sleep(50);
|
|
631
|
+
}
|
|
632
|
+
} finally {
|
|
633
|
+
writeArtifact("buffering-1d", samples, {
|
|
634
|
+
mode: "1d-density-deadlock",
|
|
635
|
+
stepSize: SMALL_STEP,
|
|
636
|
+
denseBlockMs: DENSE_BLOCK_MS,
|
|
637
|
+
perSlot: PER_SLOT,
|
|
638
|
+
cap: CAP_1D,
|
|
639
|
+
liveTarget: LIVE_TARGET_1D,
|
|
640
|
+
peakParBuf: peakPar,
|
|
641
|
+
lastApplied,
|
|
642
|
+
});
|
|
643
|
+
await node.stop();
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Liveness: the node produces blocks (no wedge on block 1).
|
|
647
|
+
expect(lastApplied).toBeGreaterThanOrEqual(LIVE_TARGET_1D);
|
|
648
|
+
// Bounded by the per-slot need: ~one slot of parallel data plus one chunk.
|
|
649
|
+
expect(peakPar).toBeLessThanOrEqual(DENSE_BUFFER_BOUND);
|
|
650
|
+
}, 60_000);
|