envio 3.5.0-alpha.2 → 3.5.0-alpha.3
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/package.json +6 -6
- package/src/BatchProcessing.res +14 -0
- package/src/BatchProcessing.res.mjs +8 -0
- package/src/ChainState.res +10 -4
- package/src/ChainState.res.mjs +7 -6
- package/src/ChainState.resi +1 -1
- package/src/Config.res +2 -2
- package/src/Config.res.mjs +2 -2
- package/src/CrossChainState.res +33 -10
- package/src/CrossChainState.res.mjs +21 -6
- package/src/CrossChainState.resi +3 -0
- package/src/FetchState.res +230 -66
- package/src/FetchState.res.mjs +213 -67
- package/src/FinalizeBackfill.res +34 -0
- package/src/FinalizeBackfill.res.mjs +26 -0
- package/src/InMemoryTable.res +15 -15
- package/src/InMemoryTable.res.mjs +14 -14
- package/src/IndexerState.res +12 -0
- package/src/IndexerState.res.mjs +19 -0
- package/src/IndexerState.resi +3 -0
- package/src/LoadLayer.res +9 -1
- package/src/LoadLayer.res.mjs +1 -0
- package/src/Persistence.res +11 -0
- package/src/PgStorage.res +356 -35
- package/src/PgStorage.res.mjs +237 -24
- package/src/TestIndexer.res +3 -0
- package/src/TestIndexer.res.mjs +2 -0
- package/src/db/EntityFilter.res +0 -4
- package/src/db/EntityFilter.res.mjs +1 -8
- package/src/db/IndexRegistry.res +219 -0
- package/src/db/IndexRegistry.res.mjs +195 -0
- package/src/db/InternalTable.res +7 -0
- package/src/db/InternalTable.res.mjs +7 -0
- package/src/db/Table.res +13 -13
- package/src/db/Table.res.mjs +12 -12
package/src/PgStorage.res
CHANGED
|
@@ -58,7 +58,7 @@ let makeCreateCompositeIndexQuery = (
|
|
|
58
58
|
`CREATE INDEX IF NOT EXISTS "${indexName}" ON "${pgSchema}"."${tableName}"(${index});`
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
let
|
|
61
|
+
let makeCreateTableIndexesQuery = (table: Table.table, ~pgSchema) => {
|
|
62
62
|
let tableName = table.tableName
|
|
63
63
|
let createIndex = indexField =>
|
|
64
64
|
makeCreateIndexQuery(~tableName, ~indexFields=[indexField], ~pgSchema)
|
|
@@ -66,11 +66,138 @@ let makeCreateTableIndicesQuery = (table: Table.table, ~pgSchema) => {
|
|
|
66
66
|
makeCreateCompositeIndexQuery(~tableName, ~indexFields, ~pgSchema)
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
let
|
|
70
|
-
let
|
|
69
|
+
let singleIndexes = table->Table.getSingleIndexes
|
|
70
|
+
let compositeIndexes = table->Table.getCompositeIndexes
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
singleIndexes->Array.map(createIndex)->Array.joinUnsafe("\n") ++
|
|
73
|
+
compositeIndexes->Array.map(createCompositeIndex)->Array.joinUnsafe("\n")
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// One index the schema promises: an `@index` field, a composite index, or the
|
|
77
|
+
// index backing a derived relationship. Carries enough to derive its name, its
|
|
78
|
+
// DDL and its registry key, so the deferred creation at the end of backfill and
|
|
79
|
+
// the registry stay described by the same value.
|
|
80
|
+
type schemaIndex = {
|
|
81
|
+
tableName: string,
|
|
82
|
+
indexFields: array<Table.compositeIndexField>,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// `<Entity>_<column>` for a single column, with each further column appended in
|
|
86
|
+
// order (and `_desc` where it applies). The full description, before Postgres'
|
|
87
|
+
// identifier limit is applied — `validateIndexNamesOrThrow` compares these, so
|
|
88
|
+
// it can tell two descriptions apart after they truncate to the same name.
|
|
89
|
+
let schemaIndexDescription = ({tableName, indexFields}: schemaIndex) =>
|
|
90
|
+
tableName ++
|
|
91
|
+
"_" ++
|
|
92
|
+
indexFields
|
|
93
|
+
->Array.map(f => f.fieldName ++ directionToIndexName(f.direction))
|
|
94
|
+
->Array.joinUnsafe("_")
|
|
95
|
+
|
|
96
|
+
// The identifier Postgres actually stores. Automatic and schema-defined indexes
|
|
97
|
+
// share it, so both are named the same way and both stay reachable through
|
|
98
|
+
// `CREATE INDEX IF NOT EXISTS`.
|
|
99
|
+
let schemaIndexName = schemaIndex =>
|
|
100
|
+
schemaIndex->schemaIndexDescription->IndexRegistry.truncateIndexName
|
|
101
|
+
|
|
102
|
+
let schemaIndexKey = ({tableName, indexFields}: schemaIndex) =>
|
|
103
|
+
IndexRegistry.makeKey(
|
|
104
|
+
~tableName,
|
|
105
|
+
~columns=indexFields->IndexRegistry.fromIndexFields,
|
|
106
|
+
~method=IndexRegistry.btree,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
// Plain DDL, not CONCURRENTLY: it builds from a single table scan instead of
|
|
110
|
+
// two, and the SHARE lock it takes blocks writes but not reads, so queries keep
|
|
111
|
+
// being served while it runs. The indexer is the only writer, and both callers
|
|
112
|
+
// are happy to wait on it.
|
|
113
|
+
let makeCreateSchemaIndexQuery = (schemaIndex, ~pgSchema) => {
|
|
114
|
+
let {tableName, indexFields} = schemaIndex
|
|
115
|
+
let columns =
|
|
116
|
+
indexFields
|
|
117
|
+
->Array.map(f => `"${f.fieldName}"${directionToSql(f.direction)}`)
|
|
118
|
+
->Array.joinUnsafe(", ")
|
|
119
|
+
`CREATE INDEX IF NOT EXISTS "${schemaIndex->schemaIndexName}" ON "${pgSchema}"."${tableName}"(${columns});`
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let formatSeconds = (timeRef: Performance.timeRef) =>
|
|
123
|
+
(Math.round(timeRef->Performance.secondsSince *. 100.) /. 100.)->Float.toString
|
|
124
|
+
|
|
125
|
+
// Every index build blocks writes to its table for as long as it runs, and on a
|
|
126
|
+
// large database that is not quick. Both build paths say so up front, so a
|
|
127
|
+
// stalled-looking indexer is explainable from the logs alone.
|
|
128
|
+
let slowOnLargeDatabaseNotice = "This can take a long time on a large database."
|
|
129
|
+
|
|
130
|
+
// An index Postgres reports as INVALID still owns its name, so
|
|
131
|
+
// `CREATE INDEX IF NOT EXISTS` would quietly skip it and leave the query
|
|
132
|
+
// unserved. REINDEX rebuilds it from its own stored definition — a repair, not
|
|
133
|
+
// a replacement — which is right only when that definition is the one we
|
|
134
|
+
// wanted. Runs inside a transaction, unlike REINDEX CONCURRENTLY.
|
|
135
|
+
let makeReindexQuery = (~pgSchema, ~indexName) => `REINDEX INDEX "${pgSchema}"."${indexName}";`
|
|
136
|
+
|
|
137
|
+
// A name taken by an invalid index the indexer didn't create isn't ours to
|
|
138
|
+
// touch: rebuilding it reuses its own definition, so it would come back valid
|
|
139
|
+
// but still not serve the query, and dropping it could take out a unique
|
|
140
|
+
// constraint someone else relies on.
|
|
141
|
+
let nameCollisionError = (~indexName, ~pgSchema) =>
|
|
142
|
+
Utils.Error.make(
|
|
143
|
+
`Cannot create the index "${indexName}" in schema "${pgSchema}". An index of that name already exists and PostgreSQL reports it as invalid, but it isn't one the indexer created — it covers different columns, or it is a unique or partial index — so it is left untouched. Drop or repair it by hand, then restart the indexer.`,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
let makeSingleColumnSchemaIndex = (~tableName, ~column): schemaIndex => {
|
|
147
|
+
tableName,
|
|
148
|
+
indexFields: [{fieldName: column, direction: Table.Asc}],
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Every index the entity schema promises. Deferred past the initial DDL and
|
|
152
|
+
// created in one transaction once backfill completes, so a resumed indexer that
|
|
153
|
+
// reports itself ready always has all of them.
|
|
154
|
+
//
|
|
155
|
+
// `entities` is the Postgres-backed set, and every `@derivedFrom` target within
|
|
156
|
+
// it resolves: config parsing rejects a Postgres entity deriving from one that
|
|
157
|
+
// isn't in Postgres (`validate_relationship_storage`).
|
|
158
|
+
let getSchemaIndexes = (~entities: array<Internal.entityConfig>): array<schemaIndex> => {
|
|
159
|
+
let derivedSchema = Schema.make(entities->Array.map(e => e.table))
|
|
160
|
+
let all = []
|
|
161
|
+
|
|
162
|
+
entities->Array.forEach(({table}) => {
|
|
163
|
+
table
|
|
164
|
+
->Table.getSingleIndexes
|
|
165
|
+
->Array.forEach(column =>
|
|
166
|
+
all->Array.push(makeSingleColumnSchemaIndex(~tableName=table.tableName, ~column))->ignore
|
|
167
|
+
)
|
|
168
|
+
table
|
|
169
|
+
->Table.getCompositeIndexes
|
|
170
|
+
->Array.forEach(indexFields =>
|
|
171
|
+
all->Array.push({tableName: table.tableName, indexFields})->ignore
|
|
172
|
+
)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
entities->Array.forEach(({table}) =>
|
|
176
|
+
table
|
|
177
|
+
->Table.getDerivedFromFields
|
|
178
|
+
->Array.forEach(derivedFromField => {
|
|
179
|
+
let column =
|
|
180
|
+
derivedSchema->Schema.getDerivedFromPgFieldName(derivedFromField)->Utils.unwrapResultExn
|
|
181
|
+
all
|
|
182
|
+
->Array.push(
|
|
183
|
+
makeSingleColumnSchemaIndex(~tableName=derivedFromField.derivedFromEntity, ~column),
|
|
184
|
+
)
|
|
185
|
+
->ignore
|
|
186
|
+
})
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
// An `@index` field and a derived relationship pointing at it describe the
|
|
190
|
+
// same index, so the list is deduped on identity rather than on name.
|
|
191
|
+
let seen = Utils.Set.make()
|
|
192
|
+
all->Array.filter(schemaIndex => {
|
|
193
|
+
let key = schemaIndex->schemaIndexKey
|
|
194
|
+
if seen->Utils.Set.has(key) {
|
|
195
|
+
false
|
|
196
|
+
} else {
|
|
197
|
+
seen->Utils.Set.add(key)->ignore
|
|
198
|
+
true
|
|
199
|
+
}
|
|
200
|
+
})
|
|
74
201
|
}
|
|
75
202
|
|
|
76
203
|
let makeCreateTableQuery = (table: Table.table, ~pgSchema, ~isNumericArrayAsText) => {
|
|
@@ -152,7 +279,7 @@ let getEntityHistory = (~entityConfig: Internal.entityConfig): EntityHistory.pgE
|
|
|
152
279
|
~entityName=entityTableName,
|
|
153
280
|
~entityIndex=entityConfig.index,
|
|
154
281
|
)
|
|
155
|
-
//ignore composite
|
|
282
|
+
//ignore composite indexes
|
|
156
283
|
let table = Table.mkTable(
|
|
157
284
|
historyTableName,
|
|
158
285
|
~fields=dataFields->Array.concat([checkpointIdField, actionField]),
|
|
@@ -183,6 +310,10 @@ let makeInitializeTransaction = (
|
|
|
183
310
|
~entities=[],
|
|
184
311
|
~enums=[],
|
|
185
312
|
~isEmptyPgSchema=false,
|
|
313
|
+
// Backfill writes are far cheaper without the schema's read indexes, so the
|
|
314
|
+
// initial DDL creates only tables, primary keys, views and chain rows; the
|
|
315
|
+
// rest is created by `finalizeBackfill` before the indexer reports ready.
|
|
316
|
+
~deferSchemaIndexes=false,
|
|
186
317
|
) => {
|
|
187
318
|
let generalTables = [
|
|
188
319
|
InternalTable.Chains.table,
|
|
@@ -192,13 +323,13 @@ let makeInitializeTransaction = (
|
|
|
192
323
|
]
|
|
193
324
|
|
|
194
325
|
let allTables = generalTables->Array.copy
|
|
195
|
-
let allEntityTables = []
|
|
196
326
|
entities->Array.forEach((entityConfig: Internal.entityConfig) => {
|
|
197
|
-
allEntityTables->Array.push(entityConfig.table)->ignore
|
|
198
327
|
allTables->Array.push(entityConfig.table)->ignore
|
|
199
328
|
allTables->Array.push(getEntityHistory(~entityConfig).table)->ignore
|
|
200
329
|
})
|
|
201
|
-
|
|
330
|
+
|
|
331
|
+
let schemaIndexes = getSchemaIndexes(~entities)
|
|
332
|
+
IndexRegistry.validateIndexNamesOrThrow(schemaIndexes->Array.map(schemaIndexDescription))
|
|
202
333
|
|
|
203
334
|
let query = ref(
|
|
204
335
|
(
|
|
@@ -232,31 +363,12 @@ GRANT ALL ON SCHEMA "${pgSchema}" TO public;`,
|
|
|
232
363
|
makeCreateTableQuery(table, ~pgSchema, ~isNumericArrayAsText=isHasuraEnabled)
|
|
233
364
|
})
|
|
234
365
|
|
|
235
|
-
// Then batch all
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
query := query.contents ++ "\n" ++ indices
|
|
240
|
-
}
|
|
241
|
-
})
|
|
242
|
-
|
|
243
|
-
// Add derived indices
|
|
244
|
-
entities->Array.forEach((entity: Internal.entityConfig) => {
|
|
245
|
-
entity.table
|
|
246
|
-
->Table.getDerivedFromFields
|
|
247
|
-
->Array.forEach(derivedFromField => {
|
|
248
|
-
let indexField =
|
|
249
|
-
derivedSchema->Schema.getDerivedFromPgFieldName(derivedFromField)->Utils.unwrapResultExn
|
|
250
|
-
query :=
|
|
251
|
-
query.contents ++
|
|
252
|
-
"\n" ++
|
|
253
|
-
makeCreateIndexQuery(
|
|
254
|
-
~tableName=derivedFromField.derivedFromEntity,
|
|
255
|
-
~indexFields=[indexField],
|
|
256
|
-
~pgSchema,
|
|
257
|
-
)
|
|
366
|
+
// Then batch all indexes (better performance when tables exist)
|
|
367
|
+
if !deferSchemaIndexes {
|
|
368
|
+
schemaIndexes->Array.forEach(schemaIndex => {
|
|
369
|
+
query := query.contents ++ "\n" ++ makeCreateSchemaIndexQuery(schemaIndex, ~pgSchema)
|
|
258
370
|
})
|
|
259
|
-
}
|
|
371
|
+
}
|
|
260
372
|
|
|
261
373
|
// Create views for Hasura integration
|
|
262
374
|
query := query.contents ++ "\n" ++ InternalTable.Views.makeMetaViewQuery(~pgSchema)
|
|
@@ -1293,6 +1405,37 @@ let make = (
|
|
|
1293
1405
|
"cache",
|
|
1294
1406
|
])
|
|
1295
1407
|
|
|
1408
|
+
// The metric label, the `storage` field on this backend's logs, and the
|
|
1409
|
+
// storage record's own name are all the same string.
|
|
1410
|
+
let storageName = "postgres"
|
|
1411
|
+
|
|
1412
|
+
let indexRegistry = IndexRegistry.make()
|
|
1413
|
+
|
|
1414
|
+
// The only point where the catalog is consulted. Afterwards this process is
|
|
1415
|
+
// the sole writer of the schema, so the registry is kept in sync by adding
|
|
1416
|
+
// each index right after its DDL succeeds.
|
|
1417
|
+
let reloadIndexRegistry = async () => {
|
|
1418
|
+
let rows =
|
|
1419
|
+
(await sql->Postgres.unsafe(IndexRegistry.makeCatalogQuery(~pgSchema)))->S.parseOrThrow(
|
|
1420
|
+
IndexRegistry.catalogRowsSchema,
|
|
1421
|
+
)
|
|
1422
|
+
switch indexRegistry->IndexRegistry.reload(~rows) {
|
|
1423
|
+
| [] => ()
|
|
1424
|
+
| invalidIndexNames =>
|
|
1425
|
+
Logging.warn({
|
|
1426
|
+
"storage": storageName,
|
|
1427
|
+
"msg": `Ignoring invalid PostgreSQL indexes in schema "${pgSchema}". They are left untouched — drop and recreate them manually if you need them.`,
|
|
1428
|
+
"indexes": invalidIndexNames,
|
|
1429
|
+
})
|
|
1430
|
+
}
|
|
1431
|
+
Logging.info({
|
|
1432
|
+
"storage": storageName,
|
|
1433
|
+
"msg": `Found ${indexRegistry
|
|
1434
|
+
->IndexRegistry.size
|
|
1435
|
+
->Int.toString} existing indexes in the indexer schema.`,
|
|
1436
|
+
})
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1296
1439
|
let isInitialized = async () => {
|
|
1297
1440
|
let envioTables = await sql->Postgres.unsafe(
|
|
1298
1441
|
`SELECT table_schema FROM information_schema.tables WHERE table_schema = '${pgSchema}' AND (table_name = '${// This is for indexer before envio@2.28
|
|
@@ -1484,6 +1627,7 @@ let make = (
|
|
|
1484
1627
|
~chainConfigs,
|
|
1485
1628
|
~isEmptyPgSchema=schemaTableNames->Utils.Array.isEmpty,
|
|
1486
1629
|
~isHasuraEnabled,
|
|
1630
|
+
~deferSchemaIndexes=true,
|
|
1487
1631
|
)
|
|
1488
1632
|
// Execute all queries within a single transaction for integrity.
|
|
1489
1633
|
// The envio_info row is written in the same transaction so a successful
|
|
@@ -1520,6 +1664,8 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
|
|
|
1520
1664
|
|
|
1521
1665
|
let cache = await restoreEffectCache(~withUpload=true)
|
|
1522
1666
|
|
|
1667
|
+
await reloadIndexRegistry()
|
|
1668
|
+
|
|
1523
1669
|
// Integration with other tools like Hasura
|
|
1524
1670
|
switch onInitialize {
|
|
1525
1671
|
| Some(onInitialize) => await onInitialize()
|
|
@@ -1576,6 +1722,177 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
|
|
|
1576
1722
|
}
|
|
1577
1723
|
}
|
|
1578
1724
|
|
|
1725
|
+
// The physical columns a filter reads, deduped and in first-seen order.
|
|
1726
|
+
// Unknown field names are left to `loadOrThrow`, which reports them properly.
|
|
1727
|
+
let filterColumns = (~table: Table.table, ~filters: array<EntityFilter.t>) => {
|
|
1728
|
+
let queryFields = table->Table.queryFields
|
|
1729
|
+
let columns = []
|
|
1730
|
+
let seen = Utils.Set.make()
|
|
1731
|
+
let rec collect = (filter: EntityFilter.t) =>
|
|
1732
|
+
switch filter {
|
|
1733
|
+
| Eq({fieldName}) | Gt({fieldName}) | Lt({fieldName}) | In({fieldName}) =>
|
|
1734
|
+
switch queryFields->Utils.Dict.dangerouslyGetNonOption(fieldName) {
|
|
1735
|
+
| Some({pgDbFieldName}) =>
|
|
1736
|
+
if !(seen->Utils.Set.has(pgDbFieldName)) {
|
|
1737
|
+
seen->Utils.Set.add(pgDbFieldName)->ignore
|
|
1738
|
+
columns->Array.push(pgDbFieldName)->ignore
|
|
1739
|
+
}
|
|
1740
|
+
| None => ()
|
|
1741
|
+
}
|
|
1742
|
+
| And({filters}) => filters->Array.forEach(collect)
|
|
1743
|
+
}
|
|
1744
|
+
filters->Array.forEach(collect)
|
|
1745
|
+
columns
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
// The DDL that leaves `schemaIndex` in place. Normally a create; a repair
|
|
1749
|
+
// when an invalid index already holds the name with the definition we want.
|
|
1750
|
+
let makeEnsureIndexQuery = schemaIndex => {
|
|
1751
|
+
let indexName = schemaIndex->schemaIndexName
|
|
1752
|
+
switch indexRegistry->IndexRegistry.getInvalid(indexName) {
|
|
1753
|
+
| None => makeCreateSchemaIndexQuery(schemaIndex, ~pgSchema)
|
|
1754
|
+
| Some({key, isRepairable: true}) if key === schemaIndex->schemaIndexKey =>
|
|
1755
|
+
makeReindexQuery(~pgSchema, ~indexName)
|
|
1756
|
+
| Some(_) => throw(nameCollisionError(~indexName, ~pgSchema))
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
let isRepair = schemaIndex =>
|
|
1761
|
+
indexRegistry->IndexRegistry.getInvalid(schemaIndex->schemaIndexName)->Option.isSome
|
|
1762
|
+
|
|
1763
|
+
let ensureQueryIndexes = async (~table: Table.table, ~filters: array<EntityFilter.t>) => {
|
|
1764
|
+
let missing = filterColumns(~table, ~filters)->Array.filterMap(column => {
|
|
1765
|
+
let schemaIndex = makeSingleColumnSchemaIndex(~tableName=table.tableName, ~column)
|
|
1766
|
+
indexRegistry->IndexRegistry.has(schemaIndex->schemaIndexKey) ? None : Some(schemaIndex)
|
|
1767
|
+
})
|
|
1768
|
+
|
|
1769
|
+
if missing->Utils.Array.notEmpty {
|
|
1770
|
+
let _ = await missing
|
|
1771
|
+
->Array.map(schemaIndex => {
|
|
1772
|
+
let indexName = schemaIndex->schemaIndexName
|
|
1773
|
+
indexRegistry
|
|
1774
|
+
->IndexRegistry.ensure(
|
|
1775
|
+
~key=schemaIndex->schemaIndexKey,
|
|
1776
|
+
~tableName=table.tableName,
|
|
1777
|
+
~build=async () => {
|
|
1778
|
+
// Resolved before logging so a repair is reported as one, and an
|
|
1779
|
+
// unrelated index holding the name fails before any DDL runs.
|
|
1780
|
+
let query = makeEnsureIndexQuery(schemaIndex)
|
|
1781
|
+
let verb = schemaIndex->isRepair ? "Repairing invalid index" : "Creating index"
|
|
1782
|
+
// Logged from inside the build so it reports the one request that
|
|
1783
|
+
// actually creates the index, not the ones waiting on it.
|
|
1784
|
+
Logging.info({
|
|
1785
|
+
"storage": storageName,
|
|
1786
|
+
"msg": `${verb} "${indexName}" to serve a getWhere query on "${table.tableName}". Writes to the table are paused until it completes. ${slowOnLargeDatabaseNotice}`,
|
|
1787
|
+
})
|
|
1788
|
+
let timeRef = Performance.now()
|
|
1789
|
+
let _ = await sql->Postgres.unsafe(query)
|
|
1790
|
+
indexRegistry->IndexRegistry.clearInvalidName(indexName)
|
|
1791
|
+
Logging.info({
|
|
1792
|
+
"storage": storageName,
|
|
1793
|
+
"msg": `Index "${indexName}" is ready after ${timeRef->formatSeconds}s. Resuming indexing.`,
|
|
1794
|
+
})
|
|
1795
|
+
},
|
|
1796
|
+
)
|
|
1797
|
+
// A failed build leaves the registry untouched, so the next getWhere
|
|
1798
|
+
// retries. Meanwhile the query still runs — just without the index.
|
|
1799
|
+
->Utils.Promise.catchResolve(exn => {
|
|
1800
|
+
Logging.warn({
|
|
1801
|
+
"storage": storageName,
|
|
1802
|
+
"msg": `Failed to create the index "${indexName}" for a getWhere query. The query runs without it.`,
|
|
1803
|
+
"err": exn->Utils.prettifyExn,
|
|
1804
|
+
})
|
|
1805
|
+
})
|
|
1806
|
+
})
|
|
1807
|
+
->Promise.all
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
let finalizeBackfill = async (
|
|
1812
|
+
~entities: array<Internal.entityConfig>,
|
|
1813
|
+
~chainIds: array<int>,
|
|
1814
|
+
~readyAt: Date.t,
|
|
1815
|
+
) => {
|
|
1816
|
+
let schemaIndexes = getSchemaIndexes(
|
|
1817
|
+
~entities=entities->Array.filter((e: Internal.entityConfig) => e.storage.postgres),
|
|
1818
|
+
)
|
|
1819
|
+
IndexRegistry.validateIndexNamesOrThrow(schemaIndexes->Array.map(schemaIndexDescription))
|
|
1820
|
+
|
|
1821
|
+
let missing =
|
|
1822
|
+
schemaIndexes->Array.filter(schemaIndex =>
|
|
1823
|
+
!(indexRegistry->IndexRegistry.has(schemaIndex->schemaIndexKey))
|
|
1824
|
+
)
|
|
1825
|
+
|
|
1826
|
+
// `IF NOT EXISTS` would quietly skip an index whose name is taken by an
|
|
1827
|
+
// invalid one, and the indexer would then report ready with an index the
|
|
1828
|
+
// planner can't use. Those are repaired in the same transaction instead.
|
|
1829
|
+
let repaired = missing->Array.filter(isRepair)
|
|
1830
|
+
if repaired->Utils.Array.notEmpty {
|
|
1831
|
+
Logging.warn({
|
|
1832
|
+
"storage": storageName,
|
|
1833
|
+
"msg": `PostgreSQL reports ${repaired
|
|
1834
|
+
->Array.length
|
|
1835
|
+
->Int.toString} existing indexes as invalid, so they can't serve queries. Rebuilding them.`,
|
|
1836
|
+
"indexes": repaired->Array.map(schemaIndexName),
|
|
1837
|
+
})
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
switch missing {
|
|
1841
|
+
| [] =>
|
|
1842
|
+
Logging.info({
|
|
1843
|
+
"storage": storageName,
|
|
1844
|
+
"msg": `All ${schemaIndexes
|
|
1845
|
+
->Array.length
|
|
1846
|
+
->Int.toString} schema indexes are already in place. Marking the indexer ready.`,
|
|
1847
|
+
})
|
|
1848
|
+
| _ =>
|
|
1849
|
+
Logging.info({
|
|
1850
|
+
"storage": storageName,
|
|
1851
|
+
"msg": `Creating the ${missing
|
|
1852
|
+
->Array.length
|
|
1853
|
+
->Int.toString} remaining schema indexes before the indexer reports ready. Writes are paused until they are committed. ${slowOnLargeDatabaseNotice}`,
|
|
1854
|
+
"indexes": missing->Array.map(schemaIndexName),
|
|
1855
|
+
})
|
|
1856
|
+
}
|
|
1857
|
+
// Resolved up front so a name held by an unrelated invalid index fails
|
|
1858
|
+
// before the transaction opens, rather than rolling one back.
|
|
1859
|
+
let indexQueries = missing->Array.map(makeEnsureIndexQuery)
|
|
1860
|
+
let timeRef = Performance.now()
|
|
1861
|
+
|
|
1862
|
+
await sql->Postgres.beginSql(async sql => {
|
|
1863
|
+
// Sequential rather than Promise.all: they share one connection inside
|
|
1864
|
+
// the transaction, and several of them target the same table.
|
|
1865
|
+
for idx in 0 to indexQueries->Array.length - 1 {
|
|
1866
|
+
let _ = await sql->Postgres.unsafe(indexQueries->Array.getUnsafe(idx))
|
|
1867
|
+
}
|
|
1868
|
+
await sql
|
|
1869
|
+
->Postgres.preparedUnsafe(
|
|
1870
|
+
InternalTable.Chains.makeSetReadyAtQuery(~pgSchema),
|
|
1871
|
+
[
|
|
1872
|
+
readyAt->(Utils.magic: Date.t => unknown),
|
|
1873
|
+
chainIds->(Utils.magic: array<int> => unknown),
|
|
1874
|
+
]->(Utils.magic: array<unknown> => unknown),
|
|
1875
|
+
)
|
|
1876
|
+
->Utils.Promise.ignoreValue
|
|
1877
|
+
})
|
|
1878
|
+
|
|
1879
|
+
// Only after the commit: a rollback takes both the indexes and ready_at
|
|
1880
|
+
// with it, and the registry must not claim what the schema doesn't have.
|
|
1881
|
+
missing->Array.forEach(schemaIndex =>
|
|
1882
|
+
indexRegistry->IndexRegistry.add(schemaIndex->schemaIndexKey)
|
|
1883
|
+
)
|
|
1884
|
+
repaired->Array.forEach(schemaIndex =>
|
|
1885
|
+
indexRegistry->IndexRegistry.clearInvalidName(schemaIndex->schemaIndexName)
|
|
1886
|
+
)
|
|
1887
|
+
|
|
1888
|
+
Logging.info({
|
|
1889
|
+
"storage": storageName,
|
|
1890
|
+
"msg": `Committed ${missing
|
|
1891
|
+
->Array.length
|
|
1892
|
+
->Int.toString} schema indexes and the ready timestamp in ${timeRef->formatSeconds}s.`,
|
|
1893
|
+
})
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1579
1896
|
let setOrThrow = (
|
|
1580
1897
|
type item,
|
|
1581
1898
|
~items: array<item>,
|
|
@@ -1720,6 +2037,8 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
|
|
|
1720
2037
|
InternalTable.EnvioInfo.read(sql, ~pgSchema),
|
|
1721
2038
|
))
|
|
1722
2039
|
|
|
2040
|
+
await reloadIndexRegistry()
|
|
2041
|
+
|
|
1723
2042
|
let checkpointId = (checkpointIdResult->Array.getUnsafe(0))["id"]->BigInt.fromStringOrThrow
|
|
1724
2043
|
|
|
1725
2044
|
// Convert string checkpoint IDs from DB to bigint
|
|
@@ -1869,17 +2188,19 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
|
|
|
1869
2188
|
~sinkPromise,
|
|
1870
2189
|
~chainMetaData,
|
|
1871
2190
|
)
|
|
1872
|
-
onWrite(~storage=
|
|
2191
|
+
onWrite(~storage=storageName, ~timeSeconds=primaryTimerRef->Performance.secondsSince)
|
|
1873
2192
|
}
|
|
1874
2193
|
|
|
1875
2194
|
let close = () => sql->Postgres.endSql
|
|
1876
2195
|
|
|
1877
2196
|
{
|
|
1878
|
-
name:
|
|
2197
|
+
name: storageName,
|
|
1879
2198
|
isInitialized,
|
|
1880
2199
|
initialize,
|
|
1881
2200
|
resumeInitialState,
|
|
1882
2201
|
loadOrThrow,
|
|
2202
|
+
ensureQueryIndexes,
|
|
2203
|
+
finalizeBackfill,
|
|
1883
2204
|
dumpEffectCache,
|
|
1884
2205
|
reset,
|
|
1885
2206
|
setChainMeta,
|