envio 3.3.0-alpha.0 → 3.3.0-alpha.10
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/evm.schema.json +10 -0
- package/index.d.ts +45 -25
- package/package.json +7 -7
- package/src/Address.res +5 -2
- package/src/Address.res.mjs +3 -1
- package/src/Api.res +1 -1
- package/src/Api.res.mjs +1 -1
- package/src/Batch.res +0 -6
- package/src/Batch.res.mjs +1 -12
- package/src/BatchProcessing.res +7 -12
- package/src/BatchProcessing.res.mjs +8 -7
- package/src/Bin.res +3 -0
- package/src/Bin.res.mjs +4 -0
- package/src/ChainFetching.res +48 -16
- package/src/ChainFetching.res.mjs +36 -17
- package/src/ChainMetadata.res +1 -11
- package/src/ChainMetadata.res.mjs +1 -10
- package/src/ChainState.res +628 -149
- package/src/ChainState.res.mjs +440 -102
- package/src/ChainState.resi +74 -4
- package/src/Config.res +61 -33
- package/src/Config.res.mjs +50 -10
- package/src/ContractRegisterContext.res +2 -12
- package/src/ContractRegisterContext.res.mjs +3 -5
- package/src/Core.res +14 -3
- package/src/CrossChainState.res +142 -93
- package/src/CrossChainState.res.mjs +88 -60
- package/src/CrossChainState.resi +2 -10
- package/src/Ecosystem.res +5 -6
- package/src/Ecosystem.res.mjs +3 -3
- package/src/Env.res +0 -7
- package/src/Env.res.mjs +0 -6
- package/src/Envio.res +17 -11
- package/src/EnvioGlobal.res +53 -0
- package/src/EnvioGlobal.res.mjs +31 -0
- package/src/EventConfigBuilder.res +177 -88
- package/src/EventConfigBuilder.res.mjs +99 -42
- package/src/EventProcessing.res +74 -28
- package/src/EventProcessing.res.mjs +55 -29
- package/src/ExitOnCaughtUp.res +10 -2
- package/src/ExitOnCaughtUp.res.mjs +10 -1
- package/src/FetchState.res +558 -300
- package/src/FetchState.res.mjs +437 -423
- package/src/HandlerLoader.res +8 -104
- package/src/HandlerLoader.res.mjs +2 -81
- package/src/HandlerRegister.res +518 -144
- package/src/HandlerRegister.res.mjs +285 -133
- package/src/HandlerRegister.resi +24 -8
- package/src/IndexerLoop.res +2 -3
- package/src/IndexerLoop.res.mjs +1 -1
- package/src/IndexerState.res +10 -11
- package/src/IndexerState.res.mjs +14 -11
- package/src/IndexerState.resi +2 -5
- package/src/IndexingAddresses.res +108 -0
- package/src/IndexingAddresses.res.mjs +101 -0
- package/src/IndexingAddresses.resi +34 -0
- package/src/Internal.res +155 -46
- package/src/Internal.res.mjs +30 -2
- package/src/LoadLayer.res +5 -5
- package/src/LoadLayer.res.mjs +6 -6
- package/src/LogSelection.res +102 -165
- package/src/LogSelection.res.mjs +101 -116
- package/src/Main.res +71 -236
- package/src/Main.res.mjs +60 -145
- package/src/Metrics.res +74 -0
- package/src/Metrics.res.mjs +75 -0
- package/src/PgStorage.res +4 -4
- package/src/PgStorage.res.mjs +5 -5
- package/src/Prometheus.res +10 -74
- package/src/Prometheus.res.mjs +124 -186
- package/src/PruneStaleHistory.res +2 -2
- package/src/PruneStaleHistory.res.mjs +3 -3
- package/src/RawEvent.res +7 -2
- package/src/RawEvent.res.mjs +4 -4
- package/src/Rollback.res +3 -3
- package/src/Rollback.res.mjs +4 -4
- package/src/RollbackCommit.res +4 -1
- package/src/RollbackCommit.res.mjs +3 -2
- package/src/SimulateDeadInputTracker.res +85 -0
- package/src/SimulateDeadInputTracker.res.mjs +73 -0
- package/src/SimulateDeadInputTracker.resi +12 -0
- package/src/SimulateItems.res +91 -23
- package/src/SimulateItems.res.mjs +55 -32
- package/src/TestIndexer.res +54 -40
- package/src/TestIndexer.res.mjs +37 -30
- package/src/bindings/Performance.res +7 -0
- package/src/bindings/Performance.res.mjs +21 -0
- package/src/bindings/Performance.resi +7 -0
- package/src/bindings/Viem.res +0 -41
- package/src/bindings/Viem.res.mjs +1 -43
- package/src/sources/BlockStore.res +46 -0
- package/src/sources/BlockStore.res.mjs +24 -0
- package/src/sources/EventRouter.res +36 -33
- package/src/sources/EventRouter.res.mjs +13 -14
- package/src/sources/Evm.res +83 -72
- package/src/sources/Evm.res.mjs +53 -69
- package/src/sources/EvmChain.res +18 -20
- package/src/sources/EvmChain.res.mjs +15 -15
- package/src/sources/EvmRpcClient.res +74 -5
- package/src/sources/EvmRpcClient.res.mjs +13 -4
- package/src/sources/FieldMask.res +39 -0
- package/src/sources/FieldMask.res.mjs +42 -0
- package/src/sources/Fuel.res +9 -6
- package/src/sources/Fuel.res.mjs +5 -10
- package/src/sources/HyperFuelSource.res +46 -50
- package/src/sources/HyperFuelSource.res.mjs +64 -54
- package/src/sources/HyperSync.res +39 -7
- package/src/sources/HyperSync.res.mjs +57 -37
- package/src/sources/HyperSync.resi +12 -2
- package/src/sources/HyperSyncClient.res +22 -111
- package/src/sources/HyperSyncClient.res.mjs +2 -30
- package/src/sources/HyperSyncHeightStream.res +1 -8
- package/src/sources/HyperSyncHeightStream.res.mjs +0 -2
- package/src/sources/HyperSyncSource.res +94 -107
- package/src/sources/HyperSyncSource.res.mjs +82 -86
- package/src/sources/Rpc.res +15 -47
- package/src/sources/Rpc.res.mjs +25 -56
- package/src/sources/RpcSource.res +320 -467
- package/src/sources/RpcSource.res.mjs +268 -385
- package/src/sources/RpcWebSocketHeightStream.res +0 -5
- package/src/sources/RpcWebSocketHeightStream.res.mjs +0 -2
- package/src/sources/SimulateSource.res +10 -4
- package/src/sources/SimulateSource.res.mjs +16 -6
- package/src/sources/Source.res +34 -6
- package/src/sources/SourceManager.res +86 -27
- package/src/sources/SourceManager.res.mjs +84 -47
- package/src/sources/SourceManager.resi +12 -3
- package/src/sources/Svm.res +31 -16
- package/src/sources/Svm.res.mjs +36 -13
- package/src/sources/SvmHyperSyncClient.res +5 -32
- package/src/sources/SvmHyperSyncSource.res +159 -134
- package/src/sources/SvmHyperSyncSource.res.mjs +164 -129
- package/src/sources/TransactionStore.res +49 -0
- package/src/sources/TransactionStore.res.mjs +30 -0
- package/src/tui/Tui.res +13 -16
- package/src/tui/Tui.res.mjs +12 -14
- package/svm.schema.json +49 -37
- package/src/bindings/Hrtime.res +0 -58
- package/src/bindings/Hrtime.res.mjs +0 -90
- package/src/bindings/Hrtime.resi +0 -30
package/src/CrossChainState.res
CHANGED
|
@@ -11,10 +11,6 @@ type t = {
|
|
|
11
11
|
// True once every chain has caught up to head/endBlock. Monotonic during a run.
|
|
12
12
|
mutable isRealtime: bool,
|
|
13
13
|
mutable isInReorgThreshold: bool,
|
|
14
|
-
// Indexer-wide caps on concurrent data-source queries, shared across all
|
|
15
|
-
// chains. The realtime budget applies once every chain is at head.
|
|
16
|
-
maxBackfillConcurrency: int,
|
|
17
|
-
maxRealtimeConcurrency: int,
|
|
18
14
|
// Indexer-wide fetch buffer pool (item count), shared across all chains.
|
|
19
15
|
targetBufferSize: int,
|
|
20
16
|
}
|
|
@@ -26,18 +22,10 @@ let calculateTargetBufferSize = () =>
|
|
|
26
22
|
| None => 100_000
|
|
27
23
|
}
|
|
28
24
|
|
|
29
|
-
// The concurrency budget in force for the current phase.
|
|
30
|
-
let maxConcurrency = (crossChainState: t) =>
|
|
31
|
-
crossChainState.isRealtime
|
|
32
|
-
? crossChainState.maxRealtimeConcurrency
|
|
33
|
-
: crossChainState.maxBackfillConcurrency
|
|
34
|
-
|
|
35
25
|
let make = (
|
|
36
26
|
~chainStates,
|
|
37
27
|
~isInReorgThreshold,
|
|
38
28
|
~isRealtime,
|
|
39
|
-
~maxBackfillConcurrency=Env.maxBackfillConcurrency,
|
|
40
|
-
~maxRealtimeConcurrency=Env.maxRealtimeConcurrency,
|
|
41
29
|
~targetBufferSize=calculateTargetBufferSize(),
|
|
42
30
|
): t => {
|
|
43
31
|
let crossChainState = {
|
|
@@ -45,11 +33,8 @@ let make = (
|
|
|
45
33
|
chainIds: chainStates->Dict.valuesToArray->Array.map(cs => (cs->ChainState.chainConfig).id),
|
|
46
34
|
isRealtime,
|
|
47
35
|
isInReorgThreshold,
|
|
48
|
-
maxBackfillConcurrency,
|
|
49
|
-
maxRealtimeConcurrency,
|
|
50
36
|
targetBufferSize,
|
|
51
37
|
}
|
|
52
|
-
Prometheus.IndexingMaxConcurrency.set(~maxConcurrency=crossChainState->maxConcurrency)
|
|
53
38
|
Prometheus.IndexingTargetBufferSize.set(~targetBufferSize)
|
|
54
39
|
crossChainState
|
|
55
40
|
}
|
|
@@ -65,17 +50,6 @@ let chainStates = (crossChainState: t) => crossChainState.chainStates
|
|
|
65
50
|
let isRealtime = (crossChainState: t) => crossChainState.isRealtime
|
|
66
51
|
let isInReorgThreshold = (crossChainState: t) => crossChainState.isInReorgThreshold
|
|
67
52
|
|
|
68
|
-
// Partition queries in flight across every chain — the live draw against
|
|
69
|
-
// maxConcurrency.
|
|
70
|
-
let inFlight = (crossChainState: t) => {
|
|
71
|
-
let total = ref(0)
|
|
72
|
-
for i in 0 to crossChainState.chainIds->Array.length - 1 {
|
|
73
|
-
let cs = crossChainState->getChainState(crossChainState.chainIds->Array.getUnsafe(i))
|
|
74
|
-
total := total.contents + cs->ChainState.sourceManager->SourceManager.inFlightCount
|
|
75
|
-
}
|
|
76
|
-
total.contents
|
|
77
|
-
}
|
|
78
|
-
|
|
79
53
|
// Ready-to-process items across every chain — the live draw against
|
|
80
54
|
// targetBufferSize, which is a budget of processable events (items stuck behind
|
|
81
55
|
// a gap don't count toward the goal of keeping ~targetBufferSize ready).
|
|
@@ -83,7 +57,7 @@ let totalReadyCount = (crossChainState: t) => {
|
|
|
83
57
|
let total = ref(0)
|
|
84
58
|
for i in 0 to crossChainState.chainIds->Array.length - 1 {
|
|
85
59
|
let cs = crossChainState->getChainState(crossChainState.chainIds->Array.getUnsafe(i))
|
|
86
|
-
total := total.contents + cs->ChainState.
|
|
60
|
+
total := total.contents + cs->ChainState.bufferReadyCount
|
|
87
61
|
}
|
|
88
62
|
total.contents
|
|
89
63
|
}
|
|
@@ -91,9 +65,7 @@ let totalReadyCount = (crossChainState: t) => {
|
|
|
91
65
|
// --- Derived (pure). ---
|
|
92
66
|
|
|
93
67
|
let nextItemIsNone = (crossChainState: t): bool =>
|
|
94
|
-
!
|
|
95
|
-
crossChainState.chainStates->Dict.valuesToArray->Array.map(ChainState.fetchState),
|
|
96
|
-
)
|
|
68
|
+
!(crossChainState.chainStates->Dict.valuesToArray->Array.some(ChainState.hasReadyItem))
|
|
97
69
|
|
|
98
70
|
let getSafeCheckpointId = (crossChainState: t) => {
|
|
99
71
|
let result: ref<option<bigint>> = ref(None)
|
|
@@ -105,7 +77,7 @@ let getSafeCheckpointId = (crossChainState: t) => {
|
|
|
105
77
|
| Some(safeCheckpointTracking) =>
|
|
106
78
|
let safeCheckpointId =
|
|
107
79
|
safeCheckpointTracking->SafeCheckpointTracking.getSafeCheckpointId(
|
|
108
|
-
~sourceBlockNumber=
|
|
80
|
+
~sourceBlockNumber=cs->ChainState.knownHeight,
|
|
109
81
|
)
|
|
110
82
|
switch result.contents {
|
|
111
83
|
| None => result := Some(safeCheckpointId)
|
|
@@ -137,16 +109,9 @@ let createBatch = (
|
|
|
137
109
|
// in an append-only ClickHouse insert.
|
|
138
110
|
isRollback ? 1n : 0n,
|
|
139
111
|
),
|
|
140
|
-
~chainsBeforeBatch=crossChainState.chainStates->Utils.Dict.mapValues(
|
|
141
|
-
|
|
142
|
-
)
|
|
143
|
-
fetchState: cs->ChainState.fetchState,
|
|
144
|
-
progressBlockNumber: cs->ChainState.committedProgressBlockNumber,
|
|
145
|
-
totalEventsProcessed: cs->ChainState.numEventsProcessed,
|
|
146
|
-
sourceBlockNumber: (cs->ChainState.fetchState).knownHeight,
|
|
147
|
-
reorgDetection: cs->ChainState.reorgDetection,
|
|
148
|
-
chainConfig: cs->ChainState.chainConfig,
|
|
149
|
-
}),
|
|
112
|
+
~chainsBeforeBatch=crossChainState.chainStates->Utils.Dict.mapValues(
|
|
113
|
+
ChainState.toChainBeforeBatch,
|
|
114
|
+
),
|
|
150
115
|
~batchSizeTarget,
|
|
151
116
|
)
|
|
152
117
|
}
|
|
@@ -170,13 +135,13 @@ let enterReorgThreshold = (crossChainState: t) => {
|
|
|
170
135
|
// whole indexer. A chain is marked caught up only once EVERY chain is caught up
|
|
171
136
|
// (reached endblock or fetched/processed to head) with no processable events
|
|
172
137
|
// left — so no chain flips to ready while another is still backfilling.
|
|
173
|
-
let applyBatchProgress = (crossChainState: t, ~batch: Batch.t) => {
|
|
138
|
+
let applyBatchProgress = (crossChainState: t, ~batch: Batch.t, ~blockTimestampName: string) => {
|
|
174
139
|
let chainIds = crossChainState.chainIds
|
|
175
140
|
|
|
176
141
|
let everyChainCaughtUp = ref(true)
|
|
177
142
|
for i in 0 to chainIds->Array.length - 1 {
|
|
178
143
|
let cs = crossChainState->getChainState(chainIds->Array.getUnsafe(i))
|
|
179
|
-
cs->ChainState.applyBatchProgress(~batch)
|
|
144
|
+
cs->ChainState.applyBatchProgress(~batch, ~blockTimestampName)
|
|
180
145
|
if !(cs->ChainState.hasProcessedToEndblock || cs->ChainState.isProgressAtHead) {
|
|
181
146
|
everyChainCaughtUp := false
|
|
182
147
|
}
|
|
@@ -199,71 +164,155 @@ let applyBatchProgress = (crossChainState: t, ~batch: Batch.t) => {
|
|
|
199
164
|
Prometheus.ProgressReady.setAllReady()
|
|
200
165
|
}
|
|
201
166
|
|
|
202
|
-
let wasRealtime = crossChainState.isRealtime
|
|
203
167
|
crossChainState.isRealtime = crossChainState.isRealtime || allChainsReady.contents
|
|
204
|
-
if !wasRealtime && crossChainState.isRealtime {
|
|
205
|
-
// The realtime budget takes over now that every chain is at head.
|
|
206
|
-
Prometheus.IndexingMaxConcurrency.set(~maxConcurrency=crossChainState->maxConcurrency)
|
|
207
|
-
}
|
|
208
168
|
}
|
|
209
169
|
|
|
210
170
|
// --- Fetch control. ---
|
|
211
171
|
|
|
212
|
-
// Chains ordered furthest-behind first, so the shared
|
|
213
|
-
//
|
|
172
|
+
// Chains ordered furthest-behind first, so the shared buffer pool goes to the
|
|
173
|
+
// chains with the most backfill work before the rest.
|
|
214
174
|
let priorityOrder = (crossChainState: t) =>
|
|
215
175
|
crossChainState.chainStates
|
|
216
176
|
->Dict.valuesToArray
|
|
217
177
|
->Array.toSorted((a, b) =>
|
|
218
|
-
Float.compare(
|
|
219
|
-
a->ChainState.fetchState->FetchState.getProgressPercentage,
|
|
220
|
-
b->ChainState.fetchState->FetchState.getProgressPercentage,
|
|
221
|
-
)
|
|
178
|
+
Float.compare(a->ChainState.getProgressPercentage, b->ChainState.getProgressPercentage)
|
|
222
179
|
)
|
|
223
180
|
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
181
|
+
// In-flight estimated items across every chain — the live draw against
|
|
182
|
+
// targetBufferSize alongside totalReadyCount, so the pool isn't re-dispatched
|
|
183
|
+
// while queries are still being fetched.
|
|
184
|
+
let totalReservedSize = (crossChainState: t) => {
|
|
185
|
+
let total = ref(0.)
|
|
186
|
+
for i in 0 to crossChainState.chainIds->Array.length - 1 {
|
|
187
|
+
let cs = crossChainState->getChainState(crossChainState.chainIds->Array.getUnsafe(i))
|
|
188
|
+
total := total.contents +. cs->ChainState.pendingBudget
|
|
189
|
+
}
|
|
190
|
+
total.contents
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Dispatch a fetch tick across the whole indexer from one shared pool of
|
|
194
|
+
// ~targetBufferSize ready events, as a waterfall: visit chains furthest-behind
|
|
195
|
+
// first, hand each the budget remaining at that point (plus its own
|
|
196
|
+
// already-reserved share, since a chain's pending queries aren't "spent" —
|
|
197
|
+
// they're this chain's), let it turn that into queries sized against its own
|
|
198
|
+
// chain-density-derived target block, then subtract what it actually used
|
|
199
|
+
// before moving to the next chain. So a chain that can only use a little
|
|
200
|
+
// (density too low, or already caught up) leaves the rest for the others
|
|
201
|
+
// automatically. A chain visited after the budget ran out simply doesn't
|
|
202
|
+
// query this round — the leader's reservations release as its responses land,
|
|
203
|
+
// so the next tick redistributes. Chains that do get budget are additionally
|
|
204
|
+
// capped at the most-behind chain's target progress mapped onto their own
|
|
205
|
+
// range, so no chain runs further ahead than the chain the pool is
|
|
206
|
+
// prioritizing.
|
|
233
207
|
let checkAndFetch = async (
|
|
234
208
|
crossChainState: t,
|
|
235
|
-
~
|
|
236
|
-
~chain: ChainMap.Chain.t,
|
|
237
|
-
~concurrencyLimit: int,
|
|
238
|
-
~bufferLimit: int,
|
|
239
|
-
) => promise<unit>,
|
|
209
|
+
~dispatchChain: (~chain: ChainMap.Chain.t, ~action: FetchState.nextQuery) => promise<unit>,
|
|
240
210
|
) => {
|
|
241
|
-
let
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
|
|
253
|
-
let
|
|
211
|
+
let remaining = ref(
|
|
212
|
+
Pervasives.max(
|
|
213
|
+
0.,
|
|
214
|
+
crossChainState.targetBufferSize->Int.toFloat -.
|
|
215
|
+
crossChainState->totalReadyCount->Int.toFloat -.
|
|
216
|
+
crossChainState->totalReservedSize,
|
|
217
|
+
),
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
// A chain with no density signal probes blind, so it only gets a bounded
|
|
221
|
+
// slice of the pool — one unknown chain shouldn't hold the whole budget
|
|
222
|
+
// while it takes its first measurements.
|
|
223
|
+
let coldChainBudget = Pervasives.min(5_000., crossChainState.targetBufferSize->Int.toFloat)
|
|
224
|
+
|
|
225
|
+
// Chunk reservations get headroom over the density estimate so a
|
|
226
|
+
// denser-than-expected range doesn't truncate at the server cap; realtime
|
|
227
|
+
// gets more since a forced catch-up query there costs a head-poll roundtrip.
|
|
228
|
+
let chunkItemsMultiplier = crossChainState.isRealtime ? 3. : 1.5
|
|
229
|
+
|
|
230
|
+
let actionByChain = Dict.make()
|
|
231
|
+
let maxProgress = ref(None)
|
|
232
|
+
crossChainState
|
|
233
|
+
->priorityOrder
|
|
234
|
+
->Array.forEach(cs => {
|
|
235
|
+
let chainId = (cs->ChainState.chainConfig).id
|
|
236
|
+
if cs->ChainState.knownHeight == 0 {
|
|
237
|
+
// No height yet — nothing to size a budget or an alignment line
|
|
238
|
+
// against. Skip without consuming budget or claiming leadership, so a
|
|
239
|
+
// chain whose source hasn't reported doesn't unconstrain everyone else.
|
|
240
|
+
actionByChain->Utils.Dict.setByInt(chainId, FetchState.WaitingForNewBlock)
|
|
241
|
+
} else {
|
|
242
|
+
let isCold = cs->ChainState.effectiveDensity === None
|
|
243
|
+
let chainTargetItems =
|
|
244
|
+
(isCold ? Pervasives.min(remaining.contents, coldChainBudget) : remaining.contents) +.
|
|
245
|
+
cs->ChainState.pendingBudget
|
|
246
|
+
let maxTargetBlock = switch maxProgress.contents {
|
|
247
|
+
| None if isCold =>
|
|
248
|
+
// A cold chain's target is a guess — it doesn't set the alignment
|
|
249
|
+
// line; the first density-bearing chain after it does.
|
|
250
|
+
None
|
|
251
|
+
| None =>
|
|
252
|
+
// The most-behind chain sets the alignment line for everyone after it.
|
|
253
|
+
maxProgress :=
|
|
254
|
+
Some(
|
|
255
|
+
cs->ChainState.progressAtBlock(
|
|
256
|
+
~blockNumber=cs->ChainState.targetBlock(~chainTargetItems),
|
|
257
|
+
),
|
|
258
|
+
)
|
|
259
|
+
None
|
|
260
|
+
// 5% margin past the leader's line: chains whose progress tracks the
|
|
261
|
+
// leader closely would otherwise flap in and out of the clamp as
|
|
262
|
+
// leadership alternates, stalling their pipeline every other tick.
|
|
263
|
+
| Some(progress) => Some(cs->ChainState.blockAtProgress(~progress=progress +. 0.05))
|
|
264
|
+
}
|
|
265
|
+
switch cs->ChainState.getNextQuery(
|
|
266
|
+
~chainTargetItems,
|
|
267
|
+
~chunkItemsMultiplier,
|
|
268
|
+
~maxTargetBlock?,
|
|
269
|
+
) {
|
|
270
|
+
| (WaitingForNewBlock | NothingToQuery) as action =>
|
|
271
|
+
actionByChain->Utils.Dict.setByInt(chainId, action)
|
|
272
|
+
| Ready(queries) => {
|
|
273
|
+
let consumed =
|
|
274
|
+
queries->Array.reduce(0., (acc, query: FetchState.query) =>
|
|
275
|
+
acc +. query.itemsEst->Int.toFloat
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
let partitions = Dict.make()
|
|
279
|
+
queries->Array.forEach((query: FetchState.query) =>
|
|
280
|
+
partitions->Dict.set(
|
|
281
|
+
query.partitionId,
|
|
282
|
+
{
|
|
283
|
+
"fromBlock": query.fromBlock,
|
|
284
|
+
"targetBlock": query.toBlock,
|
|
285
|
+
"targetEvents": query.itemsTarget,
|
|
286
|
+
},
|
|
287
|
+
)
|
|
288
|
+
)
|
|
289
|
+
Logging.trace({
|
|
290
|
+
"msg": "Started querying",
|
|
291
|
+
"chainId": chainId,
|
|
292
|
+
"partitions": partitions,
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
actionByChain->Utils.Dict.setByInt(chainId, FetchState.Ready(queries))
|
|
296
|
+
// Mark the queries in flight and reserve their size against the
|
|
297
|
+
// shared budget; released as each response lands in
|
|
298
|
+
// handleQueryResult.
|
|
299
|
+
cs->ChainState.startFetchingQueries(~queries)
|
|
300
|
+
remaining := Pervasives.max(0., remaining.contents -. consumed)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
})
|
|
305
|
+
|
|
254
306
|
let promises = []
|
|
255
|
-
for i in 0 to
|
|
256
|
-
let
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
(
|
|
263
|
-
|
|
264
|
-
let inFlightBefore = sourceManager->SourceManager.inFlightCount
|
|
265
|
-
promises->Array.push(fetchChain(~chain, ~concurrencyLimit, ~bufferLimit))
|
|
266
|
-
inFlight := inFlight.contents + (sourceManager->SourceManager.inFlightCount - inFlightBefore)
|
|
307
|
+
for i in 0 to crossChainState.chainIds->Array.length - 1 {
|
|
308
|
+
let chainId = crossChainState.chainIds->Array.getUnsafe(i)
|
|
309
|
+
switch actionByChain->Utils.Dict.dangerouslyGetByIntNonOption(chainId) {
|
|
310
|
+
| Some(NothingToQuery)
|
|
311
|
+
| None => ()
|
|
312
|
+
| Some(action) =>
|
|
313
|
+
let chain = ChainMap.Chain.makeUnsafe(~chainId)
|
|
314
|
+
promises->Array.push(dispatchChain(~chain, ~action))
|
|
315
|
+
}
|
|
267
316
|
}
|
|
268
317
|
let _ = await promises->Promise.all
|
|
269
318
|
}
|
|
@@ -6,10 +6,8 @@ import * as Utils from "./Utils.res.mjs";
|
|
|
6
6
|
import * as Logging from "./Logging.res.mjs";
|
|
7
7
|
import * as ChainMap from "./ChainMap.res.mjs";
|
|
8
8
|
import * as ChainState from "./ChainState.res.mjs";
|
|
9
|
-
import * as FetchState from "./FetchState.res.mjs";
|
|
10
9
|
import * as Prometheus from "./Prometheus.res.mjs";
|
|
11
|
-
import * as
|
|
12
|
-
import * as SourceManager from "./sources/SourceManager.res.mjs";
|
|
10
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
13
11
|
import * as Primitive_float from "@rescript/runtime/lib/es6/Primitive_float.js";
|
|
14
12
|
import * as SafeCheckpointTracking from "./SafeCheckpointTracking.res.mjs";
|
|
15
13
|
|
|
@@ -21,17 +19,7 @@ function calculateTargetBufferSize() {
|
|
|
21
19
|
}
|
|
22
20
|
}
|
|
23
21
|
|
|
24
|
-
function
|
|
25
|
-
if (crossChainState.isRealtime) {
|
|
26
|
-
return crossChainState.maxRealtimeConcurrency;
|
|
27
|
-
} else {
|
|
28
|
-
return crossChainState.maxBackfillConcurrency;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function make(chainStates, isInReorgThreshold, isRealtime, maxBackfillConcurrencyOpt, maxRealtimeConcurrencyOpt, targetBufferSizeOpt) {
|
|
33
|
-
let maxBackfillConcurrency = maxBackfillConcurrencyOpt !== undefined ? maxBackfillConcurrencyOpt : Env.maxBackfillConcurrency;
|
|
34
|
-
let maxRealtimeConcurrency = maxRealtimeConcurrencyOpt !== undefined ? maxRealtimeConcurrencyOpt : Env.maxRealtimeConcurrency;
|
|
22
|
+
function make(chainStates, isInReorgThreshold, isRealtime, targetBufferSizeOpt) {
|
|
35
23
|
let targetBufferSize = targetBufferSizeOpt !== undefined ? targetBufferSizeOpt : (
|
|
36
24
|
Env.targetBufferSize !== undefined ? Env.targetBufferSize : 100000
|
|
37
25
|
);
|
|
@@ -40,11 +28,8 @@ function make(chainStates, isInReorgThreshold, isRealtime, maxBackfillConcurrenc
|
|
|
40
28
|
chainIds: Object.values(chainStates).map(cs => ChainState.chainConfig(cs).id),
|
|
41
29
|
isRealtime: isRealtime,
|
|
42
30
|
isInReorgThreshold: isInReorgThreshold,
|
|
43
|
-
maxBackfillConcurrency: maxBackfillConcurrency,
|
|
44
|
-
maxRealtimeConcurrency: maxRealtimeConcurrency,
|
|
45
31
|
targetBufferSize: targetBufferSize
|
|
46
32
|
};
|
|
47
|
-
Prometheus.IndexingMaxConcurrency.set(maxConcurrency(crossChainState));
|
|
48
33
|
Prometheus.IndexingTargetBufferSize.set(targetBufferSize);
|
|
49
34
|
return crossChainState;
|
|
50
35
|
}
|
|
@@ -61,26 +46,17 @@ function isInReorgThreshold(crossChainState) {
|
|
|
61
46
|
return crossChainState.isInReorgThreshold;
|
|
62
47
|
}
|
|
63
48
|
|
|
64
|
-
function inFlight(crossChainState) {
|
|
65
|
-
let total = 0;
|
|
66
|
-
for (let i = 0, i_finish = crossChainState.chainIds.length; i < i_finish; ++i) {
|
|
67
|
-
let cs = crossChainState.chainStates[crossChainState.chainIds[i]];
|
|
68
|
-
total = total + SourceManager.inFlightCount(ChainState.sourceManager(cs)) | 0;
|
|
69
|
-
}
|
|
70
|
-
return total;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
49
|
function totalReadyCount(crossChainState) {
|
|
74
50
|
let total = 0;
|
|
75
51
|
for (let i = 0, i_finish = crossChainState.chainIds.length; i < i_finish; ++i) {
|
|
76
52
|
let cs = crossChainState.chainStates[crossChainState.chainIds[i]];
|
|
77
|
-
total = total +
|
|
53
|
+
total = total + ChainState.bufferReadyCount(cs) | 0;
|
|
78
54
|
}
|
|
79
55
|
return total;
|
|
80
56
|
}
|
|
81
57
|
|
|
82
58
|
function nextItemIsNone(crossChainState) {
|
|
83
|
-
return !
|
|
59
|
+
return !Object.values(crossChainState.chainStates).some(ChainState.hasReadyItem);
|
|
84
60
|
}
|
|
85
61
|
|
|
86
62
|
function getSafeCheckpointId(crossChainState) {
|
|
@@ -89,7 +65,7 @@ function getSafeCheckpointId(crossChainState) {
|
|
|
89
65
|
let cs = crossChainState.chainStates[crossChainState.chainIds[i]];
|
|
90
66
|
let safeCheckpointTracking = ChainState.safeCheckpointTracking(cs);
|
|
91
67
|
if (safeCheckpointTracking !== undefined) {
|
|
92
|
-
let safeCheckpointId = SafeCheckpointTracking.getSafeCheckpointId(safeCheckpointTracking, ChainState.
|
|
68
|
+
let safeCheckpointId = SafeCheckpointTracking.getSafeCheckpointId(safeCheckpointTracking, ChainState.knownHeight(cs));
|
|
93
69
|
let current = result;
|
|
94
70
|
if (!(current !== undefined && safeCheckpointId >= current)) {
|
|
95
71
|
result = safeCheckpointId;
|
|
@@ -105,14 +81,7 @@ function getSafeCheckpointId(crossChainState) {
|
|
|
105
81
|
function createBatch(crossChainState, processedCheckpointId, batchSizeTarget, isRollback) {
|
|
106
82
|
return Batch.make(processedCheckpointId + (
|
|
107
83
|
isRollback ? 1n : 0n
|
|
108
|
-
), Utils.Dict.mapValues(crossChainState.chainStates,
|
|
109
|
-
fetchState: ChainState.fetchState(cs),
|
|
110
|
-
reorgDetection: ChainState.reorgDetection(cs),
|
|
111
|
-
progressBlockNumber: ChainState.committedProgressBlockNumber(cs),
|
|
112
|
-
sourceBlockNumber: ChainState.fetchState(cs).knownHeight,
|
|
113
|
-
totalEventsProcessed: ChainState.numEventsProcessed(cs),
|
|
114
|
-
chainConfig: ChainState.chainConfig(cs)
|
|
115
|
-
})), batchSizeTarget, crossChainState.isInReorgThreshold);
|
|
84
|
+
), Utils.Dict.mapValues(crossChainState.chainStates, ChainState.toChainBeforeBatch), batchSizeTarget, crossChainState.isInReorgThreshold);
|
|
116
85
|
}
|
|
117
86
|
|
|
118
87
|
function enterReorgThreshold(crossChainState) {
|
|
@@ -124,12 +93,12 @@ function enterReorgThreshold(crossChainState) {
|
|
|
124
93
|
crossChainState.isInReorgThreshold = true;
|
|
125
94
|
}
|
|
126
95
|
|
|
127
|
-
function applyBatchProgress(crossChainState, batch) {
|
|
96
|
+
function applyBatchProgress(crossChainState, batch, blockTimestampName) {
|
|
128
97
|
let chainIds = crossChainState.chainIds;
|
|
129
98
|
let everyChainCaughtUp = true;
|
|
130
99
|
for (let i = 0, i_finish = chainIds.length; i < i_finish; ++i) {
|
|
131
100
|
let cs = crossChainState.chainStates[chainIds[i]];
|
|
132
|
-
ChainState.applyBatchProgress(cs, batch);
|
|
101
|
+
ChainState.applyBatchProgress(cs, batch, blockTimestampName);
|
|
133
102
|
if (!(ChainState.hasProcessedToEndblock(cs) || ChainState.isProgressAtHead(cs))) {
|
|
134
103
|
everyChainCaughtUp = false;
|
|
135
104
|
}
|
|
@@ -148,32 +117,93 @@ function applyBatchProgress(crossChainState, batch) {
|
|
|
148
117
|
if (allChainsReady) {
|
|
149
118
|
Prometheus.ProgressReady.setAllReady();
|
|
150
119
|
}
|
|
151
|
-
let wasRealtime = crossChainState.isRealtime;
|
|
152
120
|
crossChainState.isRealtime = crossChainState.isRealtime || allChainsReady;
|
|
153
|
-
if (!wasRealtime && crossChainState.isRealtime) {
|
|
154
|
-
return Prometheus.IndexingMaxConcurrency.set(maxConcurrency(crossChainState));
|
|
155
|
-
}
|
|
156
121
|
}
|
|
157
122
|
|
|
158
123
|
function priorityOrder(crossChainState) {
|
|
159
|
-
return Object.values(crossChainState.chainStates).toSorted((a, b) => Primitive_float.compare(
|
|
124
|
+
return Object.values(crossChainState.chainStates).toSorted((a, b) => Primitive_float.compare(ChainState.getProgressPercentage(a), ChainState.getProgressPercentage(b)));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function totalReservedSize(crossChainState) {
|
|
128
|
+
let total = 0;
|
|
129
|
+
for (let i = 0, i_finish = crossChainState.chainIds.length; i < i_finish; ++i) {
|
|
130
|
+
let cs = crossChainState.chainStates[crossChainState.chainIds[i]];
|
|
131
|
+
total = total + ChainState.pendingBudget(cs);
|
|
132
|
+
}
|
|
133
|
+
return total;
|
|
160
134
|
}
|
|
161
135
|
|
|
162
|
-
async function checkAndFetch(crossChainState,
|
|
163
|
-
let
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
let
|
|
136
|
+
async function checkAndFetch(crossChainState, dispatchChain) {
|
|
137
|
+
let remaining = {
|
|
138
|
+
contents: Primitive_float.max(0, crossChainState.targetBufferSize - totalReadyCount(crossChainState) - totalReservedSize(crossChainState))
|
|
139
|
+
};
|
|
140
|
+
let coldChainBudget = Primitive_float.min(5000, crossChainState.targetBufferSize);
|
|
141
|
+
let chunkItemsMultiplier = crossChainState.isRealtime ? 3 : 1.5;
|
|
142
|
+
let actionByChain = {};
|
|
143
|
+
let maxProgress = {
|
|
144
|
+
contents: undefined
|
|
145
|
+
};
|
|
146
|
+
priorityOrder(crossChainState).forEach(cs => {
|
|
147
|
+
let chainId = ChainState.chainConfig(cs).id;
|
|
148
|
+
if (ChainState.knownHeight(cs) === 0) {
|
|
149
|
+
actionByChain[chainId] = "WaitingForNewBlock";
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
let isCold = ChainState.effectiveDensity(cs) === undefined;
|
|
153
|
+
let chainTargetItems = (
|
|
154
|
+
isCold ? Primitive_float.min(remaining.contents, coldChainBudget) : remaining.contents
|
|
155
|
+
) + ChainState.pendingBudget(cs);
|
|
156
|
+
let progress = maxProgress.contents;
|
|
157
|
+
let maxTargetBlock = progress !== undefined ? ChainState.blockAtProgress(cs, progress + 0.05) : (
|
|
158
|
+
isCold ? undefined : (maxProgress.contents = ChainState.progressAtBlock(cs, ChainState.targetBlock(cs, chainTargetItems)), undefined)
|
|
159
|
+
);
|
|
160
|
+
let action = ChainState.getNextQuery(cs, chainTargetItems, chunkItemsMultiplier, maxTargetBlock);
|
|
161
|
+
if (typeof action !== "object") {
|
|
162
|
+
if (action === "WaitingForNewBlock") {
|
|
163
|
+
actionByChain[chainId] = action;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
actionByChain[chainId] = action;
|
|
167
|
+
return;
|
|
168
|
+
} else {
|
|
169
|
+
let queries = action._0;
|
|
170
|
+
let consumed = Stdlib_Array.reduce(queries, 0, (acc, query) => acc + query.itemsEst);
|
|
171
|
+
let partitions = {};
|
|
172
|
+
queries.forEach(query => {
|
|
173
|
+
partitions[query.partitionId] = {
|
|
174
|
+
fromBlock: query.fromBlock,
|
|
175
|
+
targetBlock: query.toBlock,
|
|
176
|
+
targetEvents: query.itemsTarget
|
|
177
|
+
};
|
|
178
|
+
});
|
|
179
|
+
Logging.trace({
|
|
180
|
+
msg: "Started querying",
|
|
181
|
+
chainId: chainId,
|
|
182
|
+
partitions: partitions
|
|
183
|
+
});
|
|
184
|
+
actionByChain[chainId] = {
|
|
185
|
+
TAG: "Ready",
|
|
186
|
+
_0: queries
|
|
187
|
+
};
|
|
188
|
+
ChainState.startFetchingQueries(cs, queries);
|
|
189
|
+
remaining.contents = Primitive_float.max(0, remaining.contents - consumed);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
});
|
|
167
193
|
let promises = [];
|
|
168
|
-
for (let i = 0, i_finish =
|
|
169
|
-
let
|
|
170
|
-
let
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
194
|
+
for (let i = 0, i_finish = crossChainState.chainIds.length; i < i_finish; ++i) {
|
|
195
|
+
let chainId = crossChainState.chainIds[i];
|
|
196
|
+
let action = actionByChain[chainId];
|
|
197
|
+
if (action !== undefined) {
|
|
198
|
+
let exit = 0;
|
|
199
|
+
if (typeof action === "object" || action === "WaitingForNewBlock") {
|
|
200
|
+
exit = 1;
|
|
201
|
+
}
|
|
202
|
+
if (exit === 1) {
|
|
203
|
+
let chain = ChainMap.Chain.makeUnsafe(chainId);
|
|
204
|
+
promises.push(dispatchChain(chain, action));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
177
207
|
}
|
|
178
208
|
await Promise.all(promises);
|
|
179
209
|
}
|
|
@@ -184,8 +214,6 @@ export {
|
|
|
184
214
|
chainStates,
|
|
185
215
|
isRealtime,
|
|
186
216
|
isInReorgThreshold,
|
|
187
|
-
maxConcurrency,
|
|
188
|
-
inFlight,
|
|
189
217
|
nextItemIsNone,
|
|
190
218
|
getSafeCheckpointId,
|
|
191
219
|
createBatch,
|
package/src/CrossChainState.resi
CHANGED
|
@@ -9,8 +9,6 @@ let make: (
|
|
|
9
9
|
~chainStates: dict<ChainState.t>,
|
|
10
10
|
~isInReorgThreshold: bool,
|
|
11
11
|
~isRealtime: bool,
|
|
12
|
-
~maxBackfillConcurrency: int=?,
|
|
13
|
-
~maxRealtimeConcurrency: int=?,
|
|
14
12
|
~targetBufferSize: int=?,
|
|
15
13
|
) => t
|
|
16
14
|
|
|
@@ -18,8 +16,6 @@ let make: (
|
|
|
18
16
|
let chainStates: t => dict<ChainState.t>
|
|
19
17
|
let isRealtime: t => bool
|
|
20
18
|
let isInReorgThreshold: t => bool
|
|
21
|
-
let maxConcurrency: t => int
|
|
22
|
-
let inFlight: t => int
|
|
23
19
|
|
|
24
20
|
// Derived (pure).
|
|
25
21
|
let nextItemIsNone: t => bool
|
|
@@ -33,15 +29,11 @@ let createBatch: (
|
|
|
33
29
|
~isRollback: bool,
|
|
34
30
|
) => Batch.t
|
|
35
31
|
let enterReorgThreshold: t => unit
|
|
36
|
-
let applyBatchProgress: (t, ~batch: Batch.t) => unit
|
|
32
|
+
let applyBatchProgress: (t, ~batch: Batch.t, ~blockTimestampName: string) => unit
|
|
37
33
|
|
|
38
34
|
// Fetch control.
|
|
39
35
|
let priorityOrder: t => array<ChainState.t>
|
|
40
36
|
let checkAndFetch: (
|
|
41
37
|
t,
|
|
42
|
-
~
|
|
43
|
-
~chain: ChainMap.Chain.t,
|
|
44
|
-
~concurrencyLimit: int,
|
|
45
|
-
~bufferLimit: int,
|
|
46
|
-
) => promise<unit>,
|
|
38
|
+
~dispatchChain: (~chain: ChainMap.Chain.t, ~action: FetchState.nextQuery) => promise<unit>,
|
|
47
39
|
) => promise<unit>
|
package/src/Ecosystem.res
CHANGED
|
@@ -2,8 +2,6 @@ type name = | @as("evm") Evm | @as("fuel") Fuel | @as("svm") Svm
|
|
|
2
2
|
|
|
3
3
|
type t = {
|
|
4
4
|
name: name,
|
|
5
|
-
blockFields: array<string>,
|
|
6
|
-
transactionFields: array<string>,
|
|
7
5
|
blockNumberName: string,
|
|
8
6
|
blockTimestampName: string,
|
|
9
7
|
blockHashName: string,
|
|
@@ -33,7 +31,8 @@ type t = {
|
|
|
33
31
|
loggers (see `getItemLogger`). */
|
|
34
32
|
logger: Pino.t,
|
|
35
33
|
/** Materialise the user-facing event handed to handlers and contract
|
|
36
|
-
registration from an item's opaque payload.
|
|
34
|
+
registration from an item's opaque payload. `event.transaction` is written
|
|
35
|
+
onto the payload at batch prep (HyperSync) or inline (RPC/simulate). */
|
|
37
36
|
toEvent: Internal.eventItem => Internal.event,
|
|
38
37
|
/** Build the per-item child logger for an event item, with
|
|
39
38
|
ecosystem-specific log fields (EVM/Fuel: contract/event/address; SVM:
|
|
@@ -70,12 +69,12 @@ let getItemLogger = {
|
|
|
70
69
|
| None =>
|
|
71
70
|
let logger = switch item {
|
|
72
71
|
| Internal.Event(_) => ecosystem.toEventLogger(item->Internal.castUnsafeEventItem)
|
|
73
|
-
| Block({blockNumber,
|
|
72
|
+
| Block({blockNumber, onBlockRegistration}) =>
|
|
74
73
|
Logging.createChildFrom(
|
|
75
74
|
~logger=ecosystem.logger,
|
|
76
75
|
~params={
|
|
77
|
-
"onBlock":
|
|
78
|
-
"chainId":
|
|
76
|
+
"onBlock": onBlockRegistration.name,
|
|
77
|
+
"chainId": onBlockRegistration.chainId,
|
|
79
78
|
"block": blockNumber,
|
|
80
79
|
},
|
|
81
80
|
)
|
package/src/Ecosystem.res.mjs
CHANGED
|
@@ -26,10 +26,10 @@ function getItemLogger(item, ecosystem) {
|
|
|
26
26
|
if (item.kind === 0) {
|
|
27
27
|
logger$1 = ecosystem.toEventLogger(item);
|
|
28
28
|
} else {
|
|
29
|
-
let
|
|
29
|
+
let onBlockRegistration = item.onBlockRegistration;
|
|
30
30
|
logger$1 = Logging.createChildFrom(ecosystem.logger, {
|
|
31
|
-
onBlock:
|
|
32
|
-
chainId:
|
|
31
|
+
onBlock: onBlockRegistration.name,
|
|
32
|
+
chainId: onBlockRegistration.chainId,
|
|
33
33
|
block: item.blockNumber
|
|
34
34
|
});
|
|
35
35
|
}
|
package/src/Env.res
CHANGED
|
@@ -8,13 +8,6 @@ let updateSyncTimeOnRestart =
|
|
|
8
8
|
envSafe->EnvSafe.get("UPDATE_SYNC_TIME_ON_RESTART", S.bool, ~fallback=true)
|
|
9
9
|
let targetBufferSize = envSafe->EnvSafe.get("ENVIO_INDEXING_MAX_BUFFER_SIZE", S.option(S.int))
|
|
10
10
|
let maxAddrInPartition = envSafe->EnvSafe.get("MAX_PARTITION_SIZE", S.int, ~fallback=5_000)
|
|
11
|
-
// Global caps on concurrent data-source queries across the whole indexer (all
|
|
12
|
-
// chains share the budget), not per chain. Realtime allows far more since head
|
|
13
|
-
// queries are small and many chains follow the head at once.
|
|
14
|
-
let maxBackfillConcurrency =
|
|
15
|
-
envSafe->EnvSafe.get("ENVIO_MAX_BACKFILL_CONCURRENCY", S.int, ~fallback=30)
|
|
16
|
-
let maxRealtimeConcurrency =
|
|
17
|
-
envSafe->EnvSafe.get("ENVIO_MAX_REALTIME_CONCURRENCY", S.int, ~fallback=200)
|
|
18
11
|
|
|
19
12
|
// Target number of in-memory objects (uncommitted entity/effect changes plus
|
|
20
13
|
// unwritten batch items) the store holds before processing waits for the write
|