envio 3.12.1 → 3.13.0-alpha.0

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.
Files changed (47) hide show
  1. package/package.json +6 -6
  2. package/src/BatchProcessing.res +9 -9
  3. package/src/BatchProcessing.res.mjs +5 -5
  4. package/src/Bin.res +24 -17
  5. package/src/Bin.res.mjs +5 -0
  6. package/src/ChainFetching.res +24 -10
  7. package/src/ChainFetching.res.mjs +8 -3
  8. package/src/ChainState.res +43 -0
  9. package/src/ChainState.res.mjs +43 -0
  10. package/src/ChainState.resi +2 -0
  11. package/src/Config.res +35 -0
  12. package/src/Config.res.mjs +39 -0
  13. package/src/Core.res +4 -0
  14. package/src/Core.res.mjs +4 -0
  15. package/src/CrossChainState.res +60 -3
  16. package/src/CrossChainState.res.mjs +38 -4
  17. package/src/CrossChainState.resi +10 -1
  18. package/src/Env.res +4 -0
  19. package/src/IndexerLoop.res +2 -0
  20. package/src/IndexerLoop.res.mjs +1 -0
  21. package/src/IndexerState.res +41 -1
  22. package/src/IndexerState.res.mjs +43 -4
  23. package/src/IndexerState.resi +10 -0
  24. package/src/Logging.res +38 -6
  25. package/src/Logging.res.mjs +32 -5
  26. package/src/Main.res +131 -268
  27. package/src/Main.res.mjs +28 -152
  28. package/src/Metrics.res +263 -102
  29. package/src/Metrics.res.mjs +227 -48
  30. package/src/Persistence.res +27 -2
  31. package/src/Persistence.res.mjs +9 -2
  32. package/src/PgStorage.res +17 -9
  33. package/src/PgStorage.res.mjs +11 -8
  34. package/src/Server.res +181 -0
  35. package/src/Server.res.mjs +143 -0
  36. package/src/Supervisor.res +415 -0
  37. package/src/Supervisor.res.mjs +325 -0
  38. package/src/TestIndexer.res.mjs +1 -1
  39. package/src/Worker.res +95 -0
  40. package/src/Worker.res.mjs +80 -0
  41. package/src/bindings/NodeJs.res +41 -0
  42. package/src/db/InternalTable.res +8 -1
  43. package/src/db/InternalTable.res.mjs +5 -1
  44. package/src/tui/Tui.res +24 -0
  45. package/src/tui/Tui.res.mjs +18 -0
  46. package/src/tui/components/SyncETA.res +12 -6
  47. package/src/tui/components/SyncETA.res.mjs +12 -8
@@ -17,6 +17,11 @@ type t = {
17
17
  mutable isCaughtUp: bool,
18
18
  // Indexer-wide fetch buffer pool (item count), shared across all chains.
19
19
  targetBufferSize: int,
20
+ // Set on a process driving part of a split run: the chains it drives may be
21
+ // at the head while chains in another process are still backfilling, and an
22
+ // indexer switches to realtime as a whole or not at all. Cleared by the
23
+ // supervisor once every chain in the run has arrived.
24
+ mutable holdRealtime: bool,
20
25
  }
21
26
 
22
27
  // The whole-indexer fetch buffer pool, independent of chain count.
@@ -26,16 +31,50 @@ let calculateTargetBufferSize = () =>
26
31
  | None => 100_000
27
32
  }
28
33
 
29
- let make = (~chainStates, ~isRealtime, ~targetBufferSize=calculateTargetBufferSize()): t => {
34
+ let make = (
35
+ ~chainStates,
36
+ ~isRealtime,
37
+ ~targetBufferSize=calculateTargetBufferSize(),
38
+ ~holdRealtime=false,
39
+ ): t => {
30
40
  {
31
41
  chainStates,
32
42
  chainIds: chainStates->Dict.valuesToArray->Array.map(cs => (cs->ChainState.chainConfig).id),
33
43
  isRealtime,
34
44
  isCaughtUp: isRealtime,
35
45
  targetBufferSize,
46
+ holdRealtime,
36
47
  }
37
48
  }
38
49
 
50
+ // The supervisor's go-ahead: every chain in the run has reached the head, so
51
+ // this process may make the transitions it has been holding back.
52
+ let releaseRealtime = (crossChainState: t) => crossChainState.holdRealtime = false
53
+
54
+ let isHoldingRealtime = (crossChainState: t) => crossChainState.holdRealtime
55
+
56
+ // Whether this process has got as far as it can without the run's leave. What a
57
+ // supervisor reads to decide that a split run may go realtime as one.
58
+ //
59
+ // Three ways to have arrived, because a chain can be as far along as it can get
60
+ // in three different states. Its chains have caught up; or it resumed already
61
+ // realtime; or every chain is waiting to enter the reorg threshold, which is as
62
+ // far as one can fetch while the pre-threshold lag holds it at the safe block —
63
+ // entering the threshold is what lifts that lag, so a run held until its chains
64
+ // reached the head would be holding back the transition that gets them there.
65
+ //
66
+ // The process's own conclusion rather than a reading a supervisor reassembles:
67
+ // a chain committed at what was the head and resumed once the head had moved on
68
+ // has arrived, and no live reading of it can say so — which is the same reason
69
+ // `markCaughtUpOnResume` decides before any source request.
70
+ let hasArrivedAtHead = (crossChainState: t) =>
71
+ crossChainState.isCaughtUp ||
72
+ crossChainState.isRealtime || {
73
+ let chainStates = crossChainState.chainStates->Dict.valuesToArray
74
+ chainStates->Utils.Array.notEmpty &&
75
+ chainStates->Array.every(ChainState.isReadyToEnterReorgThreshold)
76
+ }
77
+
39
78
  // Resolve a chain's state by id. The id always comes from `chainIds`, which is
40
79
  // derived from `chainStates`, so the entry is guaranteed present.
41
80
  let getChainState = (crossChainState: t, chainId) =>
@@ -122,6 +161,16 @@ let createBatch = (
122
161
 
123
162
  // Enter the reorg threshold: shrink each chain's buffer by its configured
124
163
  // blockLag and flip the flag.
164
+ // Whether every chain this process drives has buffered close enough to the head
165
+ // to enter the threshold together — and, in a split run, whether the rest of the
166
+ // run has too. Chains enter it as one indexer, so one chain still backfilling
167
+ // holds the others back whatever process it runs in.
168
+ let isReadyToEnterReorgThreshold = (crossChainState: t, ~batch) =>
169
+ !crossChainState.holdRealtime &&
170
+ crossChainState.chainStates
171
+ ->Dict.valuesToArray
172
+ ->Array.every(cs => cs->ChainState.isReadyToEnterReorgThresholdAfterBatch(~batch))
173
+
125
174
  let enterReorgThreshold = (crossChainState: t) => {
126
175
  Logging.info("Reorg threshold reached")
127
176
 
@@ -150,7 +199,10 @@ let applyBatchProgress = (crossChainState: t, ~batch: Batch.t, ~blockTimestampNa
150
199
  }
151
200
 
152
201
  crossChainState.isCaughtUp =
153
- crossChainState.isCaughtUp || (crossChainState->nextItemIsNone && everyChainCaughtUp.contents)
202
+ crossChainState.isCaughtUp ||
203
+ (!crossChainState.holdRealtime &&
204
+ crossChainState->nextItemIsNone &&
205
+ everyChainCaughtUp.contents)
154
206
  }
155
207
 
156
208
  // Every chain has buffered up to its head (or endblock) with nothing
@@ -174,8 +226,13 @@ let isSettledAtHead = (crossChainState: t) => {
174
226
  }
175
227
 
176
228
  // Enter the FinalizingIndexes phase without a batch, for the resume above.
229
+ // Not while the run holds this process back: the hold keeps the pre-threshold
230
+ // lag in place, and a chain that has fetched to a lagged head it was never
231
+ // going to get past reads as settled without having indexed anything. What a
232
+ // held process may conclude about where it stands, it concludes from what was
233
+ // persisted — see `markCaughtUpOnResume`, which the hold leaves alone.
177
234
  let markCaughtUpIfSettled = (crossChainState: t) =>
178
- if crossChainState->isSettledAtHead {
235
+ if !crossChainState.holdRealtime && crossChainState->isSettledAtHead {
179
236
  crossChainState.isCaughtUp = true
180
237
  }
181
238
 
@@ -20,19 +20,41 @@ function calculateTargetBufferSize() {
20
20
  }
21
21
  }
22
22
 
23
- function make(chainStates, isRealtime, targetBufferSizeOpt) {
23
+ function make(chainStates, isRealtime, targetBufferSizeOpt, holdRealtimeOpt) {
24
24
  let targetBufferSize = targetBufferSizeOpt !== undefined ? targetBufferSizeOpt : (
25
25
  Env.targetBufferSize !== undefined ? Env.targetBufferSize : 100000
26
26
  );
27
+ let holdRealtime = holdRealtimeOpt !== undefined ? holdRealtimeOpt : false;
27
28
  return {
28
29
  chainStates: chainStates,
29
30
  chainIds: Object.values(chainStates).map(cs => ChainState.chainConfig(cs).id),
30
31
  isRealtime: isRealtime,
31
32
  isCaughtUp: isRealtime,
32
- targetBufferSize: targetBufferSize
33
+ targetBufferSize: targetBufferSize,
34
+ holdRealtime: holdRealtime
33
35
  };
34
36
  }
35
37
 
38
+ function releaseRealtime(crossChainState) {
39
+ crossChainState.holdRealtime = false;
40
+ }
41
+
42
+ function isHoldingRealtime(crossChainState) {
43
+ return crossChainState.holdRealtime;
44
+ }
45
+
46
+ function hasArrivedAtHead(crossChainState) {
47
+ if (crossChainState.isCaughtUp || crossChainState.isRealtime) {
48
+ return true;
49
+ }
50
+ let chainStates = Object.values(crossChainState.chainStates);
51
+ if (Utils.$$Array.notEmpty(chainStates)) {
52
+ return chainStates.every(ChainState.isReadyToEnterReorgThreshold);
53
+ } else {
54
+ return false;
55
+ }
56
+ }
57
+
36
58
  function chainStates(crossChainState) {
37
59
  return crossChainState.chainStates;
38
60
  }
@@ -94,6 +116,14 @@ function createBatch(crossChainState, config, frontier, batchSizeTarget) {
94
116
  return Batch.make(config.checkpointSequence, frontier, Utils.Dict.mapValues(crossChainState.chainStates, none => ChainState.toChainBeforeBatch(none, crossChainState.isRealtime)), batchSizeTarget, HistoryPolicy.decide(config, shouldSaveHistory(crossChainState)));
95
117
  }
96
118
 
119
+ function isReadyToEnterReorgThreshold(crossChainState, batch) {
120
+ if (crossChainState.holdRealtime) {
121
+ return false;
122
+ } else {
123
+ return Object.values(crossChainState.chainStates).every(cs => ChainState.isReadyToEnterReorgThresholdAfterBatch(cs, batch));
124
+ }
125
+ }
126
+
97
127
  function enterReorgThreshold(crossChainState) {
98
128
  Logging.info("Reorg threshold reached");
99
129
  for (let i = 0, i_finish = crossChainState.chainIds.length; i < i_finish; ++i) {
@@ -111,7 +141,7 @@ function applyBatchProgress(crossChainState, batch, blockTimestampName) {
111
141
  everyChainCaughtUp = false;
112
142
  }
113
143
  }
114
- crossChainState.isCaughtUp = crossChainState.isCaughtUp || nextItemIsNone(crossChainState) && everyChainCaughtUp;
144
+ crossChainState.isCaughtUp = crossChainState.isCaughtUp || !crossChainState.holdRealtime && nextItemIsNone(crossChainState) && everyChainCaughtUp;
115
145
  }
116
146
 
117
147
  function isSettledAtHead(crossChainState) {
@@ -125,7 +155,7 @@ function isSettledAtHead(crossChainState) {
125
155
  }
126
156
 
127
157
  function markCaughtUpIfSettled(crossChainState) {
128
- if (isSettledAtHead(crossChainState)) {
158
+ if (!crossChainState.holdRealtime && isSettledAtHead(crossChainState)) {
129
159
  crossChainState.isCaughtUp = true;
130
160
  return;
131
161
  }
@@ -256,6 +286,10 @@ export {
256
286
  shouldSaveHistory,
257
287
  nextItemIsNone,
258
288
  getSafeCheckpointIdByChain,
289
+ releaseRealtime,
290
+ isHoldingRealtime,
291
+ hasArrivedAtHead,
292
+ isReadyToEnterReorgThreshold,
259
293
  createBatch,
260
294
  enterReorgThreshold,
261
295
  applyBatchProgress,
@@ -5,7 +5,12 @@ type t
5
5
 
6
6
  let calculateTargetBufferSize: unit => int
7
7
 
8
- let make: (~chainStates: dict<ChainState.t>, ~isRealtime: bool, ~targetBufferSize: int=?) => t
8
+ let make: (
9
+ ~chainStates: dict<ChainState.t>,
10
+ ~isRealtime: bool,
11
+ ~targetBufferSize: int=?,
12
+ ~holdRealtime: bool=?,
13
+ ) => t
9
14
 
10
15
  // Accessors.
11
16
  let chainStates: t => dict<ChainState.t>
@@ -25,6 +30,10 @@ let getSafeCheckpointIdByChain: (
25
30
  ) => array<(ChainId.t, option<Internal.checkpointId>)>
26
31
 
27
32
  // Cross-chain transitions.
33
+ let releaseRealtime: t => unit
34
+ let isHoldingRealtime: t => bool
35
+ let hasArrivedAtHead: t => bool
36
+ let isReadyToEnterReorgThreshold: (t, ~batch: Batch.t) => bool
28
37
  let createBatch: (t, ~config: Config.t, ~frontier: Frontier.t, ~batchSizeTarget: int) => Batch.t
29
38
  let enterReorgThreshold: t => unit
30
39
  let applyBatchProgress: (t, ~batch: Batch.t, ~blockTimestampName: string) => unit
package/src/Env.res CHANGED
@@ -127,6 +127,10 @@ module Db = {
127
127
  //the SSL modes should be provided as string otherwise as 'require' | 'allow' | 'prefer' | 'verify-full'
128
128
  ~devFallback=Bool(false),
129
129
  )
130
+ // The budget for the whole run, not for one process: a run that splits across
131
+ // workers divides it among them, and each caps its own pool to its share.
132
+ // The default buys a single worker, so a run splits only once the operator
133
+ // raises the budget it may spend.
130
134
  let maxConnections = envSafe->EnvSafe.get("ENVIO_PG_MAX_CONNECTIONS", S.int, ~fallback=2)
131
135
  }
132
136
 
@@ -45,6 +45,8 @@ let start = (state: IndexerState.t) => {
45
45
  launch(state, () => FinalizeBackfill.repairSchemaIndexes(state))
46
46
  }
47
47
 
48
+ state->IndexerState.bindScheduleProcessing(scheduleProcessing)
49
+
48
50
  scheduleFetch()
49
51
  scheduleProcessing()
50
52
  }
@@ -42,6 +42,7 @@ function start(state) {
42
42
  work();
43
43
  }
44
44
  }
45
+ IndexerState.bindScheduleProcessing(state, scheduleProcessing);
45
46
  scheduleFetch();
46
47
  scheduleProcessing();
47
48
  }
@@ -115,6 +115,10 @@ type t = {
115
115
  // waitForNewBlock waiter is bound to the old, pre-realtime source). A fetch
116
116
  // response or waiter carrying an older epoch than this is discarded.
117
117
  mutable epoch: int,
118
+ // The loop's one door in from outside it: IndexerLoop owns scheduling and
119
+ // wires this when it starts, so an event the loop can't see for itself can
120
+ // still make it re-evaluate. A no-op before then.
121
+ mutable scheduleProcessing: unit => unit,
118
122
  // None off the simulate path.
119
123
  simulateDeadInputTracker: option<SimulateDeadInputTracker.t>,
120
124
  // --- Metric counters, rendered by Metrics at scrape time. ---
@@ -141,6 +145,7 @@ let make = (
141
145
  ~chainStates: dict<ChainState.t>,
142
146
  ~isRealtime: bool,
143
147
  ~targetBufferSize=CrossChainState.calculateTargetBufferSize(),
148
+ ~holdRealtime=false,
144
149
  ~committedFrontier=Frontier.empty(),
145
150
  ~isDevelopmentMode=false,
146
151
  ~shouldUseTui=false,
@@ -180,11 +185,17 @@ let make = (
180
185
  chainMetaDirty: false,
181
186
  chainMetaThrottler,
182
187
  isProcessing: false,
183
- crossChainState: CrossChainState.make(~chainStates, ~isRealtime, ~targetBufferSize),
188
+ crossChainState: CrossChainState.make(
189
+ ~chainStates,
190
+ ~isRealtime,
191
+ ~targetBufferSize,
192
+ ~holdRealtime,
193
+ ),
184
194
  indexerStartTime: Date.make(),
185
195
  indexerStartTimeRef: Performance.now(),
186
196
  rollbackState: NoRollback,
187
197
  lastPrunedAtMillis: Dict.make(),
198
+ scheduleProcessing: () => (),
188
199
  loadManager: LoadManager.make(),
189
200
  keepProcessAlive: isDevelopmentMode || shouldUseTui,
190
201
  exitAfterFirstEventBlock,
@@ -227,6 +238,9 @@ let makeFromDbState = (
227
238
  ~exitAfterFirstEventBlock=false,
228
239
  ~reducedPollingInterval=?,
229
240
  ~targetBufferSize=CrossChainState.calculateTargetBufferSize(),
241
+ // A process driving part of a split run waits for its supervisor before
242
+ // entering the reorg threshold or switching to realtime.
243
+ ~holdRealtime=false,
230
244
  ~onError,
231
245
  ~onExit=?,
232
246
  ) => {
@@ -274,6 +288,7 @@ let makeFromDbState = (
274
288
  ~chainStates,
275
289
  ~isRealtime,
276
290
  ~targetBufferSize,
291
+ ~holdRealtime,
277
292
  ~committedFrontier=initialState.checkpointFrontier,
278
293
  ~isDevelopmentMode,
279
294
  ~shouldUseTui,
@@ -500,9 +515,33 @@ let isFinalizingIndexes = (state: t) =>
500
515
  state.crossChainState->CrossChainState.isCaughtUp &&
501
516
  !(state.crossChainState->CrossChainState.isRealtime)
502
517
 
518
+ // The FinalizingIndexes phase is the transition a held process waits on: it
519
+ // ends with `ready_at` committed and the indexer realtime.
520
+ let shouldFinalizeIndexes = (state: t) =>
521
+ state->isFinalizingIndexes && !(state.crossChainState->CrossChainState.isHoldingRealtime)
522
+
503
523
  let markCaughtUpIfSettled = (state: t) =>
504
524
  state.crossChainState->CrossChainState.markCaughtUpIfSettled
505
525
 
526
+ let isReadyToEnterReorgThreshold = (state: t, ~batch) =>
527
+ state.crossChainState->CrossChainState.isReadyToEnterReorgThreshold(~batch)
528
+
529
+ let bindScheduleProcessing = (state: t, scheduleProcessing) =>
530
+ state.scheduleProcessing = scheduleProcessing
531
+
532
+ // A process still waiting on its supervisor owes the schema the indexes its
533
+ // chains deferred, so reaching every end block doesn't make it done.
534
+ let isHoldingRealtime = (state: t) => state.crossChainState->CrossChainState.isHoldingRealtime
535
+
536
+ let hasArrivedAtHead = (state: t) => state.crossChainState->CrossChainState.hasArrivedAtHead
537
+
538
+ let releaseRealtime = (state: t) => {
539
+ state.crossChainState->CrossChainState.releaseRealtime
540
+ // Every chain is parked at the head with no batch coming, so nothing would
541
+ // notice the hold is gone without a pass through processing.
542
+ state.scheduleProcessing()
543
+ }
544
+
506
545
  let markReady = (state: t, ~readyAt) => state.crossChainState->CrossChainState.markReady(~readyAt)
507
546
 
508
547
  let rollbackState = (state: t) => state.rollbackState
@@ -572,6 +611,7 @@ let toMetrics = (state: t): Metrics.t => {
572
611
  elapsedSeconds: state.indexerStartTimeRef->Performance.secondsSince,
573
612
  targetBufferSize: state.crossChainState->CrossChainState.targetBufferSize,
574
613
  isInReorgThreshold: state.crossChainState->CrossChainState.isInReorgThreshold,
614
+ hasArrivedAtHead: state.crossChainState->CrossChainState.hasArrivedAtHead,
575
615
  rollbackEnabled: state.config.shouldRollbackOnReorg,
576
616
  maxBatchSize: state.config.batchSize,
577
617
  preloadSeconds: state.preloadSeconds,
@@ -24,8 +24,9 @@ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js
24
24
  import * as CheckpointSequence from "./db/CheckpointSequence.res.mjs";
25
25
  import * as SimulateDeadInputTracker from "./SimulateDeadInputTracker.res.mjs";
26
26
 
27
- function make(config, persistence, chainStates, isRealtime, targetBufferSizeOpt, committedFrontierOpt, isDevelopmentModeOpt, shouldUseTuiOpt, exitAfterFirstEventBlockOpt, onError, onExit) {
27
+ function make(config, persistence, chainStates, isRealtime, targetBufferSizeOpt, holdRealtimeOpt, committedFrontierOpt, isDevelopmentModeOpt, shouldUseTuiOpt, exitAfterFirstEventBlockOpt, onError, onExit) {
28
28
  let targetBufferSize = targetBufferSizeOpt !== undefined ? targetBufferSizeOpt : CrossChainState.calculateTargetBufferSize();
29
+ let holdRealtime = holdRealtimeOpt !== undefined ? holdRealtimeOpt : false;
29
30
  let committedFrontier = committedFrontierOpt !== undefined ? committedFrontierOpt : Frontier.empty();
30
31
  let isDevelopmentMode = isDevelopmentModeOpt !== undefined ? isDevelopmentModeOpt : false;
31
32
  let shouldUseTui = shouldUseTuiOpt !== undefined ? shouldUseTuiOpt : false;
@@ -54,7 +55,7 @@ function make(config, persistence, chainStates, isRealtime, targetBufferSizeOpt,
54
55
  chainMetaDirty: false,
55
56
  chainMetaThrottler: chainMetaThrottler,
56
57
  isProcessing: false,
57
- crossChainState: CrossChainState.make(chainStates, isRealtime, targetBufferSize),
58
+ crossChainState: CrossChainState.make(chainStates, isRealtime, targetBufferSize, holdRealtime),
58
59
  rollbackState: "NoRollback",
59
60
  indexerStartTime: new Date(),
60
61
  indexerStartTimeRef: Performance.now(),
@@ -66,6 +67,7 @@ function make(config, persistence, chainStates, isRealtime, targetBufferSizeOpt,
66
67
  onExit: onExit,
67
68
  isStopped: false,
68
69
  epoch: 0,
70
+ scheduleProcessing: () => {},
69
71
  simulateDeadInputTracker: SimulateDeadInputTracker.makeFromConfig(config),
70
72
  preloadSeconds: 0,
71
73
  processingSeconds: 0,
@@ -82,11 +84,12 @@ function make(config, persistence, chainStates, isRealtime, targetBufferSizeOpt,
82
84
  };
83
85
  }
84
86
 
85
- function makeFromDbState(config, persistence, initialState, registrationsByChainId, isDevelopmentModeOpt, shouldUseTuiOpt, exitAfterFirstEventBlockOpt, reducedPollingInterval, targetBufferSizeOpt, onError, onExit) {
87
+ function makeFromDbState(config, persistence, initialState, registrationsByChainId, isDevelopmentModeOpt, shouldUseTuiOpt, exitAfterFirstEventBlockOpt, reducedPollingInterval, targetBufferSizeOpt, holdRealtimeOpt, onError, onExit) {
86
88
  let isDevelopmentMode = isDevelopmentModeOpt !== undefined ? isDevelopmentModeOpt : false;
87
89
  let shouldUseTui = shouldUseTuiOpt !== undefined ? shouldUseTuiOpt : false;
88
90
  let exitAfterFirstEventBlock = exitAfterFirstEventBlockOpt !== undefined ? exitAfterFirstEventBlockOpt : false;
89
91
  let targetBufferSize = targetBufferSizeOpt !== undefined ? targetBufferSizeOpt : CrossChainState.calculateTargetBufferSize();
92
+ let holdRealtime = holdRealtimeOpt !== undefined ? holdRealtimeOpt : false;
90
93
  let isInReorgThreshold = initialState.cleanRun ? false : initialState.chains.some(resumedChainState => {
91
94
  let progressBlockNumber = resumedChainState.progressBlockNumber;
92
95
  let sourceBlockNumber = resumedChainState.sourceBlockNumber;
@@ -104,7 +107,7 @@ function makeFromDbState(config, persistence, initialState, registrationsByChain
104
107
  let chainConfig = ChainMap.get(config.chainMap, chainId);
105
108
  chainStates[resumedChainState.id] = ChainState.makeFromDbState(chainConfig, resumedChainState, initialState.reorgCheckpoints, isInReorgThreshold, isRealtime, config, initialState.contractMapping, registrationsByChainId, reducedPollingInterval);
106
109
  });
107
- let state = make(config, persistence, chainStates, isRealtime, targetBufferSize, initialState.checkpointFrontier, isDevelopmentMode, shouldUseTui, exitAfterFirstEventBlock, onError, onExit);
110
+ let state = make(config, persistence, chainStates, isRealtime, targetBufferSize, holdRealtime, initialState.checkpointFrontier, isDevelopmentMode, shouldUseTui, exitAfterFirstEventBlock, onError, onExit);
108
111
  CrossChainState.markCaughtUpOnResume(state.crossChainState);
109
112
  Utils.Dict.forEach(initialState.cache, param => {
110
113
  let count = param.count;
@@ -350,10 +353,39 @@ function isFinalizingIndexes(state) {
350
353
  }
351
354
  }
352
355
 
356
+ function shouldFinalizeIndexes(state) {
357
+ if (isFinalizingIndexes(state)) {
358
+ return !CrossChainState.isHoldingRealtime(state.crossChainState);
359
+ } else {
360
+ return false;
361
+ }
362
+ }
363
+
353
364
  function markCaughtUpIfSettled(state) {
354
365
  CrossChainState.markCaughtUpIfSettled(state.crossChainState);
355
366
  }
356
367
 
368
+ function isReadyToEnterReorgThreshold(state, batch) {
369
+ return CrossChainState.isReadyToEnterReorgThreshold(state.crossChainState, batch);
370
+ }
371
+
372
+ function bindScheduleProcessing(state, scheduleProcessing) {
373
+ state.scheduleProcessing = scheduleProcessing;
374
+ }
375
+
376
+ function isHoldingRealtime(state) {
377
+ return CrossChainState.isHoldingRealtime(state.crossChainState);
378
+ }
379
+
380
+ function hasArrivedAtHead(state) {
381
+ return CrossChainState.hasArrivedAtHead(state.crossChainState);
382
+ }
383
+
384
+ function releaseRealtime(state) {
385
+ CrossChainState.releaseRealtime(state.crossChainState);
386
+ state.scheduleProcessing();
387
+ }
388
+
357
389
  function markReady(state, readyAt) {
358
390
  CrossChainState.markReady(state.crossChainState, readyAt);
359
391
  }
@@ -447,6 +479,7 @@ function toMetrics(state) {
447
479
  elapsedSeconds: Performance.secondsSince(state.indexerStartTimeRef),
448
480
  targetBufferSize: CrossChainState.targetBufferSize(state.crossChainState),
449
481
  isInReorgThreshold: CrossChainState.isInReorgThreshold(state.crossChainState),
482
+ hasArrivedAtHead: CrossChainState.hasArrivedAtHead(state.crossChainState),
450
483
  rollbackEnabled: state.config.shouldRollbackOnReorg,
451
484
  maxBatchSize: state.config.batchSize,
452
485
  preloadSeconds: state.preloadSeconds,
@@ -844,6 +877,12 @@ export {
844
877
  isRealtime,
845
878
  isFinalizingIndexes,
846
879
  markCaughtUpIfSettled,
880
+ isReadyToEnterReorgThreshold,
881
+ shouldFinalizeIndexes,
882
+ bindScheduleProcessing,
883
+ isHoldingRealtime,
884
+ hasArrivedAtHead,
885
+ releaseRealtime,
847
886
  markReady,
848
887
  rollbackState,
849
888
  indexerStartTime,
@@ -16,6 +16,7 @@ let make: (
16
16
  ~chainStates: dict<ChainState.t>,
17
17
  ~isRealtime: bool,
18
18
  ~targetBufferSize: int=?,
19
+ ~holdRealtime: bool=?,
19
20
  ~committedFrontier: Frontier.t=?,
20
21
  ~isDevelopmentMode: bool=?,
21
22
  ~shouldUseTui: bool=?,
@@ -34,6 +35,7 @@ let makeFromDbState: (
34
35
  ~exitAfterFirstEventBlock: bool=?,
35
36
  ~reducedPollingInterval: int=?,
36
37
  ~targetBufferSize: int=?,
38
+ ~holdRealtime: bool=?,
37
39
  ~onError: ErrorHandling.t => unit,
38
40
  ~onExit: unit => unit=?,
39
41
  ) => t
@@ -93,6 +95,14 @@ let shouldSaveHistory: t => dict<bool>
93
95
  let isRealtime: t => bool
94
96
  let isFinalizingIndexes: t => bool
95
97
  let markCaughtUpIfSettled: t => unit
98
+ let isReadyToEnterReorgThreshold: (t, ~batch: Batch.t) => bool
99
+ let shouldFinalizeIndexes: t => bool
100
+ // Wires the loop's way back in. IndexerLoop calls this as it starts.
101
+ let bindScheduleProcessing: (t, unit => unit) => unit
102
+ let isHoldingRealtime: t => bool
103
+ let hasArrivedAtHead: t => bool
104
+ // The supervisor's go-ahead for a process driving part of a split run.
105
+ let releaseRealtime: t => unit
96
106
  let markReady: (t, ~readyAt: Date.t) => unit
97
107
  let rollbackState: t => rollbackState
98
108
  let indexerStartTime: t => Date.t
package/src/Logging.res CHANGED
@@ -26,6 +26,30 @@ let logLevels = [
26
26
 
27
27
  %%private(let logger = ref(None))
28
28
 
29
+ // Fields every line this process logs carries. Merged into each line rather
30
+ // than bound to a child logger: pino writes a child's bindings and the line's
31
+ // own fields side by side, so a line that names the same key would carry it
32
+ // twice. A fresh object per line, since pino merges the line's fields into
33
+ // whatever this returns.
34
+ %%private(let context: ref<dict<JSON.t>> = ref(Dict.make()))
35
+ %%private(let mixin = () => JSON.Object(context.contents->Dict.copy))
36
+
37
+ // A child logger that binds a field the process already carries would have pino
38
+ // write it twice: a child's bindings and the context are concatenated into the
39
+ // line, not merged. The context is the one place a line names it, which it can
40
+ // be because a process only takes one when its every line is about that chain.
41
+ %%private(
42
+ let withoutContext = (params: 'a) =>
43
+ switch context.contents->Dict.keysToArray {
44
+ | [] => params
45
+ | keys => {
46
+ let narrowed = params->(Utils.magic: 'a => dict<JSON.t>)->Dict.copy
47
+ keys->Array.forEach(key => narrowed->Dict.delete(key))
48
+ narrowed->(Utils.magic: dict<JSON.t> => 'a)
49
+ }
50
+ }
51
+ )
52
+
29
53
  let makeLogger = (~logStrategy, ~logFilePath, ~defaultFileLogLevel, ~userLogLevel) => {
30
54
  // Currently unused - useful if using multiple transports.
31
55
  // let pinoRaw = {"target": "pino/file", "level": Config.userLogLevel}
@@ -56,17 +80,19 @@ let makeLogger = (~logStrategy, ~logFilePath, ~defaultFileLogLevel, ~userLogLeve
56
80
  ...Pino.ECS.make(),
57
81
  customLevels: logLevels,
58
82
  base,
83
+ mixin,
59
84
  },
60
85
  Transport.make(pinoFile),
61
86
  )
62
87
  | EcsConsoleMultistream =>
63
- makeMultiStreamLogger(~logFile=None, ~options=Some({...Pino.ECS.make(), base}))
88
+ makeMultiStreamLogger(~logFile=None, ~options=Some({...Pino.ECS.make(), base, mixin}))
64
89
  | EcsConsole =>
65
90
  make({
66
91
  ...Pino.ECS.make(),
67
92
  level: userLogLevel,
68
93
  customLevels: logLevels,
69
94
  base,
95
+ mixin,
70
96
  })
71
97
  | FileOnly =>
72
98
  makeWithOptionsAndTransport(
@@ -74,12 +100,13 @@ let makeLogger = (~logStrategy, ~logFilePath, ~defaultFileLogLevel, ~userLogLeve
74
100
  customLevels: logLevels,
75
101
  level: defaultFileLogLevel,
76
102
  base,
103
+ mixin,
77
104
  },
78
105
  Transport.make(pinoFile),
79
106
  )
80
- | ConsoleRaw => makeMultiStreamLogger(~logFile=None, ~options=Some({base: base}))
81
- | ConsolePretty => makeMultiStreamLogger(~logFile=None, ~options=Some({base: base}))
82
- | Both => makeMultiStreamLogger(~logFile=Some(logFilePath), ~options=Some({base: base}))
107
+ | ConsoleRaw => makeMultiStreamLogger(~logFile=None, ~options=Some({base, mixin}))
108
+ | ConsolePretty => makeMultiStreamLogger(~logFile=None, ~options=Some({base, mixin}))
109
+ | Both => makeMultiStreamLogger(~logFile=Some(logFilePath), ~options=Some({base, mixin}))
83
110
  }
84
111
  }
85
112
 
@@ -149,10 +176,15 @@ let childFatal = (logger, params: 'a) => {
149
176
  }
150
177
 
151
178
  let createChild = (~params: 'a) => {
152
- getLogger()->child(params->createChildParams)
179
+ getLogger()->child(params->withoutContext->createChildParams)
153
180
  }
181
+
182
+ // What belongs on every line is the run's to decide; the logger only carries
183
+ // what it is handed. A line that names one of these fields itself wins.
184
+ let setContext = (fields: dict<JSON.t>) => context := fields
185
+
154
186
  let createChildFrom = (~logger: t, ~params: 'a) => {
155
- logger->child(params->createChildParams)
187
+ logger->child(params->withoutContext->createChildParams)
156
188
  }
157
189
 
158
190
  @inline
@@ -3,6 +3,7 @@
3
3
  import * as Pino from "./bindings/Pino.res.mjs";
4
4
  import * as Pino$1 from "pino";
5
5
  import * as Utils from "./Utils.res.mjs";
6
+ import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
6
7
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
7
8
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
8
9
 
@@ -53,6 +54,24 @@ let logger = {
53
54
  contents: undefined
54
55
  };
55
56
 
57
+ let context = {
58
+ contents: {}
59
+ };
60
+
61
+ function mixin() {
62
+ return Object.assign({}, context.contents);
63
+ }
64
+
65
+ function withoutContext(params) {
66
+ let keys = Object.keys(context.contents);
67
+ if (keys.length === 0) {
68
+ return params;
69
+ }
70
+ let narrowed = Object.assign({}, params);
71
+ keys.forEach(key => Stdlib_Dict.$$delete(narrowed, key));
72
+ return narrowed;
73
+ }
74
+
56
75
  function makeLogger(logStrategy, logFilePath, defaultFileLogLevel, userLogLevel) {
57
76
  let pinoFile_target = "pino/file";
58
77
  let pinoFile_options = Primitive_option.some(Pino.Transport.makeTransportOptions({
@@ -71,17 +90,18 @@ function makeLogger(logStrategy, logFilePath, defaultFileLogLevel, userLogLevel)
71
90
  switch (logStrategy) {
72
91
  case "ecs-file" :
73
92
  let newrecord = {...Pino.ECS.make()};
74
- return Pino$1.pino((newrecord.base = base, newrecord.customLevels = logLevels, newrecord), Pino$1.transport(pinoFile));
93
+ return Pino$1.pino((newrecord.base = base, newrecord.mixin = mixin, newrecord.customLevels = logLevels, newrecord), Pino$1.transport(pinoFile));
75
94
  case "ecs-console" :
76
95
  let newrecord$1 = {...Pino.ECS.make()};
77
- return Pino$1.pino((newrecord$1.base = base, newrecord$1.customLevels = logLevels, newrecord$1.level = userLogLevel, newrecord$1));
96
+ return Pino$1.pino((newrecord$1.base = base, newrecord$1.mixin = mixin, newrecord$1.customLevels = logLevels, newrecord$1.level = userLogLevel, newrecord$1));
78
97
  case "ecs-console-multistream" :
79
98
  let newrecord$2 = {...Pino.ECS.make()};
80
- return makeMultiStreamLogger(undefined, (newrecord$2.base = base, newrecord$2));
99
+ return makeMultiStreamLogger(undefined, (newrecord$2.base = base, newrecord$2.mixin = mixin, newrecord$2));
81
100
  case "file-only" :
82
101
  return Pino$1.pino({
83
102
  level: defaultFileLogLevel,
84
103
  customLevels: logLevels,
104
+ mixin: mixin,
85
105
  base: base
86
106
  }, Pino$1.transport(pinoFile));
87
107
  case "console-raw" :
@@ -89,10 +109,12 @@ function makeLogger(logStrategy, logFilePath, defaultFileLogLevel, userLogLevel)
89
109
  break;
90
110
  case "both-prettyconsole" :
91
111
  return makeMultiStreamLogger(logFilePath, {
112
+ mixin: mixin,
92
113
  base: base
93
114
  });
94
115
  }
95
116
  return makeMultiStreamLogger(undefined, {
117
+ mixin: mixin,
96
118
  base: base
97
119
  });
98
120
  }
@@ -171,11 +193,15 @@ function childFatal(logger, params) {
171
193
  }
172
194
 
173
195
  function createChild(params) {
174
- return getLogger().child(Pino.createChildParams(params));
196
+ return getLogger().child(Pino.createChildParams(withoutContext(params)));
197
+ }
198
+
199
+ function setContext(fields) {
200
+ context.contents = fields;
175
201
  }
176
202
 
177
203
  function createChildFrom(logger, params) {
178
- return logger.child(Pino.createChildParams(params));
204
+ return logger.child(Pino.createChildParams(withoutContext(params)));
179
205
  }
180
206
 
181
207
  function logAtLevel(logger, level, message, params) {
@@ -246,6 +272,7 @@ export {
246
272
  childErrorWithExn,
247
273
  childFatal,
248
274
  createChild,
275
+ setContext,
249
276
  createChildFrom,
250
277
  logAtLevel,
251
278
  noopLogger,