envio 3.3.0-alpha.8 → 3.3.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/index.d.ts +19 -0
  2. package/licenses/CLA.md +35 -0
  3. package/licenses/EULA.md +67 -0
  4. package/licenses/LICENSE.md +45 -0
  5. package/licenses/README.md +35 -0
  6. package/package.json +9 -8
  7. package/src/Batch.res.mjs +1 -1
  8. package/src/BatchProcessing.res +0 -7
  9. package/src/BatchProcessing.res.mjs +1 -8
  10. package/src/ChainFetching.res +30 -18
  11. package/src/ChainFetching.res.mjs +23 -13
  12. package/src/ChainState.res +205 -59
  13. package/src/ChainState.res.mjs +130 -27
  14. package/src/ChainState.resi +16 -1
  15. package/src/Config.res +9 -5
  16. package/src/Config.res.mjs +2 -2
  17. package/src/Core.res +20 -0
  18. package/src/Core.res.mjs +12 -0
  19. package/src/CrossChainState.res +147 -39
  20. package/src/CrossChainState.res.mjs +42 -10
  21. package/src/EffectState.res +100 -0
  22. package/src/EffectState.res.mjs +74 -0
  23. package/src/EffectState.resi +32 -0
  24. package/src/Envio.res +25 -7
  25. package/src/Envio.res.mjs +15 -19
  26. package/src/EnvioGlobal.res +53 -0
  27. package/src/EnvioGlobal.res.mjs +31 -0
  28. package/src/EventConfigBuilder.res +30 -56
  29. package/src/EventConfigBuilder.res.mjs +24 -30
  30. package/src/EventProcessing.res +8 -5
  31. package/src/EventProcessing.res.mjs +2 -1
  32. package/src/FetchState.res +842 -474
  33. package/src/FetchState.res.mjs +545 -356
  34. package/src/HandlerLoader.res +1 -1
  35. package/src/HandlerLoader.res.mjs +1 -1
  36. package/src/HandlerRegister.res +478 -305
  37. package/src/HandlerRegister.res.mjs +277 -209
  38. package/src/HandlerRegister.resi +11 -6
  39. package/src/InMemoryStore.res +14 -26
  40. package/src/InMemoryStore.res.mjs +6 -19
  41. package/src/IndexerState.res +15 -39
  42. package/src/IndexerState.res.mjs +18 -29
  43. package/src/IndexerState.resi +2 -10
  44. package/src/Internal.res +121 -18
  45. package/src/Internal.res.mjs +96 -2
  46. package/src/LoadLayer.res +44 -24
  47. package/src/LoadLayer.res.mjs +43 -20
  48. package/src/LoadLayer.resi +1 -0
  49. package/src/LogSelection.res +92 -217
  50. package/src/LogSelection.res.mjs +95 -184
  51. package/src/Main.res +29 -143
  52. package/src/Main.res.mjs +25 -88
  53. package/src/Metrics.res +1 -1
  54. package/src/Metrics.res.mjs +1 -1
  55. package/src/Persistence.res +12 -3
  56. package/src/PgStorage.res +155 -82
  57. package/src/PgStorage.res.mjs +130 -77
  58. package/src/Prometheus.res +20 -10
  59. package/src/Prometheus.res.mjs +22 -10
  60. package/src/PruneStaleHistory.res +146 -35
  61. package/src/PruneStaleHistory.res.mjs +114 -20
  62. package/src/RawEvent.res +2 -2
  63. package/src/Rollback.res +32 -13
  64. package/src/Rollback.res.mjs +24 -11
  65. package/src/RollbackCommit.res +4 -1
  66. package/src/RollbackCommit.res.mjs +3 -2
  67. package/src/SimulateDeadInputTracker.res +1 -1
  68. package/src/SimulateItems.res +39 -7
  69. package/src/SimulateItems.res.mjs +28 -9
  70. package/src/TestIndexer.res +2 -2
  71. package/src/TestIndexer.res.mjs +2 -2
  72. package/src/TopicFilter.res +1 -25
  73. package/src/TopicFilter.res.mjs +0 -74
  74. package/src/UserContext.res +62 -23
  75. package/src/UserContext.res.mjs +26 -6
  76. package/src/Writing.res +60 -21
  77. package/src/Writing.res.mjs +27 -9
  78. package/src/bindings/NodeJs.res +5 -0
  79. package/src/bindings/Viem.res +0 -5
  80. package/src/sources/EventRouter.res +0 -21
  81. package/src/sources/EventRouter.res.mjs +0 -12
  82. package/src/sources/Evm.res +4 -1
  83. package/src/sources/Evm.res.mjs +1 -1
  84. package/src/sources/EvmChain.res +2 -27
  85. package/src/sources/EvmChain.res.mjs +2 -23
  86. package/src/sources/EvmRpcClient.res +58 -23
  87. package/src/sources/EvmRpcClient.res.mjs +11 -5
  88. package/src/sources/Fuel.res +3 -1
  89. package/src/sources/Fuel.res.mjs +1 -1
  90. package/src/sources/HyperSync.res +9 -38
  91. package/src/sources/HyperSync.res.mjs +16 -28
  92. package/src/sources/HyperSync.resi +2 -2
  93. package/src/sources/HyperSyncClient.res +88 -11
  94. package/src/sources/HyperSyncClient.res.mjs +39 -6
  95. package/src/sources/HyperSyncSource.res +18 -210
  96. package/src/sources/HyperSyncSource.res.mjs +9 -137
  97. package/src/sources/Rpc.res +0 -32
  98. package/src/sources/Rpc.res.mjs +1 -46
  99. package/src/sources/RpcSource.res +129 -533
  100. package/src/sources/RpcSource.res.mjs +161 -379
  101. package/src/sources/SimulateSource.res +37 -19
  102. package/src/sources/SimulateSource.res.mjs +27 -10
  103. package/src/sources/SourceManager.res +4 -14
  104. package/src/sources/SourceManager.res.mjs +3 -9
  105. package/src/sources/SourceManager.resi +0 -2
  106. package/src/sources/SvmHyperSyncSource.res +13 -3
  107. package/src/sources/SvmHyperSyncSource.res.mjs +1 -1
@@ -493,15 +493,18 @@ function makeSchemaTableNamesQuery(pgSchema) {
493
493
  return `SELECT table_name FROM information_schema.tables WHERE table_schema = '` + pgSchema + `';`;
494
494
  }
495
495
 
496
- let cacheTablePrefixLength = Internal.cacheTablePrefix.length;
497
-
498
496
  function makeSchemaCacheTableInfoQuery(pgSchema) {
499
- return `SELECT
497
+ return `SELECT
500
498
  t.table_name,
501
499
  ` + getCacheRowCountFnName + `(t.table_name) as count
502
500
  FROM information_schema.tables t
503
- WHERE t.table_schema = '` + pgSchema + `'
504
- AND t.table_name LIKE '` + Internal.cacheTablePrefix + `%';`;
501
+ WHERE t.table_schema = '` + pgSchema + `'
502
+ AND t.table_name ~ '^envio_([0-9]+_)?effect_.+'
503
+ AND (
504
+ SELECT array_agg(c.column_name::text ORDER BY c.column_name::text)
505
+ FROM information_schema.columns c
506
+ WHERE c.table_schema = t.table_schema AND c.table_name = t.table_name
507
+ ) = ARRAY['id', 'output'];`;
505
508
  }
506
509
 
507
510
  let psqlExecState = {
@@ -766,7 +769,7 @@ async function writeBatch(sql, batch, pgSchema, rollback, isInReorgThreshold, co
766
769
  }
767
770
  throw exn;
768
771
  }),
769
- Promise.all(updatedEffectsCache.map(param => setEffectCacheOrThrow(param.effect, param.items, param.shouldInitialize)))
772
+ Promise.all(updatedEffectsCache.map(param => setEffectCacheOrThrow(param.table, param.itemSchema, param.items, param.shouldInitialize)))
770
773
  ]);
771
774
  let specificError$1 = specificError.contents;
772
775
  if (specificError$1 === undefined) {
@@ -789,7 +792,7 @@ async function writeBatch(sql, batch, pgSchema, rollback, isInReorgThreshold, co
789
792
  }
790
793
  }
791
794
 
792
- function makeGetRollbackRestoredEntitiesQuery(entityConfig, pgSchema) {
795
+ function makeGetRollbackPreTargetRowsQuery(entityConfig, pgSchema) {
793
796
  let dataFieldNames = Stdlib_Array.filterMap(entityConfig.table.fields, fieldOrDerived => {
794
797
  if (fieldOrDerived.TAG === "Field") {
795
798
  return Table.getPgDbFieldName(fieldOrDerived._0);
@@ -797,31 +800,36 @@ function makeGetRollbackRestoredEntitiesQuery(entityConfig, pgSchema) {
797
800
  });
798
801
  let dataFieldsCommaSeparated = dataFieldNames.map(name => `"` + name + `"`).join(", ");
799
802
  let historyTableName = EntityHistory.historyTableName(entityConfig.name, entityConfig.index);
800
- return `SELECT DISTINCT ON (` + Table.idFieldName + `) ` + dataFieldsCommaSeparated + `
801
- FROM "` + pgSchema + `"."` + historyTableName + `"
802
- WHERE "` + EntityHistory.checkpointIdFieldName + `" <= $1
803
- AND EXISTS (
804
- SELECT 1
805
- FROM "` + pgSchema + `"."` + historyTableName + `" h
806
- WHERE h.` + Table.idFieldName + ` = "` + historyTableName + `".` + Table.idFieldName + `
807
- AND h."` + EntityHistory.checkpointIdFieldName + `" > $1
808
- )
809
- ORDER BY ` + Table.idFieldName + `, "` + EntityHistory.checkpointIdFieldName + `" DESC`;
803
+ return `SELECT DISTINCT ON ("` + Table.idFieldName + `") ` + dataFieldsCommaSeparated + `, "` + EntityHistory.changeFieldName + `"
804
+ FROM "` + pgSchema + `"."` + historyTableName + `"
805
+ WHERE "` + EntityHistory.checkpointIdFieldName + `" <= $1
806
+ AND EXISTS (
807
+ SELECT 1
808
+ FROM "` + pgSchema + `"."` + historyTableName + `" h
809
+ WHERE h."` + Table.idFieldName + `" = "` + historyTableName + `"."` + Table.idFieldName + `"
810
+ AND h."` + EntityHistory.checkpointIdFieldName + `" > $1
811
+ )
812
+ ORDER BY "` + Table.idFieldName + `", "` + EntityHistory.checkpointIdFieldName + `" DESC`;
810
813
  }
811
814
 
812
815
  function makeGetRollbackRemovedIdsQuery(entityConfig, pgSchema) {
813
816
  let historyTableName = EntityHistory.historyTableName(entityConfig.name, entityConfig.index);
814
- return `SELECT DISTINCT ` + Table.idFieldName + `
815
- FROM "` + pgSchema + `"."` + historyTableName + `"
816
- WHERE "` + EntityHistory.checkpointIdFieldName + `" > $1
817
- AND NOT EXISTS (
818
- SELECT 1
819
- FROM "` + pgSchema + `"."` + historyTableName + `" h
820
- WHERE h.` + Table.idFieldName + ` = "` + historyTableName + `".` + Table.idFieldName + `
821
- AND h."` + EntityHistory.checkpointIdFieldName + `" <= $1
822
- )`;
817
+ return `SELECT DISTINCT "` + Table.idFieldName + `"
818
+ FROM "` + pgSchema + `"."` + historyTableName + `"
819
+ WHERE "` + EntityHistory.checkpointIdFieldName + `" > $1
820
+ AND NOT EXISTS (
821
+ SELECT 1
822
+ FROM "` + pgSchema + `"."` + historyTableName + `" h
823
+ WHERE h."` + Table.idFieldName + `" = "` + historyTableName + `"."` + Table.idFieldName + `"
824
+ AND h."` + EntityHistory.checkpointIdFieldName + `" <= $1
825
+ )`;
823
826
  }
824
827
 
828
+ let rollbackRowStateSchema = S$RescriptSchema.object(s => [
829
+ s.f(Table.idFieldName, S$RescriptSchema.string),
830
+ s.f(EntityHistory.changeFieldName, EntityHistory.RowAction.schema)
831
+ ]);
832
+
825
833
  function make(sql, pgHost, pgSchema, pgPort, pgUser, pgDatabase, pgPassword, isHasuraEnabled, sink, onInitialize, onNewTables) {
826
834
  let containerName = "envio-postgres";
827
835
  let psqlExecOptions_env = Object.fromEntries([
@@ -839,32 +847,60 @@ function make(sql, pgHost, pgSchema, pgPort, pgUser, pgDatabase, pgPassword, isH
839
847
  };
840
848
  let cacheDirPath = Path.resolve(".envio", "cache");
841
849
  let isInitialized = async () => Utils.$$Array.notEmpty(await sql.unsafe(`SELECT table_schema FROM information_schema.tables WHERE table_schema = '` + pgSchema + `' AND (table_name = '` + "event_sync_state" + `' OR table_name = '` + InternalTable.Chains.table.tableName + `');`));
850
+ let scanCacheDir = async () => {
851
+ let topEntries;
852
+ try {
853
+ topEntries = await Fs.promises.readdir(cacheDirPath);
854
+ } catch (exn) {
855
+ topEntries = [];
856
+ }
857
+ let result = [];
858
+ await Promise.all(topEntries.map(async entry => {
859
+ let entryPath = Path.join(cacheDirPath, entry);
860
+ let isDir = (await Fs.promises.stat(entryPath)).isDirectory();
861
+ if (isDir) {
862
+ let subEntries = await Fs.promises.readdir(entryPath);
863
+ let tsvs = subEntries.filter(sub => sub.endsWith(".tsv"));
864
+ let chainId = Internal.EffectCache.parseChainId(entry);
865
+ if (chainId !== undefined) {
866
+ tsvs.forEach(sub => {
867
+ let effectName = sub.slice(0, -4);
868
+ let table = Internal.makeCacheTable(effectName, chainId);
869
+ result.push([
870
+ table,
871
+ Path.join(entryPath, sub)
872
+ ]);
873
+ });
874
+ return;
875
+ } else if (Utils.$$Array.notEmpty(tsvs)) {
876
+ return Stdlib_JsError.throwWithMessage(`Invalid effect cache directory ".envio/cache/` + entry + `". Chain cache directories must be named by a numeric chain id (e.g. "1"). Found cache files: ` + tsvs.join(", ") + `.`);
877
+ } else {
878
+ return;
879
+ }
880
+ }
881
+ if (!entry.endsWith(".tsv")) {
882
+ return;
883
+ }
884
+ let effectName = entry.slice(0, -4);
885
+ let table = Internal.makeCacheTable(effectName, "crossChain");
886
+ result.push([
887
+ table,
888
+ entryPath
889
+ ]);
890
+ }));
891
+ return result;
892
+ };
842
893
  let restoreEffectCache = async withUpload => {
843
894
  if (withUpload) {
844
- let nothingToUploadErrorMessage = "Nothing to upload.";
845
- let match = await Promise.all([
846
- Stdlib_Promise.$$catch(Fs.promises.readdir(cacheDirPath).then(e => ({
847
- TAG: "Ok",
848
- _0: e
849
- })), param => Promise.resolve({
850
- TAG: "Error",
851
- _0: nothingToUploadErrorMessage
852
- })),
853
- getConnectedPsqlExec(pgUser, pgHost, pgDatabase, pgPort, containerName)
854
- ]);
855
- let exit = 0;
856
- let message;
857
- let entries = match[0];
858
- if (entries.TAG === "Ok") {
859
- let psqlExec = match[1];
895
+ let entries = await scanCacheDir();
896
+ if (entries.length !== 0) {
897
+ let psqlExec = await getConnectedPsqlExec(pgUser, pgHost, pgDatabase, pgPort, containerName);
860
898
  if (psqlExec.TAG === "Ok") {
861
899
  let psqlExec$1 = psqlExec._0;
862
- let cacheFiles = entries._0.filter(entry => entry.endsWith(".tsv"));
863
- await Promise.all(cacheFiles.map(entry => {
864
- let effectName = entry.slice(0, -4);
865
- let table = Internal.makeCacheTable(effectName);
900
+ await Promise.all(entries.map(param => {
901
+ let inputFile = param[1];
902
+ let table = param[0];
866
903
  return sql.unsafe(makeCreateTableQuery(table, pgSchema, false)).then(() => {
867
- let inputFile = Path.join(cacheDirPath, entry);
868
904
  let command = psqlExec$1 + ` -c 'COPY "` + pgSchema + `"."` + table.tableName + `" FROM STDIN WITH (FORMAT text, HEADER);' < ` + inputFile;
869
905
  return new Promise((resolve, reject) => {
870
906
  Child_process.exec(command, psqlExecOptions, (error, stdout, param) => {
@@ -879,19 +915,10 @@ function make(sql, pgHost, pgSchema, pgPort, pgUser, pgDatabase, pgPassword, isH
879
915
  }));
880
916
  Logging.info("Successfully uploaded cache.");
881
917
  } else {
882
- message = match[1]._0;
883
- exit = 1;
918
+ Logging.error(`Failed to upload cache, continuing without it. ` + psqlExec._0);
884
919
  }
885
920
  } else {
886
- message = entries._0;
887
- exit = 1;
888
- }
889
- if (exit === 1) {
890
- if (message === nothingToUploadErrorMessage) {
891
- Logging.info("No cache found to upload.");
892
- } else {
893
- Logging.error(`Failed to upload cache, continuing without it. ` + message);
894
- }
921
+ Logging.info("No cache found to upload.");
895
922
  }
896
923
  }
897
924
  let cacheTableInfo = await sql.unsafe(makeSchemaCacheTableInfoQuery(pgSchema));
@@ -900,11 +927,17 @@ function make(sql, pgHost, pgSchema, pgPort, pgUser, pgDatabase, pgPassword, isH
900
927
  }
901
928
  let cache = {};
902
929
  cacheTableInfo.forEach(param => {
903
- let effectName = param.table_name.slice(cacheTablePrefixLength);
904
- cache[effectName] = {
905
- effectName: effectName,
906
- count: param.count
907
- };
930
+ let tableName = param.table_name;
931
+ let match = Internal.EffectCache.fromTableName(tableName);
932
+ if (match !== undefined) {
933
+ cache[tableName] = {
934
+ effectName: match[0],
935
+ scope: match[1],
936
+ tableName: tableName,
937
+ count: param.count
938
+ };
939
+ return;
940
+ }
908
941
  });
909
942
  return cache;
910
943
  };
@@ -998,16 +1031,14 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
998
1031
  }
999
1032
  };
1000
1033
  let setOrThrow$1 = (items, table, itemSchema) => setOrThrow(sql, items, table, itemSchema, pgSchema);
1001
- let setEffectCacheOrThrow = async (effect, items, initialize) => {
1002
- let match = effect.storageMeta;
1003
- let table = match.table;
1034
+ let setEffectCacheOrThrow = async (table, itemSchema, items, initialize) => {
1004
1035
  if (initialize) {
1005
1036
  await sql.unsafe(makeCreateTableQuery(table, pgSchema, false));
1006
1037
  if (onNewTables !== undefined) {
1007
1038
  await onNewTables([table.tableName]);
1008
1039
  }
1009
1040
  }
1010
- return await setOrThrow$1(items, table, match.itemSchema);
1041
+ return await setOrThrow$1(items, table, itemSchema);
1011
1042
  };
1012
1043
  let dumpEffectCache = async () => {
1013
1044
  try {
@@ -1030,10 +1061,16 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
1030
1061
  Logging.info(`Dumping cache: ` + cacheTableInfo.map(param => param.table_name + " (" + param.count.toString() + " rows)").join(", "));
1031
1062
  let promises = cacheTableInfo.map(async param => {
1032
1063
  let tableName = param.table_name;
1033
- let cacheName = tableName.slice(cacheTablePrefixLength);
1034
- let outputFile = Path.join(cacheDirPath, cacheName + ".tsv");
1035
- let command = psqlExec$1 + ` -c 'COPY "` + pgSchema + `"."` + tableName + `" TO STDOUT WITH (FORMAT text, HEADER);' > ` + outputFile;
1036
- return new Promise((resolve, reject) => {
1064
+ let match = Internal.EffectCache.fromTableName(tableName);
1065
+ if (match === undefined) {
1066
+ return "";
1067
+ }
1068
+ let outputPath = Path.join(cacheDirPath, Internal.EffectCache.toCachePath(match[0], match[1]));
1069
+ await Fs.promises.mkdir(Path.dirname(outputPath), {
1070
+ recursive: true
1071
+ });
1072
+ let command = psqlExec$1 + ` -c 'COPY "` + pgSchema + `"."` + tableName + `" TO STDOUT WITH (FORMAT text, HEADER);' > ` + outputPath;
1073
+ return await new Promise((resolve, reject) => {
1037
1074
  Child_process.exec(command, psqlExecOptions, (error, stdout, param) => {
1038
1075
  if (error === null) {
1039
1076
  return resolve(stdout);
@@ -1097,10 +1134,26 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
1097
1134
  let pruneStaleEntityHistory = (entityName, entityIndex, safeCheckpointId) => EntityHistory.pruneStaleEntityHistory(sql, entityName, entityIndex, pgSchema, safeCheckpointId);
1098
1135
  let getRollbackTargetCheckpoint = (reorgChainId, lastKnownValidBlockNumber) => InternalTable.Checkpoints.getRollbackTargetCheckpoint(sql, pgSchema, reorgChainId, lastKnownValidBlockNumber);
1099
1136
  let getRollbackProgressDiff = rollbackTargetCheckpointId => InternalTable.Checkpoints.getRollbackProgressDiff(sql, pgSchema, rollbackTargetCheckpointId);
1100
- let getRollbackData = async (entityConfig, rollbackTargetCheckpointId) => await Promise.all([
1101
- sql.unsafe(makeGetRollbackRemovedIdsQuery(entityConfig, pgSchema), [rollbackTargetCheckpointId.toString()], {prepare: true}),
1102
- sql.unsafe(makeGetRollbackRestoredEntitiesQuery(entityConfig, pgSchema), [rollbackTargetCheckpointId.toString()], {prepare: true})
1103
- ]);
1137
+ let getRollbackData = async (entityConfig, rollbackTargetCheckpointId) => {
1138
+ let match = await Promise.all([
1139
+ sql.unsafe(makeGetRollbackRemovedIdsQuery(entityConfig, pgSchema), [rollbackTargetCheckpointId.toString()], {prepare: true}),
1140
+ sql.unsafe(makeGetRollbackPreTargetRowsQuery(entityConfig, pgSchema), [rollbackTargetCheckpointId.toString()], {prepare: true})
1141
+ ]);
1142
+ let removedIds = match[0].map(row => row.id);
1143
+ let restoredEntitiesResult = [];
1144
+ match[1].forEach(row => {
1145
+ let match = S$RescriptSchema.parseOrThrow(row, rollbackRowStateSchema);
1146
+ if (match[1] === "SET") {
1147
+ restoredEntitiesResult.push(row);
1148
+ return;
1149
+ }
1150
+ removedIds.push(match[0]);
1151
+ });
1152
+ return [
1153
+ removedIds,
1154
+ restoredEntitiesResult
1155
+ ];
1156
+ };
1104
1157
  let writeBatchMethod = async (batch, rollback, isInReorgThreshold, config, allEntities, updatedEffectsCache, updatedEntities, chainMetaData) => {
1105
1158
  let pgUpdates = [];
1106
1159
  let chUpdates = [];
@@ -1227,15 +1280,15 @@ export {
1227
1280
  setQueryCache,
1228
1281
  setOrThrow,
1229
1282
  makeSchemaTableNamesQuery,
1230
- cacheTablePrefixLength,
1231
1283
  makeSchemaCacheTableInfoQuery,
1232
1284
  getConnectedPsqlExec,
1233
1285
  deleteByIdsOrThrow,
1234
1286
  makeInsertDeleteUpdatesQuery,
1235
1287
  executeSet,
1236
1288
  writeBatch,
1237
- makeGetRollbackRestoredEntitiesQuery,
1289
+ makeGetRollbackPreTargetRowsQuery,
1238
1290
  makeGetRollbackRemovedIdsQuery,
1291
+ rollbackRowStateSchema,
1239
1292
  make,
1240
1293
  makeStorageFromEnv,
1241
1294
  makePersistenceFromConfig,
@@ -606,29 +606,39 @@ let effectLabelsSchema = S.object(s => {
606
606
  s.field("effect", S.string)
607
607
  })
608
608
 
609
+ // For metrics whose backing state lives per scope ("crossChain" or a chain
610
+ // id) — without the label, scopes of the same effect would clobber each
611
+ // other's gauge value.
612
+ let effectScopeLabelsSchema = S.schema(s =>
613
+ {
614
+ "effect": s.matches(S.string),
615
+ "scope": s.matches(S.string),
616
+ }
617
+ )
618
+
609
619
  module EffectCalls = {
610
620
  let timeCounter = SafeCounter.makeOrThrow(
611
621
  ~name="envio_effect_call_seconds",
612
622
  ~help="Processing time taken to call the Effect function.",
613
- ~labelSchema=effectLabelsSchema,
623
+ ~labelSchema=effectScopeLabelsSchema,
614
624
  )
615
625
 
616
626
  let sumTimeCounter = SafeCounter.makeOrThrow(
617
627
  ~name="envio_effect_call_seconds_total",
618
628
  ~help="Cumulative time spent calling the Effect function during the indexing process.",
619
- ~labelSchema=effectLabelsSchema,
629
+ ~labelSchema=effectScopeLabelsSchema,
620
630
  )
621
631
 
622
632
  let totalCallsCount = SafeCounter.makeOrThrow(
623
633
  ~name="envio_effect_call_total",
624
634
  ~help="Cumulative number of resolved Effect function calls during the indexing process.",
625
- ~labelSchema=effectLabelsSchema,
635
+ ~labelSchema=effectScopeLabelsSchema,
626
636
  )
627
637
 
628
638
  let activeCallsCount = SafeGauge.makeOrThrow(
629
639
  ~name="envio_effect_active_calls",
630
640
  ~help="The number of Effect function calls that are currently running.",
631
- ~labelSchema=effectLabelsSchema,
641
+ ~labelSchema=effectScopeLabelsSchema,
632
642
  )
633
643
  }
634
644
 
@@ -636,11 +646,11 @@ module EffectCacheCount = {
636
646
  let gauge = SafeGauge.makeOrThrow(
637
647
  ~name="envio_effect_cache",
638
648
  ~help="The number of items in the effect cache.",
639
- ~labelSchema=effectLabelsSchema,
649
+ ~labelSchema=effectScopeLabelsSchema,
640
650
  )
641
651
 
642
- let set = (~count, ~effectName) => {
643
- gauge->SafeGauge.handleInt(~labels=effectName, ~value=count)
652
+ let set = (~count, ~effectName, ~scope) => {
653
+ gauge->SafeGauge.handleInt(~labels={"effect": effectName, "scope": scope}, ~value=count)
644
654
  }
645
655
  }
646
656
 
@@ -660,7 +670,7 @@ module EffectQueueCount = {
660
670
  let gauge = SafeGauge.makeOrThrow(
661
671
  ~name="envio_effect_queue",
662
672
  ~help="The number of effect calls waiting in the rate limit queue.",
663
- ~labelSchema=effectLabelsSchema,
673
+ ~labelSchema=effectScopeLabelsSchema,
664
674
  )
665
675
 
666
676
  let timeCounter = SafeCounter.makeOrThrow(
@@ -669,8 +679,8 @@ module EffectQueueCount = {
669
679
  ~labelSchema=effectLabelsSchema,
670
680
  )
671
681
 
672
- let set = (~count, ~effectName) => {
673
- gauge->SafeGauge.handleInt(~labels=effectName, ~value=count)
682
+ let set = (~count, ~effectName, ~scope) => {
683
+ gauge->SafeGauge.handleInt(~labels={"effect": effectName, "scope": scope}, ~value=count)
674
684
  }
675
685
  }
676
686
 
@@ -651,13 +651,18 @@ let ProgressLatency = {
651
651
 
652
652
  let effectLabelsSchema = S$RescriptSchema.object(s => s.f("effect", S$RescriptSchema.string));
653
653
 
654
- let timeCounter$5 = makeOrThrow("envio_effect_call_seconds", "Processing time taken to call the Effect function.", effectLabelsSchema);
654
+ let effectScopeLabelsSchema = S$RescriptSchema.schema(s => ({
655
+ effect: s.m(S$RescriptSchema.string),
656
+ scope: s.m(S$RescriptSchema.string)
657
+ }));
658
+
659
+ let timeCounter$5 = makeOrThrow("envio_effect_call_seconds", "Processing time taken to call the Effect function.", effectScopeLabelsSchema);
655
660
 
656
- let sumTimeCounter$1 = makeOrThrow("envio_effect_call_seconds_total", "Cumulative time spent calling the Effect function during the indexing process.", effectLabelsSchema);
661
+ let sumTimeCounter$1 = makeOrThrow("envio_effect_call_seconds_total", "Cumulative time spent calling the Effect function during the indexing process.", effectScopeLabelsSchema);
657
662
 
658
- let totalCallsCount = makeOrThrow("envio_effect_call_total", "Cumulative number of resolved Effect function calls during the indexing process.", effectLabelsSchema);
663
+ let totalCallsCount = makeOrThrow("envio_effect_call_total", "Cumulative number of resolved Effect function calls during the indexing process.", effectScopeLabelsSchema);
659
664
 
660
- let activeCallsCount = makeOrThrow$1("envio_effect_active_calls", "The number of Effect function calls that are currently running.", effectLabelsSchema);
665
+ let activeCallsCount = makeOrThrow$1("envio_effect_active_calls", "The number of Effect function calls that are currently running.", effectScopeLabelsSchema);
661
666
 
662
667
  let EffectCalls = {
663
668
  timeCounter: timeCounter$5,
@@ -666,10 +671,13 @@ let EffectCalls = {
666
671
  activeCallsCount: activeCallsCount
667
672
  };
668
673
 
669
- let gauge$19 = makeOrThrow$1("envio_effect_cache", "The number of items in the effect cache.", effectLabelsSchema);
674
+ let gauge$19 = makeOrThrow$1("envio_effect_cache", "The number of items in the effect cache.", effectScopeLabelsSchema);
670
675
 
671
- function set$19(count, effectName) {
672
- handleInt$1(gauge$19, effectName, count);
676
+ function set$19(count, effectName, scope) {
677
+ handleInt$1(gauge$19, {
678
+ effect: effectName,
679
+ scope: scope
680
+ }, count);
673
681
  }
674
682
 
675
683
  let EffectCacheCount = {
@@ -688,12 +696,15 @@ let EffectCacheInvalidationsCount = {
688
696
  increment: increment$7
689
697
  };
690
698
 
691
- let gauge$20 = makeOrThrow$1("envio_effect_queue", "The number of effect calls waiting in the rate limit queue.", effectLabelsSchema);
699
+ let gauge$20 = makeOrThrow$1("envio_effect_queue", "The number of effect calls waiting in the rate limit queue.", effectScopeLabelsSchema);
692
700
 
693
701
  let timeCounter$6 = makeOrThrow("envio_effect_queue_wait_seconds", "The time spent waiting in the rate limit queue.", effectLabelsSchema);
694
702
 
695
- function set$20(count, effectName) {
696
- handleInt$1(gauge$20, effectName, count);
703
+ function set$20(count, effectName, scope) {
704
+ handleInt$1(gauge$20, {
705
+ effect: effectName,
706
+ scope: scope
707
+ }, count);
697
708
  }
698
709
 
699
710
  let EffectQueueCount = {
@@ -825,6 +836,7 @@ export {
825
836
  ProgressEventsCount,
826
837
  ProgressLatency,
827
838
  effectLabelsSchema,
839
+ effectScopeLabelsSchema,
828
840
  EffectCalls,
829
841
  EffectCacheCount,
830
842
  EffectCacheInvalidationsCount,
@@ -1,45 +1,156 @@
1
- // Throttled prune of stale entity-history rows below the safe checkpoint.
2
-
3
- let runPrune = async (state: IndexerState.t) => {
4
- switch state->IndexerState.getSafeCheckpointId {
5
- | None => ()
6
- | Some(safeCheckpointId) =>
7
- let persistence = state->IndexerState.persistence
8
- await persistence.storage.pruneStaleCheckpoints(~safeCheckpointId)
9
-
10
- for idx in 0 to persistence.allEntities->Array.length - 1 {
11
- if idx !== 0 {
12
- // Add some delay between entities
13
- // To unblock the pg client if it's needed for something else
14
- await Utils.delay(1000)
1
+ // Prune of stale entity-history rows below the safe checkpoint.
2
+ //
3
+ // Pruning must never run concurrently with a write to the same entity's history
4
+ // table: the prune's anchor deletion relies on "no history after the safe
5
+ // checkpoint", which a concurrently committing batch can falsify, losing the
6
+ // anchor and breaking a later rollback. The write loop enforces the safety by
7
+ // running the concurrent group only for entities absent from the batch being
8
+ // written (and awaiting it before the next write starts), and the forced group
9
+ // alone after the write.
10
+
11
+ let maxEntitiesPerWrite = 5
12
+ let forcedIntervalMultiplier = 5.
13
+
14
+ type targets = {
15
+ safeCheckpointId: Internal.checkpointId,
16
+ concurrent: array<Internal.entityConfig>,
17
+ forced: array<Internal.entityConfig>,
18
+ }
19
+
20
+ let selectFrom = (
21
+ ~allEntities: array<Internal.entityConfig>,
22
+ ~lastPrunedAtMillis: dict<float>,
23
+ ~writtenEntityNames: Utils.Set.t<string>,
24
+ ~isRollback,
25
+ ~nowMillis,
26
+ ~intervalMillis,
27
+ ~safeCheckpointId,
28
+ ) => {
29
+ let byOldestPrune = ((a, _), (b, _)) => a -. b
30
+ let toEntities = candidates => candidates->Array.map(((_, entityConfig)) => entityConfig)
31
+
32
+ let concurrentCandidates = []
33
+ let forcedCandidates = []
34
+ allEntities->Array.forEach(entityConfig => {
35
+ if entityConfig.storage.postgres {
36
+ let lastPrunedAt =
37
+ lastPrunedAtMillis
38
+ ->Utils.Dict.dangerouslyGetNonOption(entityConfig.name)
39
+ ->Option.getOr(0.)
40
+ if (
41
+ !isRollback &&
42
+ !(writtenEntityNames->Utils.Set.has(entityConfig.name)) &&
43
+ nowMillis -. lastPrunedAt >= intervalMillis
44
+ ) {
45
+ concurrentCandidates->Array.push((lastPrunedAt, entityConfig))
46
+ } else if nowMillis -. lastPrunedAt >= intervalMillis *. forcedIntervalMultiplier {
47
+ forcedCandidates->Array.push((lastPrunedAt, entityConfig))
15
48
  }
16
- let entityConfig = persistence.allEntities->Array.getUnsafe(idx)
17
- let timeRef = Performance.now()
18
- try {
19
- let () = await persistence.storage.pruneStaleEntityHistory(
20
- ~entityName=entityConfig.name,
21
- ~entityIndex=entityConfig.index,
49
+ }
50
+ })
51
+
52
+ let sortedConcurrent = concurrentCandidates->Array.toSorted(byOldestPrune)
53
+ // Concurrent candidates beyond the cap are not selected, so the starved
54
+ // ones among them still qualify for the forced group.
55
+ for idx in maxEntitiesPerWrite to sortedConcurrent->Array.length - 1 {
56
+ let (lastPrunedAt, _) = sortedConcurrent->Array.getUnsafe(idx)
57
+ if nowMillis -. lastPrunedAt >= intervalMillis *. forcedIntervalMultiplier {
58
+ forcedCandidates->Array.push(sortedConcurrent->Array.getUnsafe(idx))
59
+ }
60
+ }
61
+
62
+ {
63
+ safeCheckpointId,
64
+ concurrent: sortedConcurrent->Array.slice(~start=0, ~end=maxEntitiesPerWrite)->toEntities,
65
+ forced: forcedCandidates
66
+ ->Array.toSorted(byOldestPrune)
67
+ ->Array.slice(~start=0, ~end=maxEntitiesPerWrite)
68
+ ->toEntities,
69
+ }
70
+ }
71
+
72
+ let select = (state: IndexerState.t, ~writtenEntityNames, ~isRollback) => {
73
+ let config = state->IndexerState.config
74
+ if config->Config.shouldPruneHistory(~isInReorgThreshold=state->IndexerState.isInReorgThreshold) {
75
+ switch state->IndexerState.getSafeCheckpointId {
76
+ | None => None
77
+ | Some(safeCheckpointId) =>
78
+ Some(
79
+ selectFrom(
80
+ ~allEntities=(state->IndexerState.persistence).allEntities,
81
+ ~lastPrunedAtMillis=state->IndexerState.lastPrunedAtMillis,
82
+ ~writtenEntityNames,
83
+ ~isRollback,
84
+ ~nowMillis=Date.now(),
85
+ ~intervalMillis=Env.ThrottleWrites.pruneStaleDataIntervalMillis->Int.toFloat,
22
86
  ~safeCheckpointId,
23
- )
24
- } catch {
25
- | exn =>
26
- exn->ErrorHandling.mkLogAndRaise(
27
- ~msg=`Failed to prune stale entity history`,
28
- ~logger=Logging.createChild(
29
- ~params={
30
- "entityName": entityConfig.name,
31
- "safeCheckpointId": safeCheckpointId,
32
- },
33
- ),
34
- )
35
- }
87
+ ),
88
+ )
89
+ }
90
+ } else {
91
+ None
92
+ }
93
+ }
94
+
95
+ let pruneEntities = async (state: IndexerState.t, ~entities, ~safeCheckpointId) => {
96
+ let persistence = state->IndexerState.persistence
97
+ for idx in 0 to entities->Array.length - 1 {
98
+ let entityConfig: Internal.entityConfig = entities->Array.getUnsafe(idx)
99
+ let timeRef = Performance.now()
100
+ // Recorded for failures too, so a failing prune retries on the same
101
+ // interval instead of on every write.
102
+ state->IndexerState.lastPrunedAtMillis->Dict.set(entityConfig.name, Date.now())
103
+ switch await persistence.storage.pruneStaleEntityHistory(
104
+ ~entityName=entityConfig.name,
105
+ ~entityIndex=entityConfig.index,
106
+ ~safeCheckpointId,
107
+ ) {
108
+ | () =>
36
109
  Prometheus.RollbackHistoryPrune.increment(
37
110
  ~timeSeconds=Performance.secondsSince(timeRef),
38
111
  ~entityName=entityConfig.name,
39
112
  )
113
+ | exception exn =>
114
+ // Pruning is cleanup; a failure must not fail the write loop.
115
+ Logging.createChild(
116
+ ~params={
117
+ "entityName": entityConfig.name,
118
+ "safeCheckpointId": safeCheckpointId,
119
+ },
120
+ )->Logging.childErrorWithExn(exn->Utils.prettifyExn, `Failed to prune stale entity history`)
40
121
  }
41
122
  }
42
123
  }
43
124
 
44
- let schedule = (state: IndexerState.t) =>
45
- state->IndexerState.pruneStaleEntityHistoryThrottler->Throttler.schedule(() => runPrune(state))
125
+ let pruneCheckpoints = async (state: IndexerState.t, ~safeCheckpointId) => {
126
+ switch await (state->IndexerState.persistence).storage.pruneStaleCheckpoints(~safeCheckpointId) {
127
+ | () => ()
128
+ | exception exn =>
129
+ Logging.createChild(~params={"safeCheckpointId": safeCheckpointId})->Logging.childErrorWithExn(
130
+ exn->Utils.prettifyExn,
131
+ `Failed to prune stale checkpoints`,
132
+ )
133
+ }
134
+ }
135
+
136
+ let runConcurrent = async (state: IndexerState.t, ~targets) => {
137
+ switch targets {
138
+ | Some({safeCheckpointId, concurrent}) if concurrent->Utils.Array.notEmpty =>
139
+ await pruneCheckpoints(state, ~safeCheckpointId)
140
+ await pruneEntities(state, ~entities=concurrent, ~safeCheckpointId)
141
+ | Some(_) | None => ()
142
+ }
143
+ }
144
+
145
+ let runForced = async (state: IndexerState.t, ~targets) => {
146
+ switch targets {
147
+ | Some({safeCheckpointId, concurrent, forced}) if forced->Utils.Array.notEmpty =>
148
+ // When nothing ran concurrently (eg a rollback write), checkpoint pruning
149
+ // lands here, after the write, so it never overlaps a rollback transaction.
150
+ if concurrent->Utils.Array.isEmpty {
151
+ await pruneCheckpoints(state, ~safeCheckpointId)
152
+ }
153
+ await pruneEntities(state, ~entities=forced, ~safeCheckpointId)
154
+ | Some(_) | None => ()
155
+ }
156
+ }