@secondlayer/subgraphs 3.23.2 → 3.24.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/dist/src/index.js +68 -28
- package/dist/src/index.js.map +5 -5
- package/dist/src/runtime/block-processor.d.ts +13 -0
- package/dist/src/runtime/block-processor.js +68 -28
- package/dist/src/runtime/block-processor.js.map +5 -5
- package/dist/src/runtime/catchup.js +102 -28
- package/dist/src/runtime/catchup.js.map +8 -7
- package/dist/src/runtime/context.js +19 -14
- package/dist/src/runtime/context.js.map +4 -4
- package/dist/src/runtime/processor.js +200 -143
- package/dist/src/runtime/processor.js.map +9 -9
- package/dist/src/runtime/reindex.js +68 -28
- package/dist/src/runtime/reindex.js.map +5 -5
- package/dist/src/runtime/reorg.js +200 -118
- package/dist/src/runtime/reorg.js.map +9 -8
- package/dist/src/runtime/replay.js +6 -1
- package/dist/src/runtime/replay.js.map +3 -3
- package/dist/src/schema/index.js +19 -14
- package/dist/src/schema/index.js.map +4 -4
- package/dist/src/service.js +202 -145
- package/dist/src/service.js.map +9 -9
- package/dist/src/testing/index.js +19 -14
- package/dist/src/testing/index.js.map +4 -4
- package/package.json +3 -5
package/dist/src/service.js
CHANGED
|
@@ -11,6 +11,11 @@ import { createHash } from "node:crypto";
|
|
|
11
11
|
|
|
12
12
|
// src/schema/utils.ts
|
|
13
13
|
import { pgSchemaName } from "@secondlayer/shared/db/queries/subgraphs";
|
|
14
|
+
function quotePgIdent(name) {
|
|
15
|
+
if (/^[a-z_][a-z0-9_]*$/i.test(name))
|
|
16
|
+
return name;
|
|
17
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
18
|
+
}
|
|
14
19
|
|
|
15
20
|
// src/schema/generator.ts
|
|
16
21
|
var TYPE_MAP = {
|
|
@@ -35,7 +40,7 @@ function tableNeedsTrgm(tableDef) {
|
|
|
35
40
|
return Object.values(tableDef.columns).some((col) => col.search);
|
|
36
41
|
}
|
|
37
42
|
function emitTableDDL(schemaName, tableName, tableDef) {
|
|
38
|
-
const qualifiedName = `${schemaName}.${tableName}`;
|
|
43
|
+
const qualifiedName = `${quotePgIdent(schemaName)}.${quotePgIdent(tableName)}`;
|
|
39
44
|
const statements = [];
|
|
40
45
|
const columnDefs = [
|
|
41
46
|
"_id BIGSERIAL PRIMARY KEY",
|
|
@@ -46,12 +51,12 @@ function emitTableDDL(schemaName, tableName, tableDef) {
|
|
|
46
51
|
for (const [colName, col] of Object.entries(tableDef.columns)) {
|
|
47
52
|
const sqlType = TYPE_MAP[col.type];
|
|
48
53
|
const nullable = col.nullable ? "" : " NOT NULL";
|
|
49
|
-
let colDef = `${colName} ${sqlType}${nullable}`;
|
|
54
|
+
let colDef = `${quotePgIdent(colName)} ${sqlType}${nullable}`;
|
|
50
55
|
if (col.default !== undefined) {
|
|
51
56
|
colDef += ` DEFAULT ${escapeLiteralDefault(col.default)}`;
|
|
52
57
|
}
|
|
53
58
|
if (col.type === "uint") {
|
|
54
|
-
colDef += ` CHECK (${colName} >= 0)`;
|
|
59
|
+
colDef += ` CHECK (${quotePgIdent(colName)} >= 0)`;
|
|
55
60
|
}
|
|
56
61
|
columnDefs.push(colDef);
|
|
57
62
|
}
|
|
@@ -59,30 +64,30 @@ function emitTableDDL(schemaName, tableName, tableDef) {
|
|
|
59
64
|
${columnDefs.join(`,
|
|
60
65
|
`)}
|
|
61
66
|
)`);
|
|
62
|
-
statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_block_height ON ${qualifiedName} (_block_height)`);
|
|
63
|
-
statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_tx_id ON ${qualifiedName} (_tx_id)`);
|
|
67
|
+
statements.push(`CREATE INDEX IF NOT EXISTS ${quotePgIdent(`idx_${schemaName}_${tableName}_block_height`)} ON ${qualifiedName} (_block_height)`);
|
|
68
|
+
statements.push(`CREATE INDEX IF NOT EXISTS ${quotePgIdent(`idx_${schemaName}_${tableName}_tx_id`)} ON ${qualifiedName} (_tx_id)`);
|
|
64
69
|
for (const [colName, col] of Object.entries(tableDef.columns)) {
|
|
65
70
|
if (col.indexed) {
|
|
66
|
-
statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_${colName} ON ${qualifiedName} (${colName})`);
|
|
71
|
+
statements.push(`CREATE INDEX IF NOT EXISTS ${quotePgIdent(`idx_${schemaName}_${tableName}_${colName}`)} ON ${qualifiedName} (${quotePgIdent(colName)})`);
|
|
67
72
|
}
|
|
68
73
|
}
|
|
69
74
|
for (const [colName, col] of Object.entries(tableDef.columns)) {
|
|
70
75
|
if (col.search) {
|
|
71
|
-
statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_${colName}_trgm ON ${qualifiedName} USING gin (${colName} gin_trgm_ops)`);
|
|
76
|
+
statements.push(`CREATE INDEX IF NOT EXISTS ${quotePgIdent(`idx_${schemaName}_${tableName}_${colName}_trgm`)} ON ${qualifiedName} USING gin (${quotePgIdent(colName)} gin_trgm_ops)`);
|
|
72
77
|
}
|
|
73
78
|
}
|
|
74
79
|
if (tableDef.indexes) {
|
|
75
80
|
for (let i = 0;i < tableDef.indexes.length; i++) {
|
|
76
81
|
const cols = tableDef.indexes[i];
|
|
77
|
-
const idxName = `idx_${schemaName}_${tableName}_composite_${i}
|
|
78
|
-
statements.push(`CREATE INDEX IF NOT EXISTS ${idxName} ON ${qualifiedName} (${cols.join(", ")})`);
|
|
82
|
+
const idxName = quotePgIdent(`idx_${schemaName}_${tableName}_composite_${i}`);
|
|
83
|
+
statements.push(`CREATE INDEX IF NOT EXISTS ${idxName} ON ${qualifiedName} (${cols.map(quotePgIdent).join(", ")})`);
|
|
79
84
|
}
|
|
80
85
|
}
|
|
81
86
|
if (tableDef.uniqueKeys) {
|
|
82
87
|
for (let i = 0;i < tableDef.uniqueKeys.length; i++) {
|
|
83
88
|
const cols = tableDef.uniqueKeys[i];
|
|
84
|
-
const constraintName = `uq_${schemaName}_${tableName}_${cols.join("_")}
|
|
85
|
-
statements.push(`ALTER TABLE ${qualifiedName} ADD CONSTRAINT ${constraintName} UNIQUE (${cols.join(", ")})`);
|
|
89
|
+
const constraintName = quotePgIdent(`uq_${schemaName}_${tableName}_${cols.join("_")}`);
|
|
90
|
+
statements.push(`ALTER TABLE ${qualifiedName} ADD CONSTRAINT ${constraintName} UNIQUE (${cols.map(quotePgIdent).join(", ")})`);
|
|
86
91
|
}
|
|
87
92
|
}
|
|
88
93
|
return statements;
|
|
@@ -114,8 +119,8 @@ function emitFactoryDDL(schemaName) {
|
|
|
114
119
|
}
|
|
115
120
|
function emitForeignKeyDDL(schemaName, tableName, tableDef) {
|
|
116
121
|
return (tableDef.relations ?? []).map((rel) => {
|
|
117
|
-
const constraintName = `fk_${schemaName}_${tableName}_${rel.name}
|
|
118
|
-
return `ALTER TABLE ${schemaName}.${tableName} ADD CONSTRAINT ${constraintName} ` + `FOREIGN KEY (${rel.fields.join(", ")}) ` + `REFERENCES ${schemaName}.${rel.references} (${rel.referencedColumns.join(", ")})`;
|
|
122
|
+
const constraintName = quotePgIdent(`fk_${schemaName}_${tableName}_${rel.name}`);
|
|
123
|
+
return `ALTER TABLE ${quotePgIdent(schemaName)}.${quotePgIdent(tableName)} ADD CONSTRAINT ${constraintName} ` + `FOREIGN KEY (${rel.fields.map(quotePgIdent).join(", ")}) ` + `REFERENCES ${quotePgIdent(schemaName)}.${quotePgIdent(rel.references)} (${rel.referencedColumns.map(quotePgIdent).join(", ")})`;
|
|
119
124
|
});
|
|
120
125
|
}
|
|
121
126
|
function generateSubgraphSQL(def, schemaNameOverride) {
|
|
@@ -1627,9 +1632,11 @@ import { resolveTraitContractIds } from "@secondlayer/shared/db/queries/contract
|
|
|
1627
1632
|
import { advanceOperationCursor } from "@secondlayer/shared/db/queries/subgraph-operations";
|
|
1628
1633
|
import {
|
|
1629
1634
|
isByoSubgraph,
|
|
1635
|
+
recordLiveError,
|
|
1630
1636
|
recordLiveProgress,
|
|
1631
1637
|
recordSubgraphProcessed,
|
|
1632
1638
|
resolveSubgraphDb,
|
|
1639
|
+
rewindLiveProgress,
|
|
1633
1640
|
updateSubgraphStatus
|
|
1634
1641
|
} from "@secondlayer/shared/db/queries/subgraphs";
|
|
1635
1642
|
import { logger as logger6 } from "@secondlayer/shared/logger";
|
|
@@ -2337,10 +2344,20 @@ class CursorRaceLostError extends Error {
|
|
|
2337
2344
|
this.name = "CursorRaceLostError";
|
|
2338
2345
|
}
|
|
2339
2346
|
}
|
|
2347
|
+
|
|
2348
|
+
class LiveCursorRaceLostError extends Error {
|
|
2349
|
+
constructor(subgraphName, height) {
|
|
2350
|
+
super(`live cursor race lost for ${subgraphName} at block ${height}`);
|
|
2351
|
+
this.name = "LiveCursorRaceLostError";
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2340
2354
|
function opCursorMode(opts) {
|
|
2341
2355
|
const ap = opts?.atomicProgress;
|
|
2342
2356
|
return ap && "operationId" in ap ? ap : undefined;
|
|
2343
2357
|
}
|
|
2358
|
+
function isReorgRewind(opts) {
|
|
2359
|
+
return opts?.reorgRewind === true;
|
|
2360
|
+
}
|
|
2344
2361
|
function statusMode(opts) {
|
|
2345
2362
|
const ap = opts?.atomicProgress;
|
|
2346
2363
|
return ap && "status" in ap ? ap : undefined;
|
|
@@ -2410,7 +2427,11 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
|
|
|
2410
2427
|
result.matched = matched.length;
|
|
2411
2428
|
if (matched.length === 0) {
|
|
2412
2429
|
if (!opts?.skipProgressUpdate) {
|
|
2413
|
-
|
|
2430
|
+
if (isReorgRewind(opts)) {
|
|
2431
|
+
await rewindLiveProgress(targetDb, subgraphName, blockHeight);
|
|
2432
|
+
} else {
|
|
2433
|
+
await recordLiveProgress(targetDb, subgraphName, blockHeight);
|
|
2434
|
+
}
|
|
2414
2435
|
}
|
|
2415
2436
|
return result;
|
|
2416
2437
|
}
|
|
@@ -2429,13 +2450,23 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
|
|
|
2429
2450
|
};
|
|
2430
2451
|
let handlerMs = 0;
|
|
2431
2452
|
let flushMs = 0;
|
|
2432
|
-
const applyProgress = async (tx, rr) => {
|
|
2453
|
+
const applyProgress = async (tx, rr, flushedWrites) => {
|
|
2433
2454
|
if (opts?.skipProgressUpdate)
|
|
2434
2455
|
return;
|
|
2456
|
+
const rewind = isReorgRewind(opts);
|
|
2435
2457
|
if (rr.errors > 0 && rr.processed === 0) {
|
|
2436
|
-
|
|
2458
|
+
if (rewind) {
|
|
2459
|
+
await updateSubgraphStatus(tx, subgraphName, "error", blockHeight);
|
|
2460
|
+
} else {
|
|
2461
|
+
await recordLiveError(tx, subgraphName, blockHeight);
|
|
2462
|
+
}
|
|
2463
|
+
} else if (rewind) {
|
|
2464
|
+
await rewindLiveProgress(tx, subgraphName, blockHeight);
|
|
2437
2465
|
} else {
|
|
2438
|
-
await recordLiveProgress(tx, subgraphName, blockHeight);
|
|
2466
|
+
const advanced = await recordLiveProgress(tx, subgraphName, blockHeight);
|
|
2467
|
+
if (!advanced && flushedWrites && !route.byo) {
|
|
2468
|
+
throw new LiveCursorRaceLostError(subgraphName, blockHeight);
|
|
2469
|
+
}
|
|
2439
2470
|
}
|
|
2440
2471
|
if (rr.processed > 0 || rr.errors > 0) {
|
|
2441
2472
|
const lastError = rr.errors > 0 ? `${rr.errors} error(s) at block ${blockHeight}` : undefined;
|
|
@@ -2484,13 +2515,15 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
|
|
|
2484
2515
|
} else if (byoOm && manifest && manifest.count > 0) {
|
|
2485
2516
|
await advanceOperationCursor(tx, byoOm.operationId, blockHeight);
|
|
2486
2517
|
}
|
|
2487
|
-
await applyProgress(tx, runResult);
|
|
2518
|
+
await applyProgress(tx, runResult, manifest ? manifest.count > 0 : false);
|
|
2488
2519
|
});
|
|
2489
2520
|
} else {
|
|
2490
2521
|
try {
|
|
2491
2522
|
await targetDb.transaction().execute(async (tx) => {
|
|
2492
2523
|
const opMode = opCursorMode(opts);
|
|
2493
|
-
|
|
2524
|
+
const sm = statusMode(opts);
|
|
2525
|
+
const rewind = isReorgRewind(opts);
|
|
2526
|
+
if (sm) {
|
|
2494
2527
|
const row = await tx.selectFrom("subgraphs").select("last_processed_block").where("name", "=", subgraphName).executeTakeFirst();
|
|
2495
2528
|
if (row && Number(row.last_processed_block) >= blockHeight) {
|
|
2496
2529
|
result.skipped = true;
|
|
@@ -2502,6 +2535,12 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
|
|
|
2502
2535
|
result.skipped = true;
|
|
2503
2536
|
return;
|
|
2504
2537
|
}
|
|
2538
|
+
} else if (!rewind) {
|
|
2539
|
+
const row = await tx.selectFrom("subgraphs").select("last_processed_block").where("name", "=", subgraphName).executeTakeFirst();
|
|
2540
|
+
if (row && Number(row.last_processed_block) >= blockHeight) {
|
|
2541
|
+
result.skipped = true;
|
|
2542
|
+
return;
|
|
2543
|
+
}
|
|
2505
2544
|
}
|
|
2506
2545
|
const ctx = new SubgraphContext(tx, schemaName, subgraph.schema, blockMeta, initialTx, false, journalEnabled(opts));
|
|
2507
2546
|
const handlerStart = performance.now();
|
|
@@ -2519,7 +2558,6 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
|
|
|
2519
2558
|
}
|
|
2520
2559
|
flushMs = performance.now() - flushStart;
|
|
2521
2560
|
}
|
|
2522
|
-
const sm = statusMode(opts);
|
|
2523
2561
|
if (sm && flushedWrites) {
|
|
2524
2562
|
await updateSubgraphStatus(tx, subgraphName, sm.status, blockHeight);
|
|
2525
2563
|
} else if (opMode && flushedWrites) {
|
|
@@ -2528,10 +2566,10 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
|
|
|
2528
2566
|
throw new CursorRaceLostError(opMode.operationId, blockHeight);
|
|
2529
2567
|
}
|
|
2530
2568
|
}
|
|
2531
|
-
await applyProgress(tx, runResult);
|
|
2569
|
+
await applyProgress(tx, runResult, flushedWrites);
|
|
2532
2570
|
});
|
|
2533
2571
|
} catch (err) {
|
|
2534
|
-
if (err instanceof CursorRaceLostError) {
|
|
2572
|
+
if (err instanceof CursorRaceLostError || err instanceof LiveCursorRaceLostError) {
|
|
2535
2573
|
logger6.warn("cursor race lost — block already covered", {
|
|
2536
2574
|
subgraph: subgraphName,
|
|
2537
2575
|
blockHeight,
|
|
@@ -2544,11 +2582,13 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
|
|
|
2544
2582
|
}
|
|
2545
2583
|
}
|
|
2546
2584
|
const totalMs = performance.now() - blockStart;
|
|
2547
|
-
result.
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2585
|
+
if (!result.skipped) {
|
|
2586
|
+
result.timing = {
|
|
2587
|
+
totalMs: Math.round(totalMs),
|
|
2588
|
+
handlerMs: Math.round(handlerMs),
|
|
2589
|
+
flushMs: Math.round(flushMs)
|
|
2590
|
+
};
|
|
2591
|
+
}
|
|
2552
2592
|
if (blockHeight % 1000 === 0) {
|
|
2553
2593
|
try {
|
|
2554
2594
|
const tables = Object.keys(subgraph.schema);
|
|
@@ -3192,6 +3232,33 @@ async function backfillSubgraph(def, opts) {
|
|
|
3192
3232
|
import { getTargetDb as getTargetDb3 } from "@secondlayer/shared/db";
|
|
3193
3233
|
import { getSubgraph } from "@secondlayer/shared/db/queries/subgraphs";
|
|
3194
3234
|
import { logger as logger9 } from "@secondlayer/shared/logger";
|
|
3235
|
+
|
|
3236
|
+
// src/runtime/catchup-leader.ts
|
|
3237
|
+
import {
|
|
3238
|
+
SUBGRAPH_CATCHUP_LOCK_KEY,
|
|
3239
|
+
createPostgresLeaderBackend,
|
|
3240
|
+
withLeaderLock
|
|
3241
|
+
} from "@secondlayer/shared/leader";
|
|
3242
|
+
import { targetListenerUrl } from "@secondlayer/shared/queue/listener";
|
|
3243
|
+
var catchUpLeader = false;
|
|
3244
|
+
function isCatchUpLeader() {
|
|
3245
|
+
return catchUpLeader;
|
|
3246
|
+
}
|
|
3247
|
+
function startCatchUpLeader(opts = {}) {
|
|
3248
|
+
return withLeaderLock(SUBGRAPH_CATCHUP_LOCK_KEY, async () => {
|
|
3249
|
+
catchUpLeader = true;
|
|
3250
|
+
await opts.onAcquire?.();
|
|
3251
|
+
return () => {
|
|
3252
|
+
catchUpLeader = false;
|
|
3253
|
+
};
|
|
3254
|
+
}, {
|
|
3255
|
+
pollMs: opts.pollMs,
|
|
3256
|
+
heartbeatMs: opts.heartbeatMs,
|
|
3257
|
+
createBackend: opts.createBackend ?? (() => createPostgresLeaderBackend(targetListenerUrl()))
|
|
3258
|
+
});
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3261
|
+
// src/runtime/catchup.ts
|
|
3195
3262
|
var LOG_INTERVAL2 = 1000;
|
|
3196
3263
|
var STANDARD_CATCHUP_BATCH_CONFIG = {
|
|
3197
3264
|
defaultBatchSize: 500,
|
|
@@ -3313,6 +3380,13 @@ async function catchUpSubgraph(subgraph, subgraphName) {
|
|
|
3313
3380
|
let prefetchedBatchEnd = Math.min(currentHeight + batchSize - 1, chainTip);
|
|
3314
3381
|
let nextBatchPromise = batchConfig.prefetch ? source.loadBlockRange(currentHeight, prefetchedBatchEnd) : undefined;
|
|
3315
3382
|
while (currentHeight <= chainTip) {
|
|
3383
|
+
if (!isCatchUpLeader()) {
|
|
3384
|
+
logger9.info("Lost catch-up leadership, stopping catch-up", {
|
|
3385
|
+
subgraph: subgraphName,
|
|
3386
|
+
processed
|
|
3387
|
+
});
|
|
3388
|
+
break;
|
|
3389
|
+
}
|
|
3316
3390
|
const currentRow = await getSubgraph(targetDb, subgraphName);
|
|
3317
3391
|
if (!currentRow || currentRow.status !== "active") {
|
|
3318
3392
|
logger9.info("Subgraph status changed, stopping catch-up", {
|
|
@@ -3422,6 +3496,7 @@ import {
|
|
|
3422
3496
|
resolveSubgraphRawClient
|
|
3423
3497
|
} from "@secondlayer/shared/db/queries/subgraphs";
|
|
3424
3498
|
import { logger as logger10 } from "@secondlayer/shared/logger";
|
|
3499
|
+
import { sql as sql4 } from "kysely";
|
|
3425
3500
|
async function handleSubgraphReorg(blockHeight, loadSubgraphDef) {
|
|
3426
3501
|
const targetDb = getTargetDb4();
|
|
3427
3502
|
const activeSubgraphs = (await listSubgraphs(targetDb)).filter((v) => v.status === "active");
|
|
@@ -3438,96 +3513,9 @@ async function handleSubgraphReorg(blockHeight, loadSubgraphDef) {
|
|
|
3438
3513
|
continue;
|
|
3439
3514
|
bumpReorgEpoch(sg.name);
|
|
3440
3515
|
await withSubgraphBlockLock(sg.name, async () => {
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
const revertedByTable = {};
|
|
3445
|
-
for (const tableName of tableNames) {
|
|
3446
|
-
const rows = await client.unsafe(`SELECT * FROM "${schemaName}"."${tableName}" WHERE "_block_height" >= $1 LIMIT 1000`, [blockHeight]);
|
|
3447
|
-
if (rows.length > 0)
|
|
3448
|
-
revertedByTable[tableName] = rows;
|
|
3449
|
-
}
|
|
3450
|
-
const hasJournal = (await client.unsafe(`SELECT to_regclass('"${schemaName}"."_journal"') AS r`))[0]?.r;
|
|
3451
|
-
if (hasJournal) {
|
|
3452
|
-
await client.begin(async (tx) => {
|
|
3453
|
-
for (const tableName of tableNames) {
|
|
3454
|
-
const earliest = `
|
|
3455
|
-
SELECT DISTINCT ON (row_key) row_key, prev_row
|
|
3456
|
-
FROM "${schemaName}"."_journal"
|
|
3457
|
-
WHERE block_height >= $1 AND table_name = $2
|
|
3458
|
-
ORDER BY row_key, _jid ASC`;
|
|
3459
|
-
await tx.unsafe(`DELETE FROM "${schemaName}"."${tableName}" t USING (${earliest}) e WHERE to_jsonb(t.*) @> e.row_key`, [blockHeight, tableName]);
|
|
3460
|
-
await tx.unsafe(`DELETE FROM "${schemaName}"."${tableName}" WHERE "_block_height" >= $1`, [blockHeight]);
|
|
3461
|
-
await tx.unsafe(`INSERT INTO "${schemaName}"."${tableName}"
|
|
3462
|
-
SELECT r.* FROM (${earliest}) e
|
|
3463
|
-
CROSS JOIN LATERAL jsonb_populate_record(NULL::"${schemaName}"."${tableName}", e.prev_row) r
|
|
3464
|
-
WHERE e.prev_row IS NOT NULL`, [blockHeight, tableName]);
|
|
3465
|
-
}
|
|
3466
|
-
await tx.unsafe(`DELETE FROM "${schemaName}"."_journal" WHERE block_height >= $1`, [blockHeight]);
|
|
3467
|
-
});
|
|
3468
|
-
} else {
|
|
3469
|
-
logger10.warn("Subgraph has no revert journal — falling back to height delete (accumulator rows may lose history)", { subgraph: sg.name, blockHeight });
|
|
3470
|
-
for (const tableName of tableNames) {
|
|
3471
|
-
await client.unsafe(`DELETE FROM "${schemaName}"."${tableName}" WHERE "_block_height" >= $1`, [blockHeight]);
|
|
3472
|
-
}
|
|
3473
|
-
}
|
|
3474
|
-
await client.unsafe(`DO $$ BEGIN
|
|
3475
|
-
IF to_regclass('"${schemaName}"."_factory_addresses"') IS NOT NULL THEN
|
|
3476
|
-
DELETE FROM "${schemaName}"."_factory_addresses" WHERE block_height >= ${Number(blockHeight)};
|
|
3477
|
-
END IF;
|
|
3478
|
-
END $$`);
|
|
3479
|
-
for (const [tableName, rows] of Object.entries(revertedByTable)) {
|
|
3480
|
-
if (rows.length === 0)
|
|
3481
|
-
continue;
|
|
3482
|
-
await targetDb.insertInto("subscription_outbox").columns([
|
|
3483
|
-
"subscription_id",
|
|
3484
|
-
"subgraph_name",
|
|
3485
|
-
"table_name",
|
|
3486
|
-
"block_height",
|
|
3487
|
-
"event_type",
|
|
3488
|
-
"payload",
|
|
3489
|
-
"dedup_key",
|
|
3490
|
-
"row_pk",
|
|
3491
|
-
"status"
|
|
3492
|
-
]).expression((eb) => eb.selectFrom("subscriptions").select((eb2) => [
|
|
3493
|
-
"id as subscription_id",
|
|
3494
|
-
eb2.val(sg.name).as("subgraph_name"),
|
|
3495
|
-
eb2.val(tableName).as("table_name"),
|
|
3496
|
-
eb2.val(blockHeight).as("block_height"),
|
|
3497
|
-
eb2.val(`${sg.name}.${tableName}.reverted`).as("event_type"),
|
|
3498
|
-
eb2.val(JSON.stringify({
|
|
3499
|
-
subgraph: sg.name,
|
|
3500
|
-
table: tableName,
|
|
3501
|
-
reorgFromHeight: blockHeight,
|
|
3502
|
-
rows
|
|
3503
|
-
})).as("payload"),
|
|
3504
|
-
eb2.val(`reorg:${sg.name}:${tableName}:${blockHeight}`).as("dedup_key"),
|
|
3505
|
-
eb2.val({
|
|
3506
|
-
subgraph: sg.name,
|
|
3507
|
-
table: tableName,
|
|
3508
|
-
reorgRoot: blockHeight
|
|
3509
|
-
}).as("row_pk"),
|
|
3510
|
-
eb2.val("pending").as("status")
|
|
3511
|
-
]).where("subgraph_name", "=", sg.name).where("table_name", "=", tableName).where("status", "=", "active")).onConflict((oc) => oc.columns(["subscription_id", "dedup_key"]).doNothing()).execute().catch((err) => {
|
|
3512
|
-
logger10.error("Failed to emit revert event for subscriptions", {
|
|
3513
|
-
event: "reorg_revert_emit_dropped",
|
|
3514
|
-
subgraph: sg.name,
|
|
3515
|
-
table: tableName,
|
|
3516
|
-
blockHeight,
|
|
3517
|
-
error: getErrorMessage4(err)
|
|
3518
|
-
});
|
|
3519
|
-
});
|
|
3520
|
-
}
|
|
3521
|
-
logger10.info("Subgraph reorg cleanup done", {
|
|
3522
|
-
subgraph: sg.name,
|
|
3523
|
-
blockHeight,
|
|
3524
|
-
tablesAffected: Object.keys(revertedByTable).length
|
|
3525
|
-
});
|
|
3526
|
-
const def = await loadSubgraphDef(sg);
|
|
3527
|
-
await processBlock(def, sg.name, blockHeight);
|
|
3528
|
-
logger10.info("Subgraph reorg reprocessed", {
|
|
3529
|
-
subgraph: sg.name,
|
|
3530
|
-
blockHeight
|
|
3516
|
+
await getTargetDb4().transaction().execute(async (lockTx) => {
|
|
3517
|
+
await sql4`SELECT pg_advisory_xact_lock(hashtext(${`subgraph-reorg:${sg.name}`}))`.execute(lockTx);
|
|
3518
|
+
await reorgOneSubgraph(sg, schema, blockHeight, loadSubgraphDef);
|
|
3531
3519
|
});
|
|
3532
3520
|
});
|
|
3533
3521
|
} catch (err) {
|
|
@@ -3539,6 +3527,100 @@ async function handleSubgraphReorg(blockHeight, loadSubgraphDef) {
|
|
|
3539
3527
|
}
|
|
3540
3528
|
}
|
|
3541
3529
|
}
|
|
3530
|
+
async function reorgOneSubgraph(sg, schema, blockHeight, loadSubgraphDef) {
|
|
3531
|
+
const targetDb = getTargetDb4();
|
|
3532
|
+
const schemaName = sg.schema_name ?? pgSchemaName(sg.name);
|
|
3533
|
+
const client = resolveSubgraphRawClient(sg);
|
|
3534
|
+
const tableNames = Object.keys(schema);
|
|
3535
|
+
const revertedByTable = {};
|
|
3536
|
+
for (const tableName of tableNames) {
|
|
3537
|
+
const rows = await client.unsafe(`SELECT * FROM "${schemaName}"."${tableName}" WHERE "_block_height" >= $1 LIMIT 1000`, [blockHeight]);
|
|
3538
|
+
if (rows.length > 0)
|
|
3539
|
+
revertedByTable[tableName] = rows;
|
|
3540
|
+
}
|
|
3541
|
+
const hasJournal = (await client.unsafe(`SELECT to_regclass('"${schemaName}"."_journal"') AS r`))[0]?.r;
|
|
3542
|
+
if (hasJournal) {
|
|
3543
|
+
await client.begin(async (tx) => {
|
|
3544
|
+
for (const tableName of tableNames) {
|
|
3545
|
+
const earliest = `
|
|
3546
|
+
SELECT DISTINCT ON (row_key) row_key, prev_row
|
|
3547
|
+
FROM "${schemaName}"."_journal"
|
|
3548
|
+
WHERE block_height >= $1 AND table_name = $2
|
|
3549
|
+
ORDER BY row_key, _jid ASC`;
|
|
3550
|
+
await tx.unsafe(`DELETE FROM "${schemaName}"."${tableName}" t USING (${earliest}) e WHERE to_jsonb(t.*) @> e.row_key`, [blockHeight, tableName]);
|
|
3551
|
+
await tx.unsafe(`DELETE FROM "${schemaName}"."${tableName}" WHERE "_block_height" >= $1`, [blockHeight]);
|
|
3552
|
+
await tx.unsafe(`INSERT INTO "${schemaName}"."${tableName}"
|
|
3553
|
+
SELECT r.* FROM (${earliest}) e
|
|
3554
|
+
CROSS JOIN LATERAL jsonb_populate_record(NULL::"${schemaName}"."${tableName}", e.prev_row) r
|
|
3555
|
+
WHERE e.prev_row IS NOT NULL`, [blockHeight, tableName]);
|
|
3556
|
+
}
|
|
3557
|
+
await tx.unsafe(`DELETE FROM "${schemaName}"."_journal" WHERE block_height >= $1`, [blockHeight]);
|
|
3558
|
+
});
|
|
3559
|
+
} else {
|
|
3560
|
+
logger10.warn("Subgraph has no revert journal — falling back to height delete (accumulator rows may lose history)", { subgraph: sg.name, blockHeight });
|
|
3561
|
+
for (const tableName of tableNames) {
|
|
3562
|
+
await client.unsafe(`DELETE FROM "${schemaName}"."${tableName}" WHERE "_block_height" >= $1`, [blockHeight]);
|
|
3563
|
+
}
|
|
3564
|
+
}
|
|
3565
|
+
await client.unsafe(`DO $$ BEGIN
|
|
3566
|
+
IF to_regclass('"${schemaName}"."_factory_addresses"') IS NOT NULL THEN
|
|
3567
|
+
DELETE FROM "${schemaName}"."_factory_addresses" WHERE block_height >= ${Number(blockHeight)};
|
|
3568
|
+
END IF;
|
|
3569
|
+
END $$`);
|
|
3570
|
+
for (const [tableName, rows] of Object.entries(revertedByTable)) {
|
|
3571
|
+
if (rows.length === 0)
|
|
3572
|
+
continue;
|
|
3573
|
+
await targetDb.insertInto("subscription_outbox").columns([
|
|
3574
|
+
"subscription_id",
|
|
3575
|
+
"subgraph_name",
|
|
3576
|
+
"table_name",
|
|
3577
|
+
"block_height",
|
|
3578
|
+
"event_type",
|
|
3579
|
+
"payload",
|
|
3580
|
+
"dedup_key",
|
|
3581
|
+
"row_pk",
|
|
3582
|
+
"status"
|
|
3583
|
+
]).expression((eb) => eb.selectFrom("subscriptions").select((eb2) => [
|
|
3584
|
+
"id as subscription_id",
|
|
3585
|
+
eb2.val(sg.name).as("subgraph_name"),
|
|
3586
|
+
eb2.val(tableName).as("table_name"),
|
|
3587
|
+
eb2.val(blockHeight).as("block_height"),
|
|
3588
|
+
eb2.val(`${sg.name}.${tableName}.reverted`).as("event_type"),
|
|
3589
|
+
eb2.val(JSON.stringify({
|
|
3590
|
+
subgraph: sg.name,
|
|
3591
|
+
table: tableName,
|
|
3592
|
+
reorgFromHeight: blockHeight,
|
|
3593
|
+
rows
|
|
3594
|
+
})).as("payload"),
|
|
3595
|
+
eb2.val(`reorg:${sg.name}:${tableName}:${blockHeight}`).as("dedup_key"),
|
|
3596
|
+
eb2.val({
|
|
3597
|
+
subgraph: sg.name,
|
|
3598
|
+
table: tableName,
|
|
3599
|
+
reorgRoot: blockHeight
|
|
3600
|
+
}).as("row_pk"),
|
|
3601
|
+
eb2.val("pending").as("status")
|
|
3602
|
+
]).where("subgraph_name", "=", sg.name).where("table_name", "=", tableName).where("status", "=", "active")).onConflict((oc) => oc.columns(["subscription_id", "dedup_key"]).doNothing()).execute().catch((err) => {
|
|
3603
|
+
logger10.error("Failed to emit revert event for subscriptions", {
|
|
3604
|
+
event: "reorg_revert_emit_dropped",
|
|
3605
|
+
subgraph: sg.name,
|
|
3606
|
+
table: tableName,
|
|
3607
|
+
blockHeight,
|
|
3608
|
+
error: getErrorMessage4(err)
|
|
3609
|
+
});
|
|
3610
|
+
});
|
|
3611
|
+
}
|
|
3612
|
+
logger10.info("Subgraph reorg cleanup done", {
|
|
3613
|
+
subgraph: sg.name,
|
|
3614
|
+
blockHeight,
|
|
3615
|
+
tablesAffected: Object.keys(revertedByTable).length
|
|
3616
|
+
});
|
|
3617
|
+
const def = await loadSubgraphDef(sg);
|
|
3618
|
+
await processBlock(def, sg.name, blockHeight, { reorgRewind: true });
|
|
3619
|
+
logger10.info("Subgraph reorg reprocessed", {
|
|
3620
|
+
subgraph: sg.name,
|
|
3621
|
+
blockHeight
|
|
3622
|
+
});
|
|
3623
|
+
}
|
|
3542
3624
|
|
|
3543
3625
|
// src/runtime/processor.ts
|
|
3544
3626
|
import { randomUUID } from "node:crypto";
|
|
@@ -3571,31 +3653,6 @@ import {
|
|
|
3571
3653
|
targetListenerUrl as targetListenerUrl2
|
|
3572
3654
|
} from "@secondlayer/shared/queue/listener";
|
|
3573
3655
|
|
|
3574
|
-
// src/runtime/catchup-leader.ts
|
|
3575
|
-
import {
|
|
3576
|
-
SUBGRAPH_CATCHUP_LOCK_KEY,
|
|
3577
|
-
createPostgresLeaderBackend,
|
|
3578
|
-
withLeaderLock
|
|
3579
|
-
} from "@secondlayer/shared/leader";
|
|
3580
|
-
import { targetListenerUrl } from "@secondlayer/shared/queue/listener";
|
|
3581
|
-
var catchUpLeader = false;
|
|
3582
|
-
function isCatchUpLeader() {
|
|
3583
|
-
return catchUpLeader;
|
|
3584
|
-
}
|
|
3585
|
-
function startCatchUpLeader(opts = {}) {
|
|
3586
|
-
return withLeaderLock(SUBGRAPH_CATCHUP_LOCK_KEY, async () => {
|
|
3587
|
-
catchUpLeader = true;
|
|
3588
|
-
await opts.onAcquire?.();
|
|
3589
|
-
return () => {
|
|
3590
|
-
catchUpLeader = false;
|
|
3591
|
-
};
|
|
3592
|
-
}, {
|
|
3593
|
-
pollMs: opts.pollMs,
|
|
3594
|
-
heartbeatMs: opts.heartbeatMs,
|
|
3595
|
-
createBackend: opts.createBackend ?? (() => createPostgresLeaderBackend(targetListenerUrl()))
|
|
3596
|
-
});
|
|
3597
|
-
}
|
|
3598
|
-
|
|
3599
3656
|
// src/runtime/streams-reorg-poll.ts
|
|
3600
3657
|
import { getErrorMessage as getErrorMessage5 } from "@secondlayer/shared";
|
|
3601
3658
|
import { IndexHttpClient as IndexHttpClient2 } from "@secondlayer/shared/index-http";
|
|
@@ -4013,12 +4070,12 @@ async function startSubgraphProcessor(opts) {
|
|
|
4013
4070
|
// src/service.ts
|
|
4014
4071
|
import { assertDbSplit, getDb } from "@secondlayer/shared/db";
|
|
4015
4072
|
import { logger as logger13 } from "@secondlayer/shared/logger";
|
|
4016
|
-
import { sql as
|
|
4073
|
+
import { sql as sql5 } from "kysely";
|
|
4017
4074
|
var HEARTBEAT_INTERVAL_MS2 = 30000;
|
|
4018
4075
|
var SERVICE_NAME = "subgraph-processor";
|
|
4019
4076
|
async function writeHeartbeat() {
|
|
4020
4077
|
try {
|
|
4021
|
-
await getDb().insertInto("service_heartbeats").values({ name: SERVICE_NAME }).onConflict((oc) => oc.column("name").doUpdateSet({ updated_at:
|
|
4078
|
+
await getDb().insertInto("service_heartbeats").values({ name: SERVICE_NAME }).onConflict((oc) => oc.column("name").doUpdateSet({ updated_at: sql5`now()` })).execute();
|
|
4022
4079
|
} catch (err) {
|
|
4023
4080
|
logger13.warn("subgraph-processor heartbeat write failed", {
|
|
4024
4081
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -4040,5 +4097,5 @@ var shutdown = async () => {
|
|
|
4040
4097
|
process.on("SIGINT", shutdown);
|
|
4041
4098
|
process.on("SIGTERM", shutdown);
|
|
4042
4099
|
|
|
4043
|
-
//# debugId=
|
|
4100
|
+
//# debugId=2FEADCB8BDBF199464756E2164756E21
|
|
4044
4101
|
//# sourceMappingURL=service.js.map
|