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.
- package/index.d.ts +12 -0
- package/package.json +6 -6
- package/src/Batch.res +7 -1
- package/src/Batch.res.mjs +2 -1
- package/src/ChainManager.res +3 -2
- package/src/ChainManager.res.mjs +3 -3
- package/src/Env.res +6 -0
- package/src/Env.res.mjs +3 -0
- package/src/EventProcessing.res +24 -122
- package/src/EventProcessing.res.mjs +24 -88
- package/src/GlobalState.res +31 -52
- package/src/GlobalState.res.mjs +54 -33
- package/src/GlobalStateManager.res +1 -3
- package/src/InMemoryStore.res +408 -110
- package/src/InMemoryStore.res.mjs +335 -84
- package/src/InMemoryTable.res +49 -30
- package/src/InMemoryTable.res.mjs +31 -39
- package/src/Internal.res +3 -0
- package/src/LoadLayer.res +9 -7
- package/src/LoadLayer.res.mjs +4 -10
- package/src/Main.res +31 -2
- package/src/Main.res.mjs +19 -5
- package/src/Persistence.res +6 -1
- package/src/PgStorage.res +171 -68
- package/src/PgStorage.res.mjs +125 -39
- package/src/RollbackCommit.res +32 -0
- package/src/RollbackCommit.res.mjs +35 -0
- package/src/TestIndexerProxyStorage.res +1 -1
- package/src/TestIndexerProxyStorage.res.mjs +1 -1
- package/src/Throttler.res +22 -15
- package/src/Throttler.res.mjs +19 -14
- package/src/UserContext.res +1 -0
- package/src/UserContext.res.mjs +3 -1
- package/src/bindings/NodeJs.res +1 -0
package/src/InMemoryStore.res
CHANGED
|
@@ -21,36 +21,91 @@ module EntityTables = {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
type effectCacheInMemTable = {
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
//
|
|
52
|
-
//
|
|
53
|
-
let keepLatestChangesLimit =
|
|
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
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
94
|
-
//
|
|
95
|
-
let
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
~
|
|
124
|
-
~
|
|
125
|
-
~isInReorgThreshold,
|
|
352
|
+
~rollback,
|
|
353
|
+
~isInReorgThreshold=batch.isInReorgThreshold,
|
|
126
354
|
~config,
|
|
127
355
|
~allEntities=persistence.allEntities,
|
|
128
356
|
~updatedEntities,
|
|
129
|
-
~updatedEffectsCache
|
|
130
|
-
|
|
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.
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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()
|