@effectstream/runtime 0.100.25 → 0.100.27
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/README.md +12 -2
- package/package.json +12 -12
- package/src/coalesce.ts +53 -6
- package/src/finalized-stream.ts +4 -0
- package/src/main.ts +12 -3
package/README.md
CHANGED
|
@@ -84,11 +84,21 @@ default:
|
|
|
84
84
|
EFFECTSTREAM_COALESCE_EMPTY_BLOCKS=true
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
+
The lag threshold that defines "behind the chain tip" defaults to **20× the
|
|
88
|
+
main clock's block time** (falling back to 60 s when no `blockTimeMS` is
|
|
89
|
+
exposed by the network config). Override it with
|
|
90
|
+
`EFFECTSTREAM_LAG_THRESHOLD_MS` (milliseconds), e.g. for chains whose main
|
|
91
|
+
clock fires faster than the runtime can keep up with one transaction per block:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
EFFECTSTREAM_LAG_THRESHOLD_MS=30000 # engage coalescing past 30 s of lag
|
|
95
|
+
```
|
|
96
|
+
|
|
87
97
|
Behavior and guarantees:
|
|
88
98
|
|
|
89
99
|
- **Catch-up only.** Coalescing engages only while behind the chain tip
|
|
90
|
-
(`now - block.timestamp` greater than
|
|
91
|
-
|
|
100
|
+
(`now - block.timestamp` greater than the lag threshold) and disengages at
|
|
101
|
+
the tip, so steady-state stays one block per transaction.
|
|
92
102
|
- **Identical database.** A coalesced sync produces a byte-for-byte identical
|
|
93
103
|
database to a non-coalesced one. Empty blocks already never advance the block
|
|
94
104
|
hash or RNG seed, so only the endpoint's block row and the per-protocol resume
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effectstream/runtime",
|
|
3
|
-
"version": "0.100.
|
|
3
|
+
"version": "0.100.27",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"homepage": "https://effectstream.github.io/docs/",
|
|
6
6
|
"repository": {
|
|
@@ -13,17 +13,17 @@
|
|
|
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.27",
|
|
17
|
+
"@effectstream/config": "0.100.27",
|
|
18
|
+
"@effectstream/coroutine": "0.100.27",
|
|
19
|
+
"@effectstream/crypto": "0.100.27",
|
|
20
|
+
"@effectstream/db": "0.100.27",
|
|
21
|
+
"@effectstream/event-client": "0.100.27",
|
|
22
|
+
"@effectstream/event-server": "0.100.27",
|
|
23
|
+
"@effectstream/log": "0.100.27",
|
|
24
|
+
"@effectstream/sm": "0.100.27",
|
|
25
|
+
"@effectstream/sync": "0.100.27",
|
|
26
|
+
"@effectstream/utils": "0.100.27",
|
|
27
27
|
"@fastify/cors": "^11.0.1",
|
|
28
28
|
"@metamask/json-rpc-engine": "^10.0.3",
|
|
29
29
|
"pg": "8.21.0",
|
package/src/coalesce.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Empty-block coalescing during deep catch-up: fold consecutive no-op blocks into one transaction. */
|
|
2
2
|
import type { ChainBlock } from "@effectstream/sync";
|
|
3
3
|
import { call, type Operation, race, sleep, until } from "effection";
|
|
4
|
-
import type { Pool, PoolClient } from "pg";
|
|
4
|
+
import type { Client, Pool, PoolClient } from "pg";
|
|
5
5
|
import {
|
|
6
6
|
acquireDBMutex,
|
|
7
7
|
blockHeightDone,
|
|
@@ -21,6 +21,7 @@ import { ComponentNames, log, SeverityNumber } from "@effectstream/log";
|
|
|
21
21
|
import { generateEffectstreamBlockHash, Prando } from "@effectstream/crypto";
|
|
22
22
|
import type { EffectstreamBlockHash } from "@effectstream/utils";
|
|
23
23
|
import type { DBMigrations } from "@effectstream/runtime";
|
|
24
|
+
import { mergeCoalescingBoundaries } from "@effectstream/sync";
|
|
24
25
|
|
|
25
26
|
/** Scheduled-input boundaries sampled once at run start (stable until the next non-empty block). */
|
|
26
27
|
export type EmptyRunBoundaries = {
|
|
@@ -34,17 +35,63 @@ const MAX_RETRY_BACKOFF_MS = 5_000;
|
|
|
34
35
|
|
|
35
36
|
export function* loadEmptyRunBoundaries(
|
|
36
37
|
dbConn: Pool,
|
|
38
|
+
fromBlockNumber: number,
|
|
37
39
|
): Operation<EmptyRunBoundaries> {
|
|
38
40
|
const [bh] = yield* until(getEarliestScheduledBlockHeight.run(undefined, dbConn));
|
|
39
41
|
const [ts] = yield* until(getEarliestScheduledTimestamp.run(undefined, dbConn));
|
|
42
|
+
// Ignore scheduled block heights already in the past: a stale unprocessed row
|
|
43
|
+
// (e.g. from a DB reset) would otherwise permanently suppress coalescing for
|
|
44
|
+
// all current blocks, since every block number >= the stale height.
|
|
45
|
+
const minScheduledBlockHeight = bh?.min_block_height != null && bh.min_block_height >= fromBlockNumber
|
|
46
|
+
? bh.min_block_height
|
|
47
|
+
: null;
|
|
48
|
+
const minScheduledTimestampMs = ts?.min_timestamp != null
|
|
49
|
+
? new Date(ts.min_timestamp).getTime()
|
|
50
|
+
: null;
|
|
51
|
+
|
|
52
|
+
mergeCoalescingBoundaries.minScheduledBlockHeight = minScheduledBlockHeight;
|
|
53
|
+
mergeCoalescingBoundaries.minScheduledTimestampMs = minScheduledTimestampMs;
|
|
54
|
+
|
|
40
55
|
return {
|
|
41
|
-
minScheduledBlockHeight
|
|
42
|
-
minScheduledTimestampMs
|
|
43
|
-
? new Date(ts.min_timestamp).getTime()
|
|
44
|
-
: null,
|
|
56
|
+
minScheduledBlockHeight,
|
|
57
|
+
minScheduledTimestampMs,
|
|
45
58
|
};
|
|
46
59
|
}
|
|
47
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Seed {@link mergeCoalescingBoundaries} once at startup so the merge loop knows
|
|
63
|
+
* which empty blocks it may coalesce before the runtime has applied any block.
|
|
64
|
+
* `loadEmptyRunBoundaries` keeps these in sync afterwards on each non-empty block.
|
|
65
|
+
*
|
|
66
|
+
* @param fromBlockNumber the next block height to apply (lastBlockHeight + 1);
|
|
67
|
+
* scheduled heights below this are stale and ignored (see {@link loadEmptyRunBoundaries}).
|
|
68
|
+
*/
|
|
69
|
+
export function* initMergeCoalescingBoundaries(
|
|
70
|
+
dbConn: Pool | Client,
|
|
71
|
+
fromBlockNumber: number,
|
|
72
|
+
migrations: DBMigrations[] | undefined,
|
|
73
|
+
): Operation<void> {
|
|
74
|
+
const [bh] = yield* until(getEarliestScheduledBlockHeight.run(undefined, dbConn));
|
|
75
|
+
const [ts] = yield* until(getEarliestScheduledTimestamp.run(undefined, dbConn));
|
|
76
|
+
const minScheduledBlockHeight = bh?.min_block_height != null && bh.min_block_height >= fromBlockNumber
|
|
77
|
+
? bh.min_block_height
|
|
78
|
+
: null;
|
|
79
|
+
const minScheduledTimestampMs = ts?.min_timestamp != null
|
|
80
|
+
? new Date(ts.min_timestamp).getTime()
|
|
81
|
+
: null;
|
|
82
|
+
|
|
83
|
+
const migrationBlockHeights: number[] = [];
|
|
84
|
+
for (const m of migrations ?? []) {
|
|
85
|
+
if (m.blockHeight != null) {
|
|
86
|
+
migrationBlockHeights.push(m.blockHeight);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
mergeCoalescingBoundaries.minScheduledBlockHeight = minScheduledBlockHeight;
|
|
91
|
+
mergeCoalescingBoundaries.minScheduledTimestampMs = minScheduledTimestampMs;
|
|
92
|
+
mergeCoalescingBoundaries.migrationBlockHeights = migrationBlockHeights;
|
|
93
|
+
}
|
|
94
|
+
|
|
48
95
|
/** Mirrors the empty-block checks in process-blocks.ts without DB/STF work. */
|
|
49
96
|
export function isCoalescableEmptyBlock(
|
|
50
97
|
value: ChainBlock,
|
|
@@ -274,7 +321,7 @@ export function createEmptyBlockCoalescer(
|
|
|
274
321
|
Date.now() - value.timestamp > opts.lagThresholdMs;
|
|
275
322
|
if (behindTip && value.primitives.length === 0) {
|
|
276
323
|
const boundaries = pendingRun?.boundaries ??
|
|
277
|
-
(yield* loadEmptyRunBoundaries(opts.pool));
|
|
324
|
+
(yield* loadEmptyRunBoundaries(opts.pool, value.blockNumber));
|
|
278
325
|
if (isCoalescableEmptyBlock(value, boundaries, opts.migrations)) {
|
|
279
326
|
pendingRun = pendingRun
|
|
280
327
|
? { boundaries, endpoint: value, length: pendingRun.length + 1 }
|
package/src/finalized-stream.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
finalizedStreamStatus,
|
|
21
21
|
recordConsumed,
|
|
22
22
|
recordProduced,
|
|
23
|
+
recordCoalesced,
|
|
23
24
|
} from "./api/stream-status.ts";
|
|
24
25
|
import type { FinalizedBlockSubscription } from "./coalesce.ts";
|
|
25
26
|
|
|
@@ -52,6 +53,9 @@ export function* createBoundedFinalizedStream(
|
|
|
52
53
|
yield* drained.wait();
|
|
53
54
|
}
|
|
54
55
|
recordProduced();
|
|
56
|
+
if (block.coalescedCount && block.coalescedCount > 1) {
|
|
57
|
+
recordCoalesced(block.coalescedCount - 1);
|
|
58
|
+
}
|
|
55
59
|
yield* channel.send(block);
|
|
56
60
|
},
|
|
57
61
|
};
|
package/src/main.ts
CHANGED
|
@@ -33,7 +33,10 @@ import {
|
|
|
33
33
|
type PendingEvent,
|
|
34
34
|
processFinalizedBlockWithRetry,
|
|
35
35
|
} from "./process-blocks.ts";
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
createEmptyBlockCoalescer,
|
|
38
|
+
initMergeCoalescingBoundaries,
|
|
39
|
+
} from "./coalesce.ts";
|
|
37
40
|
import { startHttpServer } from "./api/http-server.ts";
|
|
38
41
|
import { recordAppliedBlock } from "./api/apply-status.ts";
|
|
39
42
|
import { recordCoalesced } from "./api/stream-status.ts";
|
|
@@ -96,12 +99,15 @@ export function* start(config: StartConfig): Operation<void> {
|
|
|
96
99
|
);
|
|
97
100
|
});
|
|
98
101
|
|
|
99
|
-
//
|
|
102
|
+
// 20× main clock block time (NTP if present, else protocol 0).
|
|
103
|
+
// Falls back to 60 s so coalescing is not silently disabled for chains that
|
|
104
|
+
// don't expose a blockTimeMS on their network config (e.g. Midnight-as-main).
|
|
100
105
|
const ntpConfig = syncInfo.find(s => s.networkType === ConfigNetworkType.NTP);
|
|
101
106
|
const clockBlockTimeMS =
|
|
102
107
|
(ntpConfig?.network as { blockTimeMS?: number } | undefined)?.blockTimeMS ??
|
|
103
108
|
(syncInfo[0]?.network as { blockTimeMS?: number } | undefined)?.blockTimeMS;
|
|
104
|
-
const lagThresholdMs =
|
|
109
|
+
const lagThresholdMs = ENV.EFFECTSTREAM_LAG_THRESHOLD_MS ??
|
|
110
|
+
(clockBlockTimeMS != null ? clockBlockTimeMS * 20 : 60_000);
|
|
105
111
|
|
|
106
112
|
// Bounded hand-off queue between the merge and the apply loop. Backpressure caps
|
|
107
113
|
// the in-memory queue so deep catch-up can't grow it toward the whole backlog
|
|
@@ -353,6 +359,9 @@ function* startup(
|
|
|
353
359
|
viewStrategy,
|
|
354
360
|
);
|
|
355
361
|
|
|
362
|
+
// Seed the merge loop's coalescing boundaries before any block is produced.
|
|
363
|
+
yield* initMergeCoalescingBoundaries(dbConn, lastBlockHeight + 1, config.migrations);
|
|
364
|
+
|
|
356
365
|
return syncProtocols;
|
|
357
366
|
} finally {
|
|
358
367
|
releaseDBMutex(`startup-node`);
|