@effectstream/runtime 0.100.26 → 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 +44 -4
- package/src/finalized-stream.ts +4 -0
- package/src/main.ts +10 -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 = {
|
|
@@ -44,14 +45,53 @@ export function* loadEmptyRunBoundaries(
|
|
|
44
45
|
const minScheduledBlockHeight = bh?.min_block_height != null && bh.min_block_height >= fromBlockNumber
|
|
45
46
|
? bh.min_block_height
|
|
46
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
|
+
|
|
47
55
|
return {
|
|
48
56
|
minScheduledBlockHeight,
|
|
49
|
-
minScheduledTimestampMs
|
|
50
|
-
? new Date(ts.min_timestamp).getTime()
|
|
51
|
-
: null,
|
|
57
|
+
minScheduledTimestampMs,
|
|
52
58
|
};
|
|
53
59
|
}
|
|
54
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
|
+
|
|
55
95
|
/** Mirrors the empty-block checks in process-blocks.ts without DB/STF work. */
|
|
56
96
|
export function isCoalescableEmptyBlock(
|
|
57
97
|
value: ChainBlock,
|
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,14 +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).
|
|
100
103
|
// Falls back to 60 s so coalescing is not silently disabled for chains that
|
|
101
104
|
// don't expose a blockTimeMS on their network config (e.g. Midnight-as-main).
|
|
102
105
|
const ntpConfig = syncInfo.find(s => s.networkType === ConfigNetworkType.NTP);
|
|
103
106
|
const clockBlockTimeMS =
|
|
104
107
|
(ntpConfig?.network as { blockTimeMS?: number } | undefined)?.blockTimeMS ??
|
|
105
108
|
(syncInfo[0]?.network as { blockTimeMS?: number } | undefined)?.blockTimeMS;
|
|
106
|
-
const lagThresholdMs =
|
|
109
|
+
const lagThresholdMs = ENV.EFFECTSTREAM_LAG_THRESHOLD_MS ??
|
|
110
|
+
(clockBlockTimeMS != null ? clockBlockTimeMS * 20 : 60_000);
|
|
107
111
|
|
|
108
112
|
// Bounded hand-off queue between the merge and the apply loop. Backpressure caps
|
|
109
113
|
// the in-memory queue so deep catch-up can't grow it toward the whole backlog
|
|
@@ -355,6 +359,9 @@ function* startup(
|
|
|
355
359
|
viewStrategy,
|
|
356
360
|
);
|
|
357
361
|
|
|
362
|
+
// Seed the merge loop's coalescing boundaries before any block is produced.
|
|
363
|
+
yield* initMergeCoalescingBoundaries(dbConn, lastBlockHeight + 1, config.migrations);
|
|
364
|
+
|
|
358
365
|
return syncProtocols;
|
|
359
366
|
} finally {
|
|
360
367
|
releaseDBMutex(`startup-node`);
|