envio 3.1.0-rc.1 → 3.1.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.
@@ -21,36 +21,91 @@ module EntityTables = {
21
21
  }
22
22
 
23
23
  type effectCacheInMemTable = {
24
- idsToStore: array<string>,
24
+ // Cache keys whose handler output is persisted on the next write. Drained
25
+ // each write; eviction is driven by the per-entry checkpointId instead.
26
+ mutable idsToStore: array<string>,
25
27
  mutable invalidationsCount: int,
26
- dict: dict<Internal.effectOutput>,
28
+ // Each entry is stamped with the checkpoint that referenced it (or
29
+ // loadedFromDbCheckpointId for db reads), so committed entries can be
30
+ // dropped once persisted/re-derivable, mirroring entity changes.
31
+ mutable dict: dict<Change.t<Internal.effectOutput>>,
32
+ mutable changesCount: float,
27
33
  effect: Internal.effect,
28
34
  }
29
35
 
30
36
  type t = {
31
37
  allEntities: array<Internal.entityConfig>,
32
- mutable rawEvents: array<InternalTable.RawEvents.t>,
33
38
  mutable entities: dict<InMemoryTable.Entity.t>,
34
39
  mutable effects: dict<effectCacheInMemTable>,
35
40
  mutable rollback: option<Persistence.rollback>,
41
+ // Last checkpoint persisted to the db.
36
42
  mutable committedCheckpointId: Internal.checkpointId,
43
+ // Processing frontier; runs ahead of committedCheckpointId while writes lag.
44
+ mutable processedCheckpointId: Internal.checkpointId,
45
+ // Processed but unwritten. The cycle drains them, splitting each write at a
46
+ // change in isInReorgThreshold so it never mixes history-saving modes.
47
+ mutable processedBatches: array<Batch.t>,
48
+ // The single in-flight write loop, None when idle.
49
+ mutable writeFiber: option<promise<unit>>,
50
+ // Set once a write throws, to stop the loop. The error itself goes to onError.
51
+ mutable hasFailedWrite: bool,
52
+ // Called once on a write failure; the caller decides what to do (exit).
53
+ onError: exn => unit,
54
+ // Resolved after every commit so capacity/flush waiters can re-evaluate.
55
+ mutable commitWaiters: array<unit => unit>,
56
+ persistence: Persistence.t,
57
+ config: Config.t,
58
+ // Latest metadata staged per chain; used to skip unchanged restages.
59
+ mutable chainMeta: dict<InternalTable.Chains.metaFields>,
60
+ // Set on a real change. Folded into a batch write, else flushed on the throttle.
61
+ mutable chainMetaDirty: bool,
62
+ // Throttles metadata-only writes when no batches flow.
63
+ chainMetaThrottler: Throttler.t,
37
64
  }
38
65
 
39
66
  let make = (
40
67
  ~entities: array<Internal.entityConfig>,
41
68
  ~committedCheckpointId=Internal.initialCheckpointId,
69
+ ~persistence: Persistence.t,
70
+ ~config: Config.t,
71
+ ~onError: exn => unit,
42
72
  ): t => {
43
- allEntities: entities,
44
- rawEvents: [],
45
- entities: EntityTables.make(entities),
46
- effects: Dict.make(),
47
- rollback: None,
48
- committedCheckpointId,
73
+ let chainMetaThrottler = {
74
+ let intervalMillis = Env.ThrottleWrites.chainMetadataIntervalMillis
75
+ Throttler.make(
76
+ ~intervalMillis,
77
+ ~logger=Logging.createChild(
78
+ ~params={
79
+ "context": "Throttler for chain metadata writes",
80
+ "intervalMillis": intervalMillis,
81
+ },
82
+ ),
83
+ )
84
+ }
85
+
86
+ {
87
+ allEntities: entities,
88
+ entities: EntityTables.make(entities),
89
+ effects: Dict.make(),
90
+ rollback: None,
91
+ committedCheckpointId,
92
+ processedCheckpointId: committedCheckpointId,
93
+ processedBatches: [],
94
+ writeFiber: None,
95
+ hasFailedWrite: false,
96
+ onError,
97
+ commitWaiters: [],
98
+ persistence,
99
+ config,
100
+ chainMeta: Dict.make(),
101
+ chainMetaDirty: false,
102
+ chainMetaThrottler,
103
+ }
49
104
  }
50
105
 
51
- // Once the store holds this many entities across all tables, we drop them
52
- // after a batch write so it doesn't grow unbounded on long running indexers.
53
- let keepLatestChangesLimit = 50_000.
106
+ // Max uncommitted entity/effect changes plus unwritten batch items before
107
+ // processing must wait for the cycle to free capacity.
108
+ let keepLatestChangesLimit = Env.inMemoryObjectsTarget
54
109
 
55
110
  let getEffectInMemTable = (inMemoryStore: t, ~effect: Internal.effect) => {
56
111
  let key = effect.name
@@ -60,6 +115,7 @@ let getEffectInMemTable = (inMemoryStore: t, ~effect: Internal.effect) => {
60
115
  let table = {
61
116
  idsToStore: [],
62
117
  dict: Dict.make(),
118
+ changesCount: 0.,
63
119
  invalidationsCount: 0,
64
120
  effect,
65
121
  }
@@ -68,6 +124,64 @@ let getEffectInMemTable = (inMemoryStore: t, ~effect: Internal.effect) => {
68
124
  }
69
125
  }
70
126
 
127
+ let getEffectOutput = (inMemTable: effectCacheInMemTable, key) =>
128
+ switch inMemTable.dict->Utils.Dict.dangerouslyGetNonOption(key) {
129
+ | Some(Set({entity: output})) => Some(output)
130
+ | Some(Delete(_)) | None => None
131
+ }
132
+
133
+ // Records a handler output. Persisted on the next write only when shouldCache;
134
+ // otherwise kept in memory (re-run on a later miss) but never written to the db.
135
+ let setEffectOutput = (
136
+ inMemTable: effectCacheInMemTable,
137
+ ~checkpointId,
138
+ ~cacheKey,
139
+ ~output,
140
+ ~shouldCache,
141
+ ) => {
142
+ switch inMemTable.dict->Utils.Dict.dangerouslyGetNonOption(cacheKey) {
143
+ | Some(_) => ()
144
+ | None => inMemTable.changesCount = inMemTable.changesCount +. 1.
145
+ }
146
+ inMemTable.dict->Dict.set(cacheKey, Set({entityId: cacheKey, entity: output, checkpointId}))
147
+ if shouldCache {
148
+ inMemTable.idsToStore->Array.push(cacheKey)->ignore
149
+ }
150
+ }
151
+
152
+ // Seeds an entry from a db read. Stamped with loadedFromDbCheckpointId so it's
153
+ // always droppable (re-readable from the db) and never re-persisted.
154
+ let initEffectOutputFromDb = (inMemTable: effectCacheInMemTable, ~cacheKey, ~output) =>
155
+ if inMemTable.dict->Utils.Dict.dangerouslyGetNonOption(cacheKey)->Option.isNone {
156
+ inMemTable.changesCount = inMemTable.changesCount +. 1.
157
+ inMemTable.dict->Dict.set(
158
+ cacheKey,
159
+ Set({entityId: cacheKey, entity: output, checkpointId: Internal.loadedFromDbCheckpointId}),
160
+ )
161
+ }
162
+
163
+ // Frees committed entries (re-readable from the db, or re-runnable for
164
+ // cache:false). Uncommitted entries stay warm. With keepLoadedFromDb, entries
165
+ // seeded from a db read are spared. Mirrors entity dropCommittedChanges.
166
+ let dropCommittedEffects = (
167
+ inMemTable: effectCacheInMemTable,
168
+ ~committedCheckpointId,
169
+ ~keepLoadedFromDb,
170
+ ) => {
171
+ let keysToDelete = []
172
+ inMemTable.dict->Utils.Dict.forEachWithKey((change, key) => {
173
+ let checkpointId = change->Change.getCheckpointId
174
+ if (
175
+ !(checkpointId > committedCheckpointId) &&
176
+ !(keepLoadedFromDb && checkpointId == Internal.loadedFromDbCheckpointId)
177
+ ) {
178
+ keysToDelete->Array.push(key)
179
+ }
180
+ })
181
+ keysToDelete->Array.forEach(key => inMemTable.dict->Utils.Dict.deleteInPlace(key))
182
+ inMemTable.changesCount = inMemTable.changesCount -. keysToDelete->Array.length->Int.toFloat
183
+ }
184
+
71
185
  let getInMemTable = (
72
186
  inMemoryStore: t,
73
187
  ~entityConfig: Internal.entityConfig,
@@ -77,139 +191,323 @@ let getInMemTable = (
77
191
 
78
192
  let isRollingBack = (inMemoryStore: t) => inMemoryStore.rollback !== None
79
193
 
80
- let writeBatch = async (
81
- inMemoryStore: t,
82
- ~persistence: Persistence.t,
83
- ~batch,
84
- ~config,
85
- ~isInReorgThreshold,
86
- ) =>
87
- switch persistence.storageStatus {
194
+ let getChangesCount = (inMemoryStore: t) => {
195
+ let total = ref(0.)
196
+ inMemoryStore.allEntities->Array.forEach(entityConfig => {
197
+ total := total.contents +. (inMemoryStore->getInMemTable(~entityConfig)).changesCount
198
+ })
199
+ inMemoryStore.effects->Utils.Dict.forEach(inMemTable => {
200
+ total := total.contents +. inMemTable.changesCount
201
+ })
202
+ inMemoryStore.processedBatches->Array.forEach(batch => {
203
+ total := total.contents +. batch.totalBatchSize->Int.toFloat
204
+ })
205
+ total.contents
206
+ }
207
+
208
+ let wakeCommitWaiters = (inMemoryStore: t) => {
209
+ let waiters = inMemoryStore.commitWaiters
210
+ inMemoryStore.commitWaiters = []
211
+ waiters->Array.forEach(resolve => resolve())
212
+ }
213
+
214
+ let waitForCommit = (inMemoryStore: t): promise<unit> =>
215
+ Promise.make((resolve, _) => {
216
+ inMemoryStore.commitWaiters->Array.push(resolve)->ignore
217
+ })
218
+
219
+ // Merges the leading run of batches sharing isInReorgThreshold into one batch;
220
+ // the rest stay queued for the next write. Caller guarantees processedBatches
221
+ // is non-empty.
222
+ let drainBatchRun = (inMemoryStore: t): Batch.t => {
223
+ let all = inMemoryStore.processedBatches
224
+ let isInReorgThreshold = (all->Array.getUnsafe(0)).isInReorgThreshold
225
+
226
+ let rest = []
227
+ let progressedChainsById = Dict.make()
228
+ let totalBatchSize = ref(0)
229
+ let items = []
230
+ let checkpointIds = []
231
+ let checkpointChainIds = []
232
+ let checkpointBlockNumbers = []
233
+ let checkpointBlockHashes = []
234
+ let checkpointEventsProcessed = []
235
+ all->Array.forEach(batch => {
236
+ // Once one batch lands in rest, all later ones follow it, preserving order.
237
+ if rest->Utils.Array.isEmpty && batch.isInReorgThreshold == isInReorgThreshold {
238
+ batch.progressedChainsById->Utils.Dict.forEachWithKey((chainAfterBatch, key) =>
239
+ progressedChainsById->Dict.set(key, chainAfterBatch)
240
+ )
241
+ totalBatchSize := totalBatchSize.contents + batch.totalBatchSize
242
+ items->Array.pushMany(batch.items)
243
+ checkpointIds->Array.pushMany(batch.checkpointIds)
244
+ checkpointChainIds->Array.pushMany(batch.checkpointChainIds)
245
+ checkpointBlockNumbers->Array.pushMany(batch.checkpointBlockNumbers)
246
+ checkpointBlockHashes->Array.pushMany(batch.checkpointBlockHashes)
247
+ checkpointEventsProcessed->Array.pushMany(batch.checkpointEventsProcessed)
248
+ } else {
249
+ rest->Array.push(batch)
250
+ }
251
+ })
252
+ inMemoryStore.processedBatches = rest
253
+
254
+ {
255
+ totalBatchSize: totalBatchSize.contents,
256
+ items,
257
+ progressedChainsById,
258
+ isInReorgThreshold,
259
+ checkpointIds,
260
+ checkpointChainIds,
261
+ checkpointBlockNumbers,
262
+ checkpointBlockHashes,
263
+ checkpointEventsProcessed,
264
+ }
265
+ }
266
+
267
+ // Captures the cache:true outputs to persist. The dict is left intact — entries
268
+ // stay warm and are reclaimed later by dropCommittedEffects once committed.
269
+ let snapshotEffects = (inMemoryStore: t, ~cache): array<Persistence.updatedEffectCache> => {
270
+ let acc = []
271
+ inMemoryStore.effects->Utils.Dict.forEach(inMemTable => {
272
+ let {idsToStore, dict, effect, invalidationsCount} = inMemTable
273
+ switch idsToStore {
274
+ | [] => ()
275
+ | ids =>
276
+ let items = ids->Array.filterMap((id): option<Internal.effectCacheItem> =>
277
+ switch dict->Dict.getUnsafe(id) {
278
+ | Set({entity: output}) => Some({id, output})
279
+ | Delete(_) => None
280
+ }
281
+ )
282
+ let effectName = effect.name
283
+ let effectCacheRecord = switch cache->Utils.Dict.dangerouslyGetNonOption(effectName) {
284
+ | Some(c) => c
285
+ | None =>
286
+ let c: Persistence.effectCacheRecord = {effectName, count: 0}
287
+ cache->Dict.set(effectName, c)
288
+ c
289
+ }
290
+ let shouldInitialize = effectCacheRecord.count === 0
291
+ effectCacheRecord.count = effectCacheRecord.count + items->Array.length - invalidationsCount
292
+ Prometheus.EffectCacheCount.set(~count=effectCacheRecord.count, ~effectName)
293
+ acc->Array.push(({effect, items, shouldInitialize}: Persistence.updatedEffectCache))->ignore
294
+ }
295
+ inMemTable.idsToStore = []
296
+ inMemTable.invalidationsCount = 0
297
+ })
298
+ acc
299
+ }
300
+
301
+ let runOneWrite = async (inMemoryStore: t, ~persistence: Persistence.t, ~config) => {
302
+ let cache = switch persistence.storageStatus {
88
303
  | Unknown
89
304
  | Initializing(_) =>
90
305
  JsError.throwWithMessage(`Failed to access the indexer storage. The Persistence layer is not initialized.`)
91
- | Ready({cache}) =>
306
+ | Ready({cache}) => cache
307
+ }
308
+
309
+ // Copy before the await: the batch write reads this later, in-transaction. A
310
+ // restage during the write re-dirties the flag and is rewritten next iteration.
311
+ let chainMetaData = if inMemoryStore.chainMetaDirty {
312
+ inMemoryStore.chainMetaDirty = false
313
+ Some(inMemoryStore.chainMeta->Utils.Dict.shallowCopy)
314
+ } else {
315
+ None
316
+ }
317
+
318
+ switch inMemoryStore.processedBatches {
319
+ | [] =>
320
+ // Metadata only: a cheap upsert, still serialized by the single write loop.
321
+ switch chainMetaData {
322
+ | Some(chainsData) =>
323
+ await persistence.storage.setChainMeta(chainsData)->Utils.Promise.ignoreValue
324
+ | None => ()
325
+ }
326
+ | _ =>
92
327
  let committedCheckpointId = inMemoryStore.committedCheckpointId
93
- // Decide before the keepMap below trims prevEntityChanges from changesCount,
94
- // so the signal still reflects every change currently held in memory.
95
- let keepLatestChanges = {
96
- let totalChanges = ref(0.)
97
- persistence.allEntities->Array.forEach(entityConfig => {
98
- totalChanges :=
99
- totalChanges.contents +. (inMemoryStore->getInMemTable(~entityConfig)).changesCount
100
- })
101
- totalChanges.contents < keepLatestChangesLimit
328
+ let batch = inMemoryStore->drainBatchRun
329
+ // The run's last checkpoint; entity changes above it stay queued for the next write.
330
+ let upToCheckpointId = switch batch.checkpointIds->Utils.Array.last {
331
+ | Some(checkpointId) => checkpointId
332
+ | None => committedCheckpointId
102
333
  }
334
+
335
+ let rollback = inMemoryStore.rollback
336
+ inMemoryStore.rollback = None
337
+
103
338
  let updatedEntities = persistence.allEntities->Array.filterMap(entityConfig => {
104
339
  let table = inMemoryStore->getInMemTable(~entityConfig)
105
-
106
- // The reset below drops prevEntityChanges and we reuse the array as the
107
- // write buffer here, so drop it from the count before appending to it.
108
- table.changesCount = table.changesCount -. table.prevEntityChanges->Array.length->Int.toFloat
109
- let changes = table.prevEntityChanges
110
- table.latestEntityChangeById->Utils.Dict.forEach(change =>
111
- if change->Change.getCheckpointId > committedCheckpointId {
112
- changes->Array.push(change)
113
- }
114
- )
340
+ let changes =
341
+ table->InMemoryTable.Entity.snapshotChanges(~committedCheckpointId, ~upToCheckpointId)
115
342
  if changes->Utils.Array.isEmpty {
116
343
  None
117
344
  } else {
118
345
  Some(({entityConfig, changes}: Persistence.updatedEntity))
119
346
  }
120
347
  })
348
+ let updatedEffectsCache = snapshotEffects(inMemoryStore, ~cache)
349
+
121
350
  await persistence.storage.writeBatch(
122
351
  ~batch,
123
- ~rawEvents=inMemoryStore.rawEvents,
124
- ~rollback=inMemoryStore.rollback,
125
- ~isInReorgThreshold,
352
+ ~rollback,
353
+ ~isInReorgThreshold=batch.isInReorgThreshold,
126
354
  ~config,
127
355
  ~allEntities=persistence.allEntities,
128
356
  ~updatedEntities,
129
- ~updatedEffectsCache={
130
- let acc = []
131
- inMemoryStore.effects->Utils.Dict.forEach(inMemTable => {
132
- let {idsToStore, dict, effect, invalidationsCount} = inMemTable
133
- switch idsToStore {
134
- | [] => ()
135
- | ids =>
136
- let items = ids->Array.map((id): Internal.effectCacheItem => {
137
- id,
138
- output: dict->Dict.getUnsafe(id),
139
- })
140
- let effectName = effect.name
141
- let effectCacheRecord = switch cache->Utils.Dict.dangerouslyGetNonOption(effectName) {
142
- | Some(c) => c
143
- | None =>
144
- let c: Persistence.effectCacheRecord = {effectName, count: 0}
145
- cache->Dict.set(effectName, c)
146
- c
147
- }
148
- let shouldInitialize = effectCacheRecord.count === 0
149
- effectCacheRecord.count =
150
- effectCacheRecord.count + items->Array.length - invalidationsCount
151
- Prometheus.EffectCacheCount.set(~count=effectCacheRecord.count, ~effectName)
152
- acc
153
- ->Array.push(({effect, items, shouldInitialize}: Persistence.updatedEffectCache))
154
- ->ignore
155
- }
156
- })
157
- acc
158
- },
357
+ ~updatedEffectsCache,
358
+ ~chainMetaData,
159
359
  )
160
360
 
161
- inMemoryStore.rawEvents = []
162
- inMemoryStore.effects = Dict.make()
163
- inMemoryStore.rollback = None
164
- inMemoryStore.committedCheckpointId = switch batch.checkpointIds->Utils.Array.last {
165
- | Some(checkpointId) => checkpointId
166
- | None => committedCheckpointId
361
+ inMemoryStore.committedCheckpointId = upToCheckpointId
362
+
363
+ switch rollback {
364
+ | Some({progressBlockNumberByChainId}) if RollbackCommit.callbacks->Utils.Array.notEmpty =>
365
+ await RollbackCommit.fire(~progressBlockNumberByChainId)
366
+ | _ => ()
167
367
  }
168
- if keepLatestChanges {
169
- persistence.allEntities->Array.forEach(entityConfig => {
170
- let table = inMemoryStore->getInMemTable(~entityConfig)
171
- inMemoryStore.entities->Dict.set(
172
- (entityConfig.name :> string),
173
- table->InMemoryTable.Entity.resetButKeepLatestChanges,
174
- )
175
- })
176
- } else {
177
- // Over the limit: drop everything written in a batch and keep only the
178
- // entities loaded from the db, so the next batch can still read them
179
- // without hitting the database.
180
- let loadedFromDbCount = ref(0.)
181
- let resetTables = persistence.allEntities->Array.map(entityConfig => {
182
- let resetTable =
183
- inMemoryStore
184
- ->getInMemTable(~entityConfig)
185
- ->InMemoryTable.Entity.resetButKeepLoadedFromDbChanges
186
- loadedFromDbCount := loadedFromDbCount.contents +. resetTable.changesCount
187
- resetTable
188
- })
189
- // Even the loaded-from-db entities alone exceed the limit, so there's no
190
- // point keeping them around - drop everything.
191
- let dropEverything = loadedFromDbCount.contents >= keepLatestChangesLimit
192
- persistence.allEntities->Array.forEachWithIndex((entityConfig, idx) => {
193
- inMemoryStore.entities->Dict.set(
194
- (entityConfig.name :> string),
195
- dropEverything ? InMemoryTable.Entity.make() : resetTables->Array.getUnsafe(idx),
196
- )
197
- })
368
+ }
369
+ }
370
+
371
+ let hasPendingWrite = (inMemoryStore: t) =>
372
+ inMemoryStore.processedBatches->Utils.Array.notEmpty || inMemoryStore.chainMetaDirty
373
+
374
+ let runWriteLoop = async (inMemoryStore: t) => {
375
+ while inMemoryStore->hasPendingWrite && !inMemoryStore.hasFailedWrite {
376
+ try {
377
+ await runOneWrite(
378
+ inMemoryStore,
379
+ ~persistence=inMemoryStore.persistence,
380
+ ~config=inMemoryStore.config,
381
+ )
382
+ inMemoryStore->wakeCommitWaiters
383
+ } catch {
384
+ | exn =>
385
+ inMemoryStore.hasFailedWrite = true
386
+ inMemoryStore.onError(exn)
387
+ }
388
+ }
389
+ inMemoryStore.writeFiber = None
390
+ inMemoryStore->wakeCommitWaiters
391
+ }
392
+
393
+ let kick = (inMemoryStore: t) =>
394
+ if (
395
+ inMemoryStore.writeFiber->Option.isNone &&
396
+ !inMemoryStore.hasFailedWrite &&
397
+ inMemoryStore->hasPendingWrite
398
+ ) {
399
+ inMemoryStore.writeFiber = Some(runWriteLoop(inMemoryStore))
400
+ }
401
+
402
+ let metaFieldsEqual = (a: InternalTable.Chains.metaFields, b: InternalTable.Chains.metaFields) =>
403
+ a.firstEventBlockNumber == b.firstEventBlockNumber &&
404
+ a.latestFetchedBlockNumber == b.latestFetchedBlockNumber &&
405
+ a.isHyperSync == b.isHyperSync &&
406
+ // Date is boxed; compare epoch ms.
407
+ a.timestampCaughtUpToHeadOrEndblock->Null.toOption->Option.map(Date.getTime) ==
408
+ b.timestampCaughtUpToHeadOrEndblock->Null.toOption->Option.map(Date.getTime)
409
+
410
+ // Stages chain metadata, dirtying only on a real change so restages are no-ops.
411
+ let setChainMeta = (inMemoryStore: t, chainsData: dict<InternalTable.Chains.metaFields>) => {
412
+ chainsData->Utils.Dict.forEachWithKey((meta, chainId) => {
413
+ let changed = switch inMemoryStore.chainMeta->Utils.Dict.dangerouslyGetNonOption(chainId) {
414
+ | Some(prev) => !metaFieldsEqual(meta, prev)
415
+ | None => true
416
+ }
417
+ if changed {
418
+ inMemoryStore.chainMeta->Dict.set(chainId, meta)
419
+ inMemoryStore.chainMetaDirty = true
198
420
  }
421
+ })
422
+ if inMemoryStore.chainMetaDirty {
423
+ inMemoryStore.chainMetaThrottler->Throttler.schedule(() => {
424
+ inMemoryStore->kick
425
+ Promise.resolve()
426
+ })
427
+ }
428
+ }
429
+
430
+ // Queues a processed batch and kicks the cycle. Returns immediately; the write
431
+ // happens off the processing path.
432
+ let commitBatch = (inMemoryStore: t, ~batch: Batch.t) => {
433
+ inMemoryStore.processedBatches->Array.push(batch)->ignore
434
+ switch batch.checkpointIds->Utils.Array.last {
435
+ | Some(checkpointId) => inMemoryStore.processedCheckpointId = checkpointId
436
+ | None => ()
199
437
  }
438
+ inMemoryStore->kick
439
+ }
440
+
441
+ // Drops committed entity and effect entries across all tables. With
442
+ // keepLoadedFromDb, entries seeded from a db read are spared.
443
+ let dropCommitted = (inMemoryStore: t, ~keepLoadedFromDb) => {
444
+ let committedCheckpointId = inMemoryStore.committedCheckpointId
445
+ inMemoryStore.allEntities->Array.forEach(entityConfig =>
446
+ inMemoryStore
447
+ ->getInMemTable(~entityConfig)
448
+ ->InMemoryTable.Entity.dropCommittedChanges(~committedCheckpointId, ~keepLoadedFromDb)
449
+ )
450
+ inMemoryStore.effects->Utils.Dict.forEach(inMemTable =>
451
+ inMemTable->dropCommittedEffects(~committedCheckpointId, ~keepLoadedFromDb)
452
+ )
453
+ }
454
+
455
+ // Blocks until the store holds fewer than keepLatestChangesLimit changes,
456
+ // freeing committed changes first and awaiting commits as a last resort.
457
+ let rec awaitCapacity = async (inMemoryStore: t) => {
458
+ // After a failed write nothing will free capacity, so bail instead of waiting
459
+ // on a commit that won't come (the error already went to onError).
460
+ if !inMemoryStore.hasFailedWrite && inMemoryStore->getChangesCount >= keepLatestChangesLimit {
461
+ // Drop committed writes first, sparing db-loaded entries (explicitly
462
+ // requested, so likelier to be read again).
463
+ inMemoryStore->dropCommitted(~keepLoadedFromDb=true)
464
+
465
+ // Still over: drop the db-loaded entries too.
466
+ if inMemoryStore->getChangesCount >= keepLatestChangesLimit {
467
+ inMemoryStore->dropCommitted(~keepLoadedFromDb=false)
468
+ }
469
+
470
+ // Still over: what's left is uncommitted. Only wait if a queued batch can
471
+ // free it; otherwise (e.g. a large rollback diff with no batch) waiting
472
+ // would deadlock, so let processing proceed.
473
+ if (
474
+ inMemoryStore->getChangesCount >= keepLatestChangesLimit &&
475
+ inMemoryStore.processedBatches->Utils.Array.notEmpty
476
+ ) {
477
+ inMemoryStore->kick
478
+ await inMemoryStore->waitForCommit
479
+ await inMemoryStore->awaitCapacity
480
+ }
481
+ }
482
+ }
483
+
484
+ // Awaits until everything processed is persisted. On a failed write we stop
485
+ // draining (onError already surfaced it) rather than throw.
486
+ let rec flush = async (inMemoryStore: t) => {
487
+ if !inMemoryStore.hasFailedWrite {
488
+ inMemoryStore->kick
489
+ switch inMemoryStore.writeFiber {
490
+ | Some(fiber) =>
491
+ await fiber
492
+ await inMemoryStore->flush
493
+ | None => ()
494
+ }
495
+ }
496
+ }
200
497
 
201
498
  let prepareRollbackDiff = async (
202
499
  inMemoryStore: t,
203
500
  ~persistence: Persistence.t,
204
501
  ~rollbackTargetCheckpointId,
205
502
  ~rollbackDiffCheckpointId,
503
+ ~progressBlockNumberByChainId,
206
504
  ) => {
207
- inMemoryStore.rawEvents = []
208
505
  inMemoryStore.entities = EntityTables.make(inMemoryStore.allEntities)
209
506
  inMemoryStore.effects = Dict.make()
210
507
  inMemoryStore.rollback = Some({
211
508
  targetCheckpointId: rollbackTargetCheckpointId,
212
509
  diffCheckpointId: rollbackDiffCheckpointId,
510
+ progressBlockNumberByChainId,
213
511
  })
214
512
 
215
513
  let deletedEntities = Dict.make()