envio 3.11.0 → 3.12.1
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 +3 -0
- package/package.json +6 -6
- package/src/Batch.res +111 -78
- package/src/Batch.res.mjs +53 -46
- package/src/BatchProcessing.res +1 -11
- package/src/BatchProcessing.res.mjs +1 -4
- package/src/ChainFetching.res +7 -3
- package/src/ChainFetching.res.mjs +1 -1
- package/src/ChainState.res +82 -20
- package/src/ChainState.res.mjs +51 -13
- package/src/ChainState.resi +6 -1
- package/src/Config.res +58 -37
- package/src/Config.res.mjs +63 -53
- package/src/CrossChainState.res +50 -53
- package/src/CrossChainState.res.mjs +25 -23
- package/src/CrossChainState.resi +6 -13
- package/src/EffectState.res +13 -4
- package/src/EffectState.resi +7 -1
- package/src/EventProcessing.res +6 -6
- package/src/EventProcessing.res.mjs +6 -6
- package/src/FetchState.res +65 -46
- package/src/FetchState.res.mjs +68 -36
- package/src/FinalizeBackfill.res +16 -12
- package/src/FinalizeBackfill.res.mjs +3 -3
- package/src/HistoryPolicy.res +46 -0
- package/src/HistoryPolicy.res.mjs +50 -0
- package/src/InMemoryStore.res +30 -32
- package/src/InMemoryStore.res.mjs +33 -28
- package/src/InMemoryTable.res +155 -67
- package/src/InMemoryTable.res.mjs +151 -60
- package/src/IndexerState.res +66 -45
- package/src/IndexerState.res.mjs +57 -29
- package/src/IndexerState.resi +8 -13
- package/src/Internal.res +4 -2
- package/src/LoadLayer.res +68 -38
- package/src/LoadLayer.res.mjs +50 -64
- package/src/LoadLayer.resi +1 -1
- package/src/Main.res +25 -24
- package/src/Main.res.mjs +24 -25
- package/src/Metrics.res +49 -1
- package/src/Metrics.res.mjs +26 -3
- package/src/Persistence.res +55 -16
- package/src/Persistence.res.mjs +18 -3
- package/src/PgStorage.res +286 -180
- package/src/PgStorage.res.mjs +209 -122
- package/src/PruneStaleHistory.res +9 -15
- package/src/PruneStaleHistory.res.mjs +11 -20
- package/src/Rollback.res +9 -14
- package/src/Rollback.res.mjs +5 -3
- package/src/Sink.res +7 -5
- package/src/Sink.res.mjs +4 -4
- package/src/Staging.res +298 -0
- package/src/Staging.res.mjs +257 -0
- package/src/TestIndexer.res +14 -30
- package/src/TestIndexer.res.mjs +11 -9
- package/src/UserContext.res +20 -36
- package/src/UserContext.res.mjs +6 -9
- package/src/Utils.res +10 -4
- package/src/Utils.res.mjs +16 -16
- package/src/Writing.res +34 -20
- package/src/Writing.res.mjs +15 -20
- package/src/bindings/ClickHouse.res +41 -34
- package/src/bindings/ClickHouse.res.mjs +36 -30
- package/src/bindings/ClickHouseSink.res +45 -186
- package/src/bindings/ClickHouseSink.res.mjs +21 -174
- package/src/db/CheckpointSequence.res +109 -0
- package/src/db/CheckpointSequence.res.mjs +99 -0
- package/src/db/EntityFilter.res +487 -275
- package/src/db/EntityFilter.res.mjs +557 -309
- package/src/db/EntityHistory.res +6 -5
- package/src/db/EntityHistory.res.mjs +5 -5
- package/src/db/Frontier.res +86 -0
- package/src/db/Frontier.res.mjs +126 -0
- package/src/db/InternalTable.res +145 -27
- package/src/db/InternalTable.res.mjs +115 -29
- package/src/db/RollbackFloors.res +34 -41
- package/src/db/RollbackFloors.res.mjs +27 -71
- package/src/db/Table.res +21 -6
- package/src/db/Table.res.mjs +13 -4
- package/src/sources/BlockStore.res +8 -0
- package/src/sources/EvmHyperSyncSource.res +5 -1
- package/src/sources/EvmHyperSyncSource.res.mjs +4 -3
- package/src/sources/FuelHyperSyncSource.res +1 -0
- package/src/sources/FuelHyperSyncSource.res.mjs +1 -1
- package/src/sources/HyperSync.res +6 -0
- package/src/sources/HyperSync.res.mjs +5 -2
- package/src/sources/HyperSync.resi +2 -0
- package/src/sources/HyperSyncClient.res +6 -0
- package/src/sources/HyperSyncSSE.res +1 -1
- package/src/sources/HyperSyncSSE.res.mjs +4 -10
- package/src/sources/RequestStat.res +8 -1
- package/src/sources/RpcSource.res +1 -0
- package/src/sources/RpcSource.res.mjs +1 -1
- package/src/sources/SimulateSource.res +1 -0
- package/src/sources/SimulateSource.res.mjs +1 -1
- package/src/sources/Source.res +11 -3
- package/src/sources/SourceManager.res +34 -13
- package/src/sources/SourceManager.res.mjs +26 -9
- package/src/sources/SourceManager.resi +2 -0
- package/src/sources/SvmHyperSyncClient.res +5 -0
- package/src/sources/SvmHyperSyncSource.res +3 -0
- package/src/sources/SvmHyperSyncSource.res.mjs +4 -2
- package/src/tui/Tui.res +56 -143
- package/src/tui/Tui.res.mjs +70 -166
- package/src/tui/components/TuiData.res +61 -16
- package/src/tui/components/TuiData.res.mjs +53 -15
- package/svm.schema.json +0 -7
- package/src/db/CheckpointBounds.res +0 -53
- package/src/db/CheckpointBounds.res.mjs +0 -44
package/src/LoadLayer.res
CHANGED
|
@@ -6,23 +6,6 @@ let scopeKeySuffix = (scope: Internal.chainScope) =>
|
|
|
6
6
|
| Chain(chainId) => `.${chainId->ChainId.toString}`
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
// Narrows a query to the scope's chain. Cross-chain entities have no chain-id
|
|
10
|
-
// column, so their filter is left untouched.
|
|
11
|
-
let scopeFilter = (filter: EntityFilter.t, ~table: Table.table, ~scope: Internal.chainScope) =>
|
|
12
|
-
switch (scope, table->Table.getChainIdField) {
|
|
13
|
-
| (Chain(chainId), Some(field)) =>
|
|
14
|
-
EntityFilter.And({
|
|
15
|
-
filters: [
|
|
16
|
-
filter,
|
|
17
|
-
Eq({
|
|
18
|
-
fieldName: field.fieldName,
|
|
19
|
-
fieldValue: chainId->(Utils.magic: ChainId.t => unknown),
|
|
20
|
-
}),
|
|
21
|
-
],
|
|
22
|
-
})
|
|
23
|
-
| _ => filter
|
|
24
|
-
}
|
|
25
|
-
|
|
26
9
|
let loadById = (
|
|
27
10
|
~loadManager,
|
|
28
11
|
~persistence: Persistence.t,
|
|
@@ -48,10 +31,10 @@ let loadById = (
|
|
|
48
31
|
(
|
|
49
32
|
await storage.loadOrThrow(
|
|
50
33
|
~table=entityConfig.table,
|
|
51
|
-
~filter=EntityFilter.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
34
|
+
~filter=EntityFilter.byIds(idsToLoad)->EntityFilter.scoped(
|
|
35
|
+
~table=entityConfig.table,
|
|
36
|
+
~scope,
|
|
37
|
+
),
|
|
55
38
|
)
|
|
56
39
|
)->(Utils.magic: array<unknown> => array<Internal.entity>)
|
|
57
40
|
} catch {
|
|
@@ -69,9 +52,10 @@ let loadById = (
|
|
|
69
52
|
let entity = dbEntities->Array.getUnsafe(idx)
|
|
70
53
|
entitiesMap->Dict.set(entity.id, entity)
|
|
71
54
|
}
|
|
55
|
+
let committedCheckpointId = indexerState->IndexerState.committedCheckpointIdFor(~scope)
|
|
72
56
|
idsToLoad->Array.forEach(entityId => {
|
|
73
57
|
inMemTable->InMemoryTable.Entity.initValue(
|
|
74
|
-
~committedCheckpointId
|
|
58
|
+
~committedCheckpointId,
|
|
75
59
|
~key=entityId,
|
|
76
60
|
~entity=entitiesMap->Utils.Dict.dangerouslyGetNonOption(entityId),
|
|
77
61
|
)
|
|
@@ -109,6 +93,7 @@ let callEffect = (
|
|
|
109
93
|
effect.handler(arg)
|
|
110
94
|
->Promise.thenResolve(output => {
|
|
111
95
|
inMemTable->InMemoryStore.setEffectOutput(
|
|
96
|
+
~chainId=arg.chainId,
|
|
112
97
|
~checkpointId=arg.checkpointId,
|
|
113
98
|
~cacheKey=arg.cacheKey,
|
|
114
99
|
~output,
|
|
@@ -271,15 +256,9 @@ let loadEffect = (
|
|
|
271
256
|
let {outputSchema} = effect.storageMeta
|
|
272
257
|
|
|
273
258
|
let dbEntities = try {
|
|
274
|
-
(
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
~filter=EntityFilter.In({
|
|
278
|
-
fieldName: Table.idFieldName,
|
|
279
|
-
fieldValue: idsToLoad->(Utils.magic: array<string> => array<unknown>),
|
|
280
|
-
}),
|
|
281
|
-
)
|
|
282
|
-
)->(Utils.magic: array<unknown> => array<Internal.effectCacheItem>)
|
|
259
|
+
(await storage.loadOrThrow(~table, ~filter=EntityFilter.byIds(idsToLoad)))->(
|
|
260
|
+
Utils.magic: array<unknown> => array<Internal.effectCacheItem>
|
|
261
|
+
)
|
|
283
262
|
} catch {
|
|
284
263
|
| exn =>
|
|
285
264
|
Ecosystem.getItemLogger(item, ~ecosystem)->Logging.childWarn({
|
|
@@ -294,7 +273,11 @@ let loadEffect = (
|
|
|
294
273
|
try {
|
|
295
274
|
let output = dbEntity.output->S.parseOrThrow(outputSchema)
|
|
296
275
|
idsFromCache->Utils.Set.add(dbEntity.id)->ignore
|
|
297
|
-
inMemTable->InMemoryStore.initEffectOutputFromDb(
|
|
276
|
+
inMemTable->InMemoryStore.initEffectOutputFromDb(
|
|
277
|
+
~chainId=item->Internal.getItemChainId,
|
|
278
|
+
~cacheKey=dbEntity.id,
|
|
279
|
+
~output,
|
|
280
|
+
)
|
|
298
281
|
} catch {
|
|
299
282
|
| S.Raised(error) =>
|
|
300
283
|
inMemTable->EffectState.recordInvalidation
|
|
@@ -349,7 +332,7 @@ let loadEffect = (
|
|
|
349
332
|
)
|
|
350
333
|
}
|
|
351
334
|
|
|
352
|
-
let
|
|
335
|
+
let loadByParsedFilter = (
|
|
353
336
|
~loadManager,
|
|
354
337
|
~persistence: Persistence.t,
|
|
355
338
|
~entityConfig: Internal.entityConfig,
|
|
@@ -372,14 +355,16 @@ let loadByFilter = (
|
|
|
372
355
|
|
|
373
356
|
let size = ref(0)
|
|
374
357
|
|
|
375
|
-
filters->Array.forEach(filter =>
|
|
358
|
+
filters->Array.forEach(filter =>
|
|
359
|
+
inMemTable->InMemoryTable.Entity.addEmptyIndex(~filter, ~table=entityConfig.table)
|
|
360
|
+
)
|
|
376
361
|
|
|
377
362
|
// Any non-derived field can be filtered on, so the columns this query reads
|
|
378
363
|
// are indexed on demand before it runs rather than promised by the schema.
|
|
379
364
|
// Inside the load timing: waiting on the build is time the handler spends
|
|
380
365
|
// waiting for this operation, and it's the only thing that explains an
|
|
381
366
|
// occasional very slow getWhere.
|
|
382
|
-
await storage.ensureQueryIndexes(~
|
|
367
|
+
await storage.ensureQueryIndexes(~entityConfig, ~scope, ~filters)
|
|
383
368
|
|
|
384
369
|
// Loading a superset of rows via a merged query is safe: every loaded
|
|
385
370
|
// entity is matched against all registered indexes, not only the
|
|
@@ -393,18 +378,23 @@ let loadByFilter = (
|
|
|
393
378
|
(
|
|
394
379
|
await storage.loadOrThrow(
|
|
395
380
|
~table=entityConfig.table,
|
|
396
|
-
~filter=filter->
|
|
381
|
+
~filter=filter->EntityFilter.scoped(~table=entityConfig.table, ~scope),
|
|
397
382
|
)
|
|
398
383
|
)->(Utils.magic: array<unknown> => array<Internal.entity>)
|
|
399
384
|
|
|
385
|
+
let committedCheckpointId = indexerState->IndexerState.committedCheckpointIdFor(~scope)
|
|
400
386
|
entities->Array.forEach(entity => {
|
|
401
387
|
inMemTable->InMemoryTable.Entity.initValue(
|
|
402
|
-
~committedCheckpointId
|
|
388
|
+
~committedCheckpointId,
|
|
403
389
|
~key=entity.id,
|
|
404
390
|
~entity=Some(entity),
|
|
405
391
|
)
|
|
406
392
|
})
|
|
407
393
|
|
|
394
|
+
// Every row this filter's values could match is now in the table, so a
|
|
395
|
+
// later getWhere naming any of them needs no round trip of its own.
|
|
396
|
+
inMemTable->InMemoryTable.Entity.recordLoadedValues(~filter, ~table=entityConfig.table)
|
|
397
|
+
|
|
408
398
|
size := size.contents + entities->Array.length
|
|
409
399
|
} catch {
|
|
410
400
|
| Persistence.StorageError({message, reason}) =>
|
|
@@ -435,13 +425,53 @@ let loadByFilter = (
|
|
|
435
425
|
)
|
|
436
426
|
}
|
|
437
427
|
|
|
428
|
+
// Keying an _in walks every value, so it's computed once here and handed to
|
|
429
|
+
// the load manager rather than recomputed by the hasher.
|
|
430
|
+
let filterKey = filter->EntityFilter.toString(~table=entityConfig.table)
|
|
431
|
+
|
|
432
|
+
if !(inMemTable->InMemoryTable.Entity.hasIndex)(filterKey) {
|
|
433
|
+
inMemTable->InMemoryTable.Entity.tryIndexFromLoadedValues(~filter, ~table=entityConfig.table)
|
|
434
|
+
}
|
|
435
|
+
|
|
438
436
|
loadManager->LoadManager.call(
|
|
439
437
|
~key,
|
|
440
438
|
~load,
|
|
441
439
|
~input=filter,
|
|
442
440
|
~shouldGroup,
|
|
443
|
-
~hasher=
|
|
441
|
+
~hasher=_ => filterKey,
|
|
444
442
|
~getUnsafeInMemory=inMemTable->InMemoryTable.Entity.getUnsafeOnIndex,
|
|
445
443
|
~hasInMemory=inMemTable->InMemoryTable.Entity.hasIndex,
|
|
446
444
|
)
|
|
447
445
|
}
|
|
446
|
+
|
|
447
|
+
let loadByFilter = (
|
|
448
|
+
~loadManager,
|
|
449
|
+
~persistence,
|
|
450
|
+
~entityConfig: Internal.entityConfig,
|
|
451
|
+
~scope,
|
|
452
|
+
~indexerState,
|
|
453
|
+
~shouldGroup,
|
|
454
|
+
~item,
|
|
455
|
+
~ecosystem,
|
|
456
|
+
~filter: dict<dict<unknown>>,
|
|
457
|
+
) =>
|
|
458
|
+
// Rejecting rather than throwing keeps a bad filter failing only its own
|
|
459
|
+
// call, even when the caller batches several with Promise.all.
|
|
460
|
+
try {
|
|
461
|
+
loadByParsedFilter(
|
|
462
|
+
~loadManager,
|
|
463
|
+
~persistence,
|
|
464
|
+
~entityConfig,
|
|
465
|
+
~scope,
|
|
466
|
+
~indexerState,
|
|
467
|
+
~shouldGroup,
|
|
468
|
+
~item,
|
|
469
|
+
~ecosystem,
|
|
470
|
+
~filter=filter->EntityFilter.parseOrThrow(
|
|
471
|
+
~entityName=entityConfig.name,
|
|
472
|
+
~table=entityConfig.table,
|
|
473
|
+
),
|
|
474
|
+
)
|
|
475
|
+
} catch {
|
|
476
|
+
| exn => Promise.reject(exn->Utils.prettifyExn)
|
|
477
|
+
}
|
package/src/LoadLayer.res.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
-
import * as Table from "./db/Table.res.mjs";
|
|
4
3
|
import * as Utils from "./Utils.res.mjs";
|
|
5
4
|
import * as ChainId from "./ChainId.res.mjs";
|
|
6
5
|
import * as Logging from "./Logging.res.mjs";
|
|
6
|
+
import * as Internal from "./Internal.res.mjs";
|
|
7
7
|
import * as Ecosystem from "./Ecosystem.res.mjs";
|
|
8
8
|
import * as EffectState from "./EffectState.res.mjs";
|
|
9
9
|
import * as LoadManager from "./LoadManager.res.mjs";
|
|
@@ -26,25 +26,6 @@ function scopeKeySuffix(scope) {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
function scopeFilter(filter, table, scope) {
|
|
30
|
-
let match = Table.getChainIdField(table);
|
|
31
|
-
if (scope === "crossChain" || match === undefined) {
|
|
32
|
-
return filter;
|
|
33
|
-
} else {
|
|
34
|
-
return {
|
|
35
|
-
operator: "and",
|
|
36
|
-
filters: [
|
|
37
|
-
filter,
|
|
38
|
-
{
|
|
39
|
-
operator: "=",
|
|
40
|
-
fieldName: match.fieldName,
|
|
41
|
-
fieldValue: scope
|
|
42
|
-
}
|
|
43
|
-
]
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
29
|
function loadById(loadManager, persistence, entityConfig, scope, indexerState, shouldGroup, item, ecosystem, entityId) {
|
|
49
30
|
let key = entityConfig.name + `.get` + scopeKeySuffix(scope);
|
|
50
31
|
let inMemTable = InMemoryStore.getInMemTable(indexerState, entityConfig, scope);
|
|
@@ -53,11 +34,7 @@ function loadById(loadManager, persistence, entityConfig, scope, indexerState, s
|
|
|
53
34
|
let timerRef = IndexerState.startStorageLoad(indexerState, storage.name, key);
|
|
54
35
|
let dbEntities;
|
|
55
36
|
try {
|
|
56
|
-
dbEntities = await storage.loadOrThrow(
|
|
57
|
-
operator: "in",
|
|
58
|
-
fieldName: Table.idFieldName,
|
|
59
|
-
fieldValue: idsToLoad
|
|
60
|
-
}, entityConfig.table, scope), entityConfig.table);
|
|
37
|
+
dbEntities = await storage.loadOrThrow(EntityFilter.scoped(EntityFilter.byIds(idsToLoad), entityConfig.table, scope), entityConfig.table);
|
|
61
38
|
} catch (raw_exn) {
|
|
62
39
|
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
63
40
|
if (exn.RE_EXN_ID === Persistence.StorageError) {
|
|
@@ -71,7 +48,8 @@ function loadById(loadManager, persistence, entityConfig, scope, indexerState, s
|
|
|
71
48
|
let entity = dbEntities[idx];
|
|
72
49
|
entitiesMap[entity.id] = entity;
|
|
73
50
|
}
|
|
74
|
-
|
|
51
|
+
let committedCheckpointId = IndexerState.committedCheckpointIdFor(indexerState, scope);
|
|
52
|
+
idsToLoad.forEach(entityId => InMemoryTable.Entity.initValue(inMemTable, committedCheckpointId, entityId, entitiesMap[entityId]));
|
|
75
53
|
return IndexerState.endStorageLoad(indexerState, timerRef, storage.name, key, idsToLoad.length, dbEntities.length);
|
|
76
54
|
};
|
|
77
55
|
return LoadManager.call(loadManager, entityId, key, load, LoadManager.noopHasher, shouldGroup, hash => hash in inMemTable.latestEntityChangeById, InMemoryTable.Entity.getUnsafe(inMemTable));
|
|
@@ -79,7 +57,7 @@ function loadById(loadManager, persistence, entityConfig, scope, indexerState, s
|
|
|
79
57
|
|
|
80
58
|
function callEffect(effect, arg, inMemTable, timerRef, onError) {
|
|
81
59
|
EffectState.startCall(inMemTable.stats, timerRef);
|
|
82
|
-
return effect.handler(arg).then(output => InMemoryStore.setEffectOutput(inMemTable, arg.checkpointId, arg.cacheKey, output, arg.context.cache)).catch(exn => onError(arg.cacheKey, exn)).finally(() => EffectState.endCall(inMemTable.stats, timerRef));
|
|
60
|
+
return effect.handler(arg).then(output => InMemoryStore.setEffectOutput(inMemTable, arg.chainId, arg.checkpointId, arg.cacheKey, output, arg.context.cache)).catch(exn => onError(arg.cacheKey, exn)).finally(() => EffectState.endCall(inMemTable.stats, timerRef));
|
|
83
61
|
}
|
|
84
62
|
|
|
85
63
|
function executeWithRateLimit(effect, effectArgs, inMemTable, onError, isFromQueue) {
|
|
@@ -155,11 +133,7 @@ function loadEffect(loadManager, persistence, effect, effectArgs, scope, indexer
|
|
|
155
133
|
let outputSchema = match$1.outputSchema;
|
|
156
134
|
let dbEntities;
|
|
157
135
|
try {
|
|
158
|
-
dbEntities = await storage.loadOrThrow(
|
|
159
|
-
operator: "in",
|
|
160
|
-
fieldName: Table.idFieldName,
|
|
161
|
-
fieldValue: idsToLoad
|
|
162
|
-
}, table);
|
|
136
|
+
dbEntities = await storage.loadOrThrow(EntityFilter.byIds(idsToLoad), table);
|
|
163
137
|
} catch (raw_exn) {
|
|
164
138
|
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
165
139
|
Logging.childWarn(Ecosystem.getItemLogger(item, ecosystem), {
|
|
@@ -173,7 +147,7 @@ function loadEffect(loadManager, persistence, effect, effectArgs, scope, indexer
|
|
|
173
147
|
try {
|
|
174
148
|
let output = S$RescriptSchema.parseOrThrow(dbEntity.output, outputSchema);
|
|
175
149
|
idsFromCache.add(dbEntity.id);
|
|
176
|
-
return InMemoryStore.initEffectOutputFromDb(inMemTable, dbEntity.id, output);
|
|
150
|
+
return InMemoryStore.initEffectOutputFromDb(inMemTable, Internal.getItemChainId(item), dbEntity.id, output);
|
|
177
151
|
} catch (raw_error) {
|
|
178
152
|
let error = Primitive_exceptions.internalToException(raw_error);
|
|
179
153
|
if (error.RE_EXN_ID === S$RescriptSchema.Raised) {
|
|
@@ -209,37 +183,49 @@ function loadEffect(loadManager, persistence, effect, effectArgs, scope, indexer
|
|
|
209
183
|
}
|
|
210
184
|
|
|
211
185
|
function loadByFilter(loadManager, persistence, entityConfig, scope, indexerState, shouldGroup, item, ecosystem, filter) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
let
|
|
216
|
-
let
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
return
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
186
|
+
try {
|
|
187
|
+
let filter$1 = EntityFilter.parseOrThrow(filter, entityConfig.name, entityConfig.table);
|
|
188
|
+
let key = EntityFilter.toOperationKey(filter$1, entityConfig.name) + scopeKeySuffix(scope);
|
|
189
|
+
let inMemTable = InMemoryStore.getInMemTable(indexerState, entityConfig, scope);
|
|
190
|
+
let load = async (filters, param) => {
|
|
191
|
+
let storage = Persistence.getInitializedStorageOrThrow(persistence);
|
|
192
|
+
let timerRef = IndexerState.startStorageLoad(indexerState, storage.name, key);
|
|
193
|
+
let size = {
|
|
194
|
+
contents: 0
|
|
195
|
+
};
|
|
196
|
+
filters.forEach(filter => InMemoryTable.Entity.addEmptyIndex(inMemTable, filter, entityConfig.table));
|
|
197
|
+
await storage.ensureQueryIndexes(entityConfig, scope, filters);
|
|
198
|
+
let queries = EntityFilter.merge(filters);
|
|
199
|
+
await Promise.all(queries.map(async filter => {
|
|
200
|
+
try {
|
|
201
|
+
let entities = await storage.loadOrThrow(EntityFilter.scoped(filter, entityConfig.table, scope), entityConfig.table);
|
|
202
|
+
let committedCheckpointId = IndexerState.committedCheckpointIdFor(indexerState, scope);
|
|
203
|
+
entities.forEach(entity => InMemoryTable.Entity.initValue(inMemTable, committedCheckpointId, entity.id, entity));
|
|
204
|
+
InMemoryTable.Entity.recordLoadedValues(inMemTable, filter, entityConfig.table);
|
|
205
|
+
size.contents = size.contents + entities.length | 0;
|
|
206
|
+
return;
|
|
207
|
+
} catch (raw_exn) {
|
|
208
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
209
|
+
if (exn.RE_EXN_ID === Persistence.StorageError) {
|
|
210
|
+
return ErrorHandling.mkLogAndRaise(Logging.createChildFrom(Ecosystem.getItemLogger(item, ecosystem), {
|
|
211
|
+
operation: key,
|
|
212
|
+
params: EntityFilter.getParams(filter)
|
|
213
|
+
}), exn.message, exn.reason);
|
|
214
|
+
}
|
|
215
|
+
throw exn;
|
|
236
216
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
217
|
+
}));
|
|
218
|
+
return IndexerState.endStorageLoad(indexerState, timerRef, storage.name, key, Stdlib_Array.reduce(queries, 0, (acc, query) => acc + EntityFilter.valuesCount(query) | 0), size.contents);
|
|
219
|
+
};
|
|
220
|
+
let filterKey = EntityFilter.toString(filter$1, entityConfig.table);
|
|
221
|
+
if (!InMemoryTable.Entity.hasIndex(inMemTable)(filterKey)) {
|
|
222
|
+
InMemoryTable.Entity.tryIndexFromLoadedValues(inMemTable, filter$1, entityConfig.table);
|
|
223
|
+
}
|
|
224
|
+
return LoadManager.call(loadManager, filter$1, key, load, param => filterKey, shouldGroup, InMemoryTable.Entity.hasIndex(inMemTable), InMemoryTable.Entity.getUnsafeOnIndex(inMemTable));
|
|
225
|
+
} catch (raw_exn) {
|
|
226
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
227
|
+
return Promise.reject(Utils.prettifyExn(exn));
|
|
228
|
+
}
|
|
243
229
|
}
|
|
244
230
|
|
|
245
231
|
export {
|
|
@@ -247,4 +233,4 @@ export {
|
|
|
247
233
|
loadByFilter,
|
|
248
234
|
loadEffect,
|
|
249
235
|
}
|
|
250
|
-
/*
|
|
236
|
+
/* Utils Not a pure module */
|
package/src/LoadLayer.resi
CHANGED
package/src/Main.res
CHANGED
|
@@ -488,7 +488,12 @@ let getGlobalIndexer = (): 'indexer => {
|
|
|
488
488
|
Utils.Proxy.make(Utils.Object.createNullObject(), traps)->(Utils.magic: {..} => 'indexer)
|
|
489
489
|
}
|
|
490
490
|
|
|
491
|
-
let startServer = (
|
|
491
|
+
let startServer = (
|
|
492
|
+
~getMetrics: unit => option<Metrics.t>,
|
|
493
|
+
~envioVersion: string,
|
|
494
|
+
~persistence: Persistence.t,
|
|
495
|
+
~isDevelopmentMode: bool,
|
|
496
|
+
) => {
|
|
492
497
|
open Express
|
|
493
498
|
|
|
494
499
|
let app = make()
|
|
@@ -520,10 +525,20 @@ let startServer = (~getState, ~persistence: Persistence.t, ~isDevelopmentMode: b
|
|
|
520
525
|
})
|
|
521
526
|
|
|
522
527
|
app->get("/console/state", (_req, res) => {
|
|
523
|
-
let state = if isDevelopmentMode {
|
|
524
|
-
getState()
|
|
525
|
-
} else {
|
|
528
|
+
let state = if !isDevelopmentMode {
|
|
526
529
|
Disabled({})
|
|
530
|
+
} else {
|
|
531
|
+
switch getMetrics() {
|
|
532
|
+
| None => Initializing({})
|
|
533
|
+
| Some(metrics) =>
|
|
534
|
+
Active({
|
|
535
|
+
envioVersion,
|
|
536
|
+
chains: metrics.chains->Array.map(toChainData),
|
|
537
|
+
indexerStartTime: metrics.startTime,
|
|
538
|
+
isPreRegisteringDynamicContracts: false,
|
|
539
|
+
rollbackOnReorg: metrics.rollbackEnabled,
|
|
540
|
+
})
|
|
541
|
+
}
|
|
527
542
|
}
|
|
528
543
|
|
|
529
544
|
res->json(state->S.reverseConvertToJsonOrThrow(stateSchema))
|
|
@@ -543,10 +558,7 @@ let startServer = (~getState, ~persistence: Persistence.t, ~isDevelopmentMode: b
|
|
|
543
558
|
|
|
544
559
|
app->get("/metrics", (_req, res) => {
|
|
545
560
|
res->set("Content-Type", Metrics.contentType)
|
|
546
|
-
let _ =
|
|
547
|
-
res->endWithData(
|
|
548
|
-
Metrics.collect(~metrics=getIndexerState()->Option.map(IndexerState.toMetrics)),
|
|
549
|
-
)
|
|
561
|
+
let _ = res->endWithData(Metrics.collect(~metrics=getMetrics()))
|
|
550
562
|
})
|
|
551
563
|
|
|
552
564
|
app->get("/metrics/runtime", (_req, res) => {
|
|
@@ -648,6 +660,7 @@ let start = async (
|
|
|
648
660
|
~resetCommand=isDevelopmentMode ? "envio dev -r" : "envio start -r",
|
|
649
661
|
~runCommand=Some(isDevelopmentMode ? "envio dev" : "envio start"),
|
|
650
662
|
~lowercaseAddresses=config.lowercaseAddresses,
|
|
663
|
+
~requireInitialized=config.isolated,
|
|
651
664
|
)
|
|
652
665
|
|
|
653
666
|
// Loads user handler files, which register handler/contractRegister/where
|
|
@@ -683,22 +696,10 @@ let start = async (
|
|
|
683
696
|
}
|
|
684
697
|
let envioVersion = Utils.EnvioPackage.value.version
|
|
685
698
|
|
|
699
|
+
let getMetrics = () => getIndexerState()->Option.map(IndexerState.toMetrics)
|
|
700
|
+
|
|
686
701
|
if !isTest {
|
|
687
|
-
startServer(~persistence, ~isDevelopmentMode, ~
|
|
688
|
-
switch getIndexerState() {
|
|
689
|
-
| None => Initializing({})
|
|
690
|
-
| Some(state) => {
|
|
691
|
-
let chains = (state->IndexerState.toMetrics).chains->Array.map(toChainData)
|
|
692
|
-
Active({
|
|
693
|
-
envioVersion,
|
|
694
|
-
chains,
|
|
695
|
-
indexerStartTime: state->IndexerState.indexerStartTime,
|
|
696
|
-
isPreRegisteringDynamicContracts: false,
|
|
697
|
-
rollbackOnReorg: config.shouldRollbackOnReorg,
|
|
698
|
-
})
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
)
|
|
702
|
+
startServer(~persistence, ~isDevelopmentMode, ~envioVersion, ~getMetrics)
|
|
702
703
|
}
|
|
703
704
|
|
|
704
705
|
let state = IndexerState.makeFromDbState(
|
|
@@ -712,7 +713,7 @@ let start = async (
|
|
|
712
713
|
~onError,
|
|
713
714
|
)
|
|
714
715
|
if shouldUseTui {
|
|
715
|
-
let _rerender = Tui.start(~
|
|
716
|
+
let _rerender = Tui.start(~config, ~getMetrics=() => state->IndexerState.toMetrics)
|
|
716
717
|
}
|
|
717
718
|
setIndexerState(state)
|
|
718
719
|
state->IndexerLoop.start
|
package/src/Main.res.mjs
CHANGED
|
@@ -402,7 +402,7 @@ function getGlobalIndexer() {
|
|
|
402
402
|
return new Proxy(Object.create(null), traps);
|
|
403
403
|
}
|
|
404
404
|
|
|
405
|
-
function startServer(
|
|
405
|
+
function startServer(getMetrics, envioVersion, persistence, isDevelopmentMode) {
|
|
406
406
|
let app = Express();
|
|
407
407
|
let consoleCorsMiddleware = (req, res, next) => {
|
|
408
408
|
let origin = req.headers["origin"];
|
|
@@ -425,9 +425,24 @@ function startServer(getState, persistence, isDevelopmentMode) {
|
|
|
425
425
|
res.sendStatus(200);
|
|
426
426
|
});
|
|
427
427
|
app.get("/console/state", (_req, res) => {
|
|
428
|
-
let state
|
|
428
|
+
let state;
|
|
429
|
+
if (isDevelopmentMode) {
|
|
430
|
+
let metrics = getMetrics();
|
|
431
|
+
state = metrics !== undefined ? ({
|
|
432
|
+
status: "active",
|
|
433
|
+
envioVersion: envioVersion,
|
|
434
|
+
chains: metrics.chains.map(toChainData),
|
|
435
|
+
indexerStartTime: metrics.startTime,
|
|
436
|
+
isPreRegisteringDynamicContracts: false,
|
|
437
|
+
rollbackOnReorg: metrics.rollbackEnabled
|
|
438
|
+
}) : ({
|
|
439
|
+
status: "initializing"
|
|
440
|
+
});
|
|
441
|
+
} else {
|
|
442
|
+
state = {
|
|
429
443
|
status: "disabled"
|
|
430
|
-
}
|
|
444
|
+
};
|
|
445
|
+
}
|
|
431
446
|
res.json(S$RescriptSchema.reverseConvertToJsonOrThrow(state, stateSchema));
|
|
432
447
|
});
|
|
433
448
|
app.post("/console/syncCache", (_req, res) => {
|
|
@@ -442,7 +457,7 @@ function startServer(getState, persistence, isDevelopmentMode) {
|
|
|
442
457
|
Metrics.startRuntimeCollectors();
|
|
443
458
|
app.get("/metrics", (_req, res) => {
|
|
444
459
|
res.set("Content-Type", Metrics.contentType);
|
|
445
|
-
res.end(Metrics.collect(
|
|
460
|
+
res.end(Metrics.collect(getMetrics()));
|
|
446
461
|
});
|
|
447
462
|
app.get("/metrics/runtime", (_req, res) => {
|
|
448
463
|
res.set("Content-Type", Metrics.contentType);
|
|
@@ -467,7 +482,7 @@ function getEnvioInfo() {
|
|
|
467
482
|
async function migrate(reset) {
|
|
468
483
|
let config = Config.load();
|
|
469
484
|
let persistence = PgStorage.makePersistenceFromConfig(config, undefined);
|
|
470
|
-
await Persistence.init(persistence, ChainMap.values(config.chainMap), config.contractMapping, Config.stripSensitiveData(Config.getPublicConfigJson()), "envio local db-migrate setup", undefined, reset, config.lowercaseAddresses, "Once");
|
|
485
|
+
await Persistence.init(persistence, ChainMap.values(config.chainMap), config.contractMapping, Config.stripSensitiveData(Config.getPublicConfigJson()), "envio local db-migrate setup", undefined, reset, config.lowercaseAddresses, undefined, "Once");
|
|
471
486
|
return await persistence.storage.close();
|
|
472
487
|
}
|
|
473
488
|
|
|
@@ -494,7 +509,7 @@ async function start(persistence, resetOpt, isTestOpt, exitAfterFirstEventBlockO
|
|
|
494
509
|
let isDevelopmentMode = !isTest && config.isDev;
|
|
495
510
|
let persistence$1 = persistence !== undefined ? persistence : PgStorage.makePersistenceFromConfig(config, undefined);
|
|
496
511
|
EnvioGlobal.value.persistence = Primitive_option.some(persistence$1);
|
|
497
|
-
await Persistence.init(persistence$1, ChainMap.values(config.chainMap), config.contractMapping, Config.stripSensitiveData(Config.getPublicConfigJson()), isDevelopmentMode ? "envio dev -r" : "envio start -r", isDevelopmentMode ? "envio dev" : "envio start", reset, config.lowercaseAddresses, undefined);
|
|
512
|
+
await Persistence.init(persistence$1, ChainMap.values(config.chainMap), config.contractMapping, Config.stripSensitiveData(Config.getPublicConfigJson()), isDevelopmentMode ? "envio dev -r" : "envio start -r", isDevelopmentMode ? "envio dev" : "envio start", reset, config.lowercaseAddresses, config.isolated, undefined);
|
|
498
513
|
let registrationsByChainId = await HandlerLoader.registerAllHandlers(config);
|
|
499
514
|
let config$1;
|
|
500
515
|
if (isTest) {
|
|
@@ -519,29 +534,13 @@ async function start(persistence, resetOpt, isTestOpt, exitAfterFirstEventBlockO
|
|
|
519
534
|
});
|
|
520
535
|
};
|
|
521
536
|
let envioVersion = Utils.EnvioPackage.value.version;
|
|
537
|
+
let getMetrics = () => Stdlib_Option.map(EnvioGlobal.value.indexerState, IndexerState.toMetrics);
|
|
522
538
|
if (!isTest) {
|
|
523
|
-
startServer(
|
|
524
|
-
let state = EnvioGlobal.value.indexerState;
|
|
525
|
-
if (state === undefined) {
|
|
526
|
-
return {
|
|
527
|
-
status: "initializing"
|
|
528
|
-
};
|
|
529
|
-
}
|
|
530
|
-
let state$1 = Primitive_option.valFromOption(state);
|
|
531
|
-
let chains = IndexerState.toMetrics(state$1).chains.map(toChainData);
|
|
532
|
-
return {
|
|
533
|
-
status: "active",
|
|
534
|
-
envioVersion: envioVersion,
|
|
535
|
-
chains: chains,
|
|
536
|
-
indexerStartTime: IndexerState.indexerStartTime(state$1),
|
|
537
|
-
isPreRegisteringDynamicContracts: false,
|
|
538
|
-
rollbackOnReorg: config$2.shouldRollbackOnReorg
|
|
539
|
-
};
|
|
540
|
-
}, persistence$1, isDevelopmentMode);
|
|
539
|
+
startServer(getMetrics, envioVersion, persistence$1, isDevelopmentMode);
|
|
541
540
|
}
|
|
542
541
|
let state = IndexerState.makeFromDbState(config$2, persistence$1, Persistence.getInitializedState(persistence$1), registrationsByChainId, isDevelopmentMode, shouldUseTui, exitAfterFirstEventBlock, undefined, undefined, onError, undefined);
|
|
543
542
|
if (shouldUseTui) {
|
|
544
|
-
Tui.start(() => state);
|
|
543
|
+
Tui.start(config$2, () => IndexerState.toMetrics(state));
|
|
545
544
|
}
|
|
546
545
|
EnvioGlobal.value.indexerState = Primitive_option.some(state);
|
|
547
546
|
IndexerLoop.start(state);
|
package/src/Metrics.res
CHANGED
|
@@ -20,6 +20,7 @@ type chainMetrics = {
|
|
|
20
20
|
// Raw committed progress (may be -1), unlike the optional latestProcessedBlock.
|
|
21
21
|
progressBlockNumber: int,
|
|
22
22
|
progressLatencyMs: option<int>,
|
|
23
|
+
progressBlockTime: option<int>,
|
|
23
24
|
concurrency: int,
|
|
24
25
|
partitionsCount: int,
|
|
25
26
|
bufferSize: int,
|
|
@@ -36,8 +37,18 @@ type chainMetrics = {
|
|
|
36
37
|
reorgCount: int,
|
|
37
38
|
reorgDetectedBlock: option<int>,
|
|
38
39
|
rollbackTargetBlock: option<int>,
|
|
40
|
+
rateLimitTimeMs: float,
|
|
41
|
+
rateLimitResetInMs: option<float>,
|
|
39
42
|
}
|
|
40
43
|
|
|
44
|
+
// Mirrors `ChainState.hasProcessedToEndblock`, which is what clamps
|
|
45
|
+
// `knownHeight` to the end block — the two must agree.
|
|
46
|
+
let hasProcessedToEndblock = (m: chainMetrics) =>
|
|
47
|
+
switch m.endBlock {
|
|
48
|
+
| Some(endBlock) => m.progressBlockNumber >= endBlock
|
|
49
|
+
| None => false
|
|
50
|
+
}
|
|
51
|
+
|
|
41
52
|
type handlerMetrics = {
|
|
42
53
|
contract: string,
|
|
43
54
|
event: string,
|
|
@@ -90,6 +101,10 @@ type sourceRequestMetrics = {
|
|
|
90
101
|
method: string,
|
|
91
102
|
count: int,
|
|
92
103
|
seconds: float,
|
|
104
|
+
// None for methods whose responses aren't measured in blocks; they render no
|
|
105
|
+
// block series at all.
|
|
106
|
+
responseBlocks: option<int>,
|
|
107
|
+
emptyResponseCount: int,
|
|
93
108
|
}
|
|
94
109
|
|
|
95
110
|
type sourceHeightMetrics = {
|
|
@@ -249,7 +264,16 @@ let renderMetrics = (b: builder, metrics: t) => {
|
|
|
249
264
|
| Some(existing) =>
|
|
250
265
|
byLabels->Dict.set(
|
|
251
266
|
labels,
|
|
252
|
-
{
|
|
267
|
+
{
|
|
268
|
+
...existing,
|
|
269
|
+
count: existing.count + s.count,
|
|
270
|
+
seconds: existing.seconds +. s.seconds,
|
|
271
|
+
responseBlocks: switch (existing.responseBlocks, s.responseBlocks) {
|
|
272
|
+
| (Some(a), Some(b)) => Some(a + b)
|
|
273
|
+
| (some, None) | (None, some) => some
|
|
274
|
+
},
|
|
275
|
+
emptyResponseCount: existing.emptyResponseCount + s.emptyResponseCount,
|
|
276
|
+
},
|
|
253
277
|
)
|
|
254
278
|
| None => byLabels->Dict.set(labels, s)
|
|
255
279
|
}
|
|
@@ -509,6 +533,23 @@ let renderMetrics = (b: builder, metrics: t) => {
|
|
|
509
533
|
~entries=sourceRequests,
|
|
510
534
|
~value=s => s.seconds !== 0. ? Some(s.seconds) : None,
|
|
511
535
|
)
|
|
536
|
+
b->seriesOpt(
|
|
537
|
+
~name="envio_source_response_blocks_total",
|
|
538
|
+
~help="The number of blocks a data source returned, summed over its responses. Counted as the source sent them, before the indexer drops the blocks no event needs. Only sources whose responses are measured in blocks report it.",
|
|
539
|
+
~kind="counter",
|
|
540
|
+
~entries=sourceRequests,
|
|
541
|
+
~value=s => s.responseBlocks->Option.map(Int.toFloat),
|
|
542
|
+
)
|
|
543
|
+
// Rendered flat at zero for any method that reports blocks: a chain scanning
|
|
544
|
+
// ranges it finds nothing in is the reading this exists for, and a series
|
|
545
|
+
// that only appears once the first empty response lands can't be alerted on.
|
|
546
|
+
b->seriesOpt(
|
|
547
|
+
~name="envio_source_response_empty_total",
|
|
548
|
+
~help="The number of responses that came back with no blocks at all — a range the source scanned and matched nothing in. Compare against envio_source_request_total for the share of requests that returned nothing.",
|
|
549
|
+
~kind="counter",
|
|
550
|
+
~entries=sourceRequests,
|
|
551
|
+
~value=s => s.responseBlocks->Option.map(_ => s.emptyResponseCount->Int.toFloat),
|
|
552
|
+
)
|
|
512
553
|
if heightStreamConnects->Array.length > 0 {
|
|
513
554
|
b->series(
|
|
514
555
|
~name="envio_source_height_stream_connects_total",
|
|
@@ -619,6 +660,13 @@ let renderMetrics = (b: builder, metrics: t) => {
|
|
|
619
660
|
~entries=chains,
|
|
620
661
|
~value=m => m.numEventsProcessed,
|
|
621
662
|
)
|
|
663
|
+
b->seriesOpt(
|
|
664
|
+
~name="envio_progress_block_time_seconds",
|
|
665
|
+
~help="Unix timestamp of the block the chain has processed up to. Subtract it from the scrape time for how far behind chain time the indexer is, which stays honest when the data source itself is behind the chain. Best effort in realtime mode, absent during backfill.",
|
|
666
|
+
~kind="gauge",
|
|
667
|
+
~entries=chains,
|
|
668
|
+
~value=m => m.progressBlockTime->Option.map(Int.toFloat),
|
|
669
|
+
)
|
|
622
670
|
b->seriesOpt(
|
|
623
671
|
~name="envio_progress_latency",
|
|
624
672
|
~help="The latency in milliseconds between the latest processed event creation and the time it was written to storage.",
|