@secondlayer/subgraphs 1.2.2 → 1.3.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 +30 -14
- package/dist/src/index.js.map +3 -3
- package/dist/src/runtime/processor.d.ts +4 -1
- package/dist/src/runtime/processor.js +779 -65
- package/dist/src/runtime/processor.js.map +7 -5
- package/dist/src/runtime/reindex.js +30 -14
- package/dist/src/runtime/reindex.js.map +3 -3
- package/dist/src/service.js +779 -66
- package/dist/src/service.js.map +7 -5
- package/package.json +2 -2
|
@@ -1299,14 +1299,103 @@ class StatsAccumulator {
|
|
|
1299
1299
|
}
|
|
1300
1300
|
}
|
|
1301
1301
|
|
|
1302
|
-
// src/runtime/
|
|
1303
|
-
import {
|
|
1302
|
+
// src/runtime/reindex.ts
|
|
1303
|
+
import { getErrorMessage as getErrorMessage2 } from "@secondlayer/shared";
|
|
1304
|
+
import { getRawClient, getSourceDb as getSourceDb2, getTargetDb as getTargetDb2 } from "@secondlayer/shared/db";
|
|
1304
1305
|
import {
|
|
1305
|
-
recordGapBatch
|
|
1306
|
+
recordGapBatch,
|
|
1307
|
+
resolveGaps
|
|
1306
1308
|
} from "@secondlayer/shared/db/queries/subgraph-gaps";
|
|
1307
|
-
import {
|
|
1309
|
+
import {
|
|
1310
|
+
recordSubgraphProcessed as recordSubgraphProcessed2,
|
|
1311
|
+
updateSubgraphStatus as updateSubgraphStatus2
|
|
1312
|
+
} from "@secondlayer/shared/db/queries/subgraphs";
|
|
1308
1313
|
import { logger as logger5 } from "@secondlayer/shared/logger";
|
|
1309
1314
|
|
|
1315
|
+
// src/schema/generator.ts
|
|
1316
|
+
var TYPE_MAP = {
|
|
1317
|
+
text: "TEXT",
|
|
1318
|
+
uint: "NUMERIC",
|
|
1319
|
+
int: "NUMERIC",
|
|
1320
|
+
principal: "TEXT",
|
|
1321
|
+
boolean: "BOOLEAN",
|
|
1322
|
+
timestamp: "TIMESTAMPTZ",
|
|
1323
|
+
jsonb: "JSONB"
|
|
1324
|
+
};
|
|
1325
|
+
function escapeLiteralDefault(value) {
|
|
1326
|
+
if (value === null || value === undefined)
|
|
1327
|
+
return "NULL";
|
|
1328
|
+
if (typeof value === "number" || typeof value === "bigint")
|
|
1329
|
+
return String(value);
|
|
1330
|
+
if (typeof value === "boolean")
|
|
1331
|
+
return value ? "TRUE" : "FALSE";
|
|
1332
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
1333
|
+
}
|
|
1334
|
+
function generateSubgraphSQL(def, schemaNameOverride) {
|
|
1335
|
+
const schemaName = schemaNameOverride ?? pgSchemaName(def.name);
|
|
1336
|
+
const statements = [];
|
|
1337
|
+
const needsTrgm = Object.values(def.schema).some((table) => Object.values(table.columns).some((col) => col.search));
|
|
1338
|
+
if (needsTrgm) {
|
|
1339
|
+
statements.push(`CREATE EXTENSION IF NOT EXISTS pg_trgm`);
|
|
1340
|
+
}
|
|
1341
|
+
statements.push(`CREATE SCHEMA IF NOT EXISTS ${schemaName}`);
|
|
1342
|
+
for (const [tableName, tableDef] of Object.entries(def.schema)) {
|
|
1343
|
+
const qualifiedName = `${schemaName}.${tableName}`;
|
|
1344
|
+
const columnDefs = [
|
|
1345
|
+
`_id BIGSERIAL PRIMARY KEY`,
|
|
1346
|
+
`_block_height BIGINT NOT NULL`,
|
|
1347
|
+
`_tx_id TEXT NOT NULL`,
|
|
1348
|
+
`_created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()`
|
|
1349
|
+
];
|
|
1350
|
+
for (const [colName, col] of Object.entries(tableDef.columns)) {
|
|
1351
|
+
const sqlType = TYPE_MAP[col.type];
|
|
1352
|
+
const nullable = col.nullable ? "" : " NOT NULL";
|
|
1353
|
+
let colDef = `${colName} ${sqlType}${nullable}`;
|
|
1354
|
+
if (col.default !== undefined) {
|
|
1355
|
+
colDef += ` DEFAULT ${escapeLiteralDefault(col.default)}`;
|
|
1356
|
+
}
|
|
1357
|
+
columnDefs.push(colDef);
|
|
1358
|
+
}
|
|
1359
|
+
statements.push(`CREATE TABLE IF NOT EXISTS ${qualifiedName} (
|
|
1360
|
+
${columnDefs.join(`,
|
|
1361
|
+
`)}
|
|
1362
|
+
)`);
|
|
1363
|
+
statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_block_height ON ${qualifiedName} (_block_height)`);
|
|
1364
|
+
statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_tx_id ON ${qualifiedName} (_tx_id)`);
|
|
1365
|
+
for (const [colName, col] of Object.entries(tableDef.columns)) {
|
|
1366
|
+
if (col.indexed) {
|
|
1367
|
+
statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_${colName} ON ${qualifiedName} (${colName})`);
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
for (const [colName, col] of Object.entries(tableDef.columns)) {
|
|
1371
|
+
if (col.search) {
|
|
1372
|
+
statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_${colName}_trgm ON ${qualifiedName} USING gin (${colName} gin_trgm_ops)`);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
if (tableDef.indexes) {
|
|
1376
|
+
for (let i = 0;i < tableDef.indexes.length; i++) {
|
|
1377
|
+
const cols = tableDef.indexes[i];
|
|
1378
|
+
const idxName = `idx_${schemaName}_${tableName}_composite_${i}`;
|
|
1379
|
+
statements.push(`CREATE INDEX IF NOT EXISTS ${idxName} ON ${qualifiedName} (${cols.join(", ")})`);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
if (tableDef.uniqueKeys) {
|
|
1383
|
+
for (let i = 0;i < tableDef.uniqueKeys.length; i++) {
|
|
1384
|
+
const cols = tableDef.uniqueKeys[i];
|
|
1385
|
+
const constraintName = `uq_${schemaName}_${tableName}_${cols.join("_")}`;
|
|
1386
|
+
statements.push(`ALTER TABLE ${qualifiedName} ADD CONSTRAINT ${constraintName} UNIQUE (${cols.join(", ")})`);
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
const hashInput = JSON.stringify({
|
|
1391
|
+
name: def.name,
|
|
1392
|
+
schema: def.schema,
|
|
1393
|
+
sources: def.sources
|
|
1394
|
+
}, (_key, value) => typeof value === "bigint" ? value.toString() : value);
|
|
1395
|
+
const hash = String(Bun.hash(hashInput));
|
|
1396
|
+
return { statements, hash };
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1310
1399
|
// src/runtime/batch-loader.ts
|
|
1311
1400
|
async function loadBlockRange(db, fromHeight, toHeight) {
|
|
1312
1401
|
const [blocks, txs, events] = await Promise.all([
|
|
@@ -1349,8 +1438,417 @@ function avgEventsPerBlock(batch) {
|
|
|
1349
1438
|
return totalEvents / batch.size;
|
|
1350
1439
|
}
|
|
1351
1440
|
|
|
1352
|
-
// src/runtime/
|
|
1441
|
+
// src/runtime/reindex.ts
|
|
1353
1442
|
var LOG_INTERVAL = 1000;
|
|
1443
|
+
var HEALTH_FLUSH_INTERVAL = 1000;
|
|
1444
|
+
var HOBBY_REINDEX_BATCH_CONFIG = {
|
|
1445
|
+
defaultBatchSize: 50,
|
|
1446
|
+
minBatchSize: 25,
|
|
1447
|
+
maxBatchSize: 100
|
|
1448
|
+
};
|
|
1449
|
+
var STANDARD_REINDEX_BATCH_CONFIG = {
|
|
1450
|
+
defaultBatchSize: 500,
|
|
1451
|
+
minBatchSize: 100,
|
|
1452
|
+
maxBatchSize: 1000
|
|
1453
|
+
};
|
|
1454
|
+
function initialReindexProgressBlock(fromBlock) {
|
|
1455
|
+
return Math.max(0, fromBlock - 1);
|
|
1456
|
+
}
|
|
1457
|
+
function resolveReindexResumeBlock(row) {
|
|
1458
|
+
if (row.reindex_from_block == null || row.reindex_to_block == null) {
|
|
1459
|
+
return null;
|
|
1460
|
+
}
|
|
1461
|
+
const lastProcessedBlock = Number(row.last_processed_block);
|
|
1462
|
+
const reindexFromBlock = Number(row.reindex_from_block);
|
|
1463
|
+
return Math.max(lastProcessedBlock + 1, reindexFromBlock);
|
|
1464
|
+
}
|
|
1465
|
+
function parsePositiveInt(value) {
|
|
1466
|
+
if (value == null || value.trim() === "")
|
|
1467
|
+
return;
|
|
1468
|
+
const parsed = Number.parseInt(value, 10);
|
|
1469
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
1470
|
+
}
|
|
1471
|
+
function resolveReindexBatchConfig(env = process.env) {
|
|
1472
|
+
const base = env.TENANT_PLAN?.trim().toLowerCase() === "hobby" ? HOBBY_REINDEX_BATCH_CONFIG : STANDARD_REINDEX_BATCH_CONFIG;
|
|
1473
|
+
const minBatchSize = parsePositiveInt(env.SUBGRAPH_REINDEX_MIN_BATCH_SIZE) ?? base.minBatchSize;
|
|
1474
|
+
const maxBatchSize = parsePositiveInt(env.SUBGRAPH_REINDEX_MAX_BATCH_SIZE) ?? base.maxBatchSize;
|
|
1475
|
+
const defaultBatchSize = parsePositiveInt(env.SUBGRAPH_REINDEX_BATCH_SIZE) ?? base.defaultBatchSize;
|
|
1476
|
+
return {
|
|
1477
|
+
minBatchSize,
|
|
1478
|
+
maxBatchSize,
|
|
1479
|
+
defaultBatchSize: Math.min(Math.max(defaultBatchSize, minBatchSize), maxBatchSize)
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
function coalesceFailedBlocks(blocks) {
|
|
1483
|
+
if (blocks.length === 0)
|
|
1484
|
+
return [];
|
|
1485
|
+
blocks.sort((a, b) => a.height - b.height);
|
|
1486
|
+
const ranges = [];
|
|
1487
|
+
let start = blocks[0].height;
|
|
1488
|
+
let end = blocks[0].height;
|
|
1489
|
+
let reason = blocks[0].reason;
|
|
1490
|
+
for (let i = 1;i < blocks.length; i++) {
|
|
1491
|
+
const b = blocks[i];
|
|
1492
|
+
if (b.height === end + 1 && b.reason === reason) {
|
|
1493
|
+
end = b.height;
|
|
1494
|
+
} else {
|
|
1495
|
+
ranges.push({ start, end, reason });
|
|
1496
|
+
start = b.height;
|
|
1497
|
+
end = b.height;
|
|
1498
|
+
reason = b.reason;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
ranges.push({ start, end, reason });
|
|
1502
|
+
return ranges;
|
|
1503
|
+
}
|
|
1504
|
+
async function processBlockRange(def, opts) {
|
|
1505
|
+
const sourceDb = getSourceDb2();
|
|
1506
|
+
const targetDb = getTargetDb2();
|
|
1507
|
+
const subgraphName = def.name;
|
|
1508
|
+
const { fromBlock, toBlock, status } = opts;
|
|
1509
|
+
const totalBlocks = toBlock - fromBlock + 1;
|
|
1510
|
+
const stats = new StatsAccumulator(subgraphName, opts.isCatchup);
|
|
1511
|
+
let blocksProcessed = 0;
|
|
1512
|
+
let totalEventsProcessed = 0;
|
|
1513
|
+
let totalErrors = 0;
|
|
1514
|
+
let pendingEventsProcessed = 0;
|
|
1515
|
+
let pendingErrors = 0;
|
|
1516
|
+
let pendingLastError;
|
|
1517
|
+
let lastHealthFlushBlock = 0;
|
|
1518
|
+
const batchConfig = resolveReindexBatchConfig();
|
|
1519
|
+
let batchSize = batchConfig.defaultBatchSize;
|
|
1520
|
+
let currentHeight = fromBlock;
|
|
1521
|
+
let aborted = false;
|
|
1522
|
+
const flushHealth = async () => {
|
|
1523
|
+
if (pendingEventsProcessed === 0 && pendingErrors === 0)
|
|
1524
|
+
return;
|
|
1525
|
+
await recordSubgraphProcessed2(targetDb, subgraphName, pendingEventsProcessed, pendingErrors, pendingLastError);
|
|
1526
|
+
pendingEventsProcessed = 0;
|
|
1527
|
+
pendingErrors = 0;
|
|
1528
|
+
pendingLastError = undefined;
|
|
1529
|
+
lastHealthFlushBlock = blocksProcessed;
|
|
1530
|
+
};
|
|
1531
|
+
let nextBatchEnd = Math.min(currentHeight + batchSize - 1, toBlock);
|
|
1532
|
+
let nextBatchPromise = loadBlockRange(sourceDb, currentHeight, nextBatchEnd);
|
|
1533
|
+
while (currentHeight <= toBlock) {
|
|
1534
|
+
if (opts.signal?.aborted) {
|
|
1535
|
+
aborted = true;
|
|
1536
|
+
logger5.info("Block processing aborted", {
|
|
1537
|
+
subgraph: subgraphName,
|
|
1538
|
+
currentBlock: currentHeight,
|
|
1539
|
+
reason: String(opts.signal.reason ?? "unknown")
|
|
1540
|
+
});
|
|
1541
|
+
break;
|
|
1542
|
+
}
|
|
1543
|
+
const batch = await nextBatchPromise;
|
|
1544
|
+
const batchEnd = nextBatchEnd;
|
|
1545
|
+
const nextStart = batchEnd + 1;
|
|
1546
|
+
if (nextStart <= toBlock) {
|
|
1547
|
+
nextBatchEnd = Math.min(nextStart + batchSize - 1, toBlock);
|
|
1548
|
+
nextBatchPromise = loadBlockRange(sourceDb, nextStart, nextBatchEnd);
|
|
1549
|
+
}
|
|
1550
|
+
const batchFailedBlocks = [];
|
|
1551
|
+
for (let height = currentHeight;height <= batchEnd; height++) {
|
|
1552
|
+
const blockData = batch.get(height);
|
|
1553
|
+
if (!blockData) {
|
|
1554
|
+
batchFailedBlocks.push({ height, reason: "block_missing" });
|
|
1555
|
+
blocksProcessed++;
|
|
1556
|
+
continue;
|
|
1557
|
+
}
|
|
1558
|
+
let result;
|
|
1559
|
+
try {
|
|
1560
|
+
result = await processBlock(def, subgraphName, height, {
|
|
1561
|
+
skipProgressUpdate: true,
|
|
1562
|
+
preloaded: blockData
|
|
1563
|
+
});
|
|
1564
|
+
} catch (err) {
|
|
1565
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
1566
|
+
logger5.error("Block processing error", {
|
|
1567
|
+
subgraph: subgraphName,
|
|
1568
|
+
blockHeight: height,
|
|
1569
|
+
error: errorMsg
|
|
1570
|
+
});
|
|
1571
|
+
batchFailedBlocks.push({ height, reason: "processing_error" });
|
|
1572
|
+
await updateSubgraphStatus2(targetDb, subgraphName, status, height).catch(() => {});
|
|
1573
|
+
blocksProcessed++;
|
|
1574
|
+
totalErrors++;
|
|
1575
|
+
pendingErrors++;
|
|
1576
|
+
pendingLastError = errorMsg;
|
|
1577
|
+
continue;
|
|
1578
|
+
}
|
|
1579
|
+
blocksProcessed++;
|
|
1580
|
+
totalEventsProcessed += result.processed;
|
|
1581
|
+
totalErrors += result.errors;
|
|
1582
|
+
pendingEventsProcessed += result.processed;
|
|
1583
|
+
pendingErrors += result.errors;
|
|
1584
|
+
if (result.errors > 0) {
|
|
1585
|
+
pendingLastError = `${result.errors} error(s) while processing block ${height}`;
|
|
1586
|
+
}
|
|
1587
|
+
if (result.timing) {
|
|
1588
|
+
stats.record(result.timing, result.processed);
|
|
1589
|
+
if (stats.shouldFlush()) {
|
|
1590
|
+
await stats.flush(targetDb);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
if (blocksProcessed % 100 === 0) {
|
|
1594
|
+
await updateSubgraphStatus2(targetDb, subgraphName, status, height);
|
|
1595
|
+
}
|
|
1596
|
+
if (blocksProcessed % LOG_INTERVAL === 0) {
|
|
1597
|
+
logger5.info(`${status === "reindexing" ? "Reindex" : "Backfill"} progress`, {
|
|
1598
|
+
subgraph: subgraphName,
|
|
1599
|
+
processed: blocksProcessed,
|
|
1600
|
+
total: totalBlocks,
|
|
1601
|
+
currentBlock: height,
|
|
1602
|
+
pct: Math.round(blocksProcessed / totalBlocks * 100)
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
if (blocksProcessed - lastHealthFlushBlock >= HEALTH_FLUSH_INTERVAL) {
|
|
1607
|
+
await flushHealth();
|
|
1608
|
+
}
|
|
1609
|
+
if (batchFailedBlocks.length > 0 && opts.subgraphId) {
|
|
1610
|
+
const gaps = coalesceFailedBlocks(batchFailedBlocks);
|
|
1611
|
+
await recordGapBatch(targetDb, opts.subgraphId, subgraphName, gaps).catch((err) => {
|
|
1612
|
+
logger5.warn("Failed to record subgraph gaps", {
|
|
1613
|
+
subgraph: subgraphName,
|
|
1614
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1615
|
+
});
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
const avg = avgEventsPerBlock(batch);
|
|
1619
|
+
if (avg > 50)
|
|
1620
|
+
batchSize = Math.max(Math.round(batchSize * 0.5), batchConfig.minBatchSize);
|
|
1621
|
+
else if (avg < 10)
|
|
1622
|
+
batchSize = Math.min(Math.round(batchSize * 1.5), batchConfig.maxBatchSize);
|
|
1623
|
+
currentHeight = batchEnd + 1;
|
|
1624
|
+
}
|
|
1625
|
+
await stats.flush(targetDb);
|
|
1626
|
+
await flushHealth();
|
|
1627
|
+
return { blocksProcessed, totalEventsProcessed, totalErrors, aborted };
|
|
1628
|
+
}
|
|
1629
|
+
async function resolveBlockRange(sourceDb, def, opts) {
|
|
1630
|
+
const fromBlock = opts?.fromBlock ?? def.startBlock ?? 1;
|
|
1631
|
+
let toBlock;
|
|
1632
|
+
if (opts?.toBlock != null) {
|
|
1633
|
+
toBlock = opts.toBlock;
|
|
1634
|
+
} else {
|
|
1635
|
+
const progress = await sourceDb.selectFrom("index_progress").selectAll().where("network", "=", process.env.NETWORK ?? "mainnet").executeTakeFirst();
|
|
1636
|
+
toBlock = progress?.highest_seen_block ?? 0;
|
|
1637
|
+
}
|
|
1638
|
+
return { fromBlock, toBlock };
|
|
1639
|
+
}
|
|
1640
|
+
async function clearReindexMetadata(db, subgraphName) {
|
|
1641
|
+
await db.updateTable("subgraphs").set({ reindex_from_block: null, reindex_to_block: null }).where("name", "=", subgraphName).execute();
|
|
1642
|
+
}
|
|
1643
|
+
async function reindexSubgraph(def, opts) {
|
|
1644
|
+
const sourceDb = getSourceDb2();
|
|
1645
|
+
const targetDb = getTargetDb2();
|
|
1646
|
+
const client = getRawClient("target");
|
|
1647
|
+
const subgraphName = def.name;
|
|
1648
|
+
const schemaName = opts?.schemaName ?? pgSchemaName(subgraphName);
|
|
1649
|
+
await updateSubgraphStatus2(targetDb, subgraphName, "reindexing");
|
|
1650
|
+
logger5.info("Reindex starting", { subgraph: subgraphName });
|
|
1651
|
+
try {
|
|
1652
|
+
const { fromBlock, toBlock } = await resolveBlockRange(sourceDb, def, opts);
|
|
1653
|
+
if (fromBlock > toBlock) {
|
|
1654
|
+
logger5.info("No blocks to reindex", {
|
|
1655
|
+
subgraph: subgraphName,
|
|
1656
|
+
fromBlock,
|
|
1657
|
+
toBlock
|
|
1658
|
+
});
|
|
1659
|
+
await updateSubgraphStatus2(targetDb, subgraphName, "active", 0);
|
|
1660
|
+
return { processed: 0 };
|
|
1661
|
+
}
|
|
1662
|
+
await client.unsafe(`DROP SCHEMA IF EXISTS "${schemaName}" CASCADE`);
|
|
1663
|
+
const { statements } = generateSubgraphSQL(def, schemaName);
|
|
1664
|
+
for (const stmt of statements) {
|
|
1665
|
+
await client.unsafe(stmt);
|
|
1666
|
+
}
|
|
1667
|
+
logger5.info("Schema recreated for reindex", { subgraph: subgraphName });
|
|
1668
|
+
await targetDb.updateTable("subgraphs").set({
|
|
1669
|
+
last_processed_block: initialReindexProgressBlock(fromBlock),
|
|
1670
|
+
reindex_from_block: fromBlock,
|
|
1671
|
+
reindex_to_block: toBlock,
|
|
1672
|
+
total_processed: 0,
|
|
1673
|
+
total_errors: 0,
|
|
1674
|
+
last_error: null,
|
|
1675
|
+
last_error_at: null,
|
|
1676
|
+
updated_at: new Date
|
|
1677
|
+
}).where("name", "=", subgraphName).execute();
|
|
1678
|
+
logger5.info("Reindexing blocks", {
|
|
1679
|
+
subgraph: subgraphName,
|
|
1680
|
+
fromBlock,
|
|
1681
|
+
toBlock,
|
|
1682
|
+
totalBlocks: toBlock - fromBlock + 1
|
|
1683
|
+
});
|
|
1684
|
+
const subgraphRow = await targetDb.selectFrom("subgraphs").select(["id"]).where("name", "=", subgraphName).executeTakeFirst();
|
|
1685
|
+
const result = await processBlockRange(def, {
|
|
1686
|
+
fromBlock,
|
|
1687
|
+
toBlock,
|
|
1688
|
+
status: "reindexing",
|
|
1689
|
+
isCatchup: false,
|
|
1690
|
+
apiKeyId: null,
|
|
1691
|
+
subgraphId: subgraphRow?.id,
|
|
1692
|
+
signal: opts?.signal
|
|
1693
|
+
});
|
|
1694
|
+
if (result.aborted) {
|
|
1695
|
+
const reason = String(opts?.signal?.reason ?? "unknown");
|
|
1696
|
+
if (reason === "user-cancelled") {
|
|
1697
|
+
await updateSubgraphStatus2(targetDb, subgraphName, "active");
|
|
1698
|
+
await clearReindexMetadata(targetDb, subgraphName);
|
|
1699
|
+
logger5.info("Reindex cancelled by user", { subgraph: subgraphName });
|
|
1700
|
+
} else {
|
|
1701
|
+
logger5.info("Reindex interrupted by shutdown, will resume", {
|
|
1702
|
+
subgraph: subgraphName
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
return { processed: result.blocksProcessed };
|
|
1706
|
+
}
|
|
1707
|
+
await updateSubgraphStatus2(targetDb, subgraphName, "active", toBlock);
|
|
1708
|
+
await clearReindexMetadata(targetDb, subgraphName);
|
|
1709
|
+
logger5.info("Reindex complete", {
|
|
1710
|
+
subgraph: subgraphName,
|
|
1711
|
+
blocks: result.blocksProcessed,
|
|
1712
|
+
events: result.totalEventsProcessed,
|
|
1713
|
+
errors: result.totalErrors
|
|
1714
|
+
});
|
|
1715
|
+
return { processed: result.blocksProcessed };
|
|
1716
|
+
} catch (err) {
|
|
1717
|
+
logger5.error("Reindex failed", {
|
|
1718
|
+
subgraph: subgraphName,
|
|
1719
|
+
error: getErrorMessage2(err)
|
|
1720
|
+
});
|
|
1721
|
+
await updateSubgraphStatus2(targetDb, subgraphName, "error");
|
|
1722
|
+
throw err;
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
async function resumeReindex(def, opts) {
|
|
1726
|
+
const targetDb = getTargetDb2();
|
|
1727
|
+
const subgraphName = def.name;
|
|
1728
|
+
const row = await targetDb.selectFrom("subgraphs").select([
|
|
1729
|
+
"id",
|
|
1730
|
+
"last_processed_block",
|
|
1731
|
+
"reindex_from_block",
|
|
1732
|
+
"reindex_to_block"
|
|
1733
|
+
]).where("name", "=", subgraphName).executeTakeFirst();
|
|
1734
|
+
if (!row)
|
|
1735
|
+
throw new Error(`Subgraph "${subgraphName}" not found`);
|
|
1736
|
+
const fromBlock = resolveReindexResumeBlock(row);
|
|
1737
|
+
const toBlock = Number(row.reindex_to_block);
|
|
1738
|
+
if (fromBlock == null) {
|
|
1739
|
+
logger5.info("No reindex metadata, starting fresh reindex", {
|
|
1740
|
+
subgraph: subgraphName
|
|
1741
|
+
});
|
|
1742
|
+
return reindexSubgraph(def, {
|
|
1743
|
+
schemaName: opts.schemaName,
|
|
1744
|
+
signal: opts.signal
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1747
|
+
if (fromBlock > toBlock) {
|
|
1748
|
+
logger5.info("Resume: no remaining blocks", { subgraph: subgraphName });
|
|
1749
|
+
await updateSubgraphStatus2(targetDb, subgraphName, "active", toBlock);
|
|
1750
|
+
await clearReindexMetadata(targetDb, subgraphName);
|
|
1751
|
+
return { processed: 0 };
|
|
1752
|
+
}
|
|
1753
|
+
logger5.info("Resuming reindex", {
|
|
1754
|
+
subgraph: subgraphName,
|
|
1755
|
+
fromBlock,
|
|
1756
|
+
toBlock,
|
|
1757
|
+
remaining: toBlock - fromBlock + 1
|
|
1758
|
+
});
|
|
1759
|
+
try {
|
|
1760
|
+
const result = await processBlockRange(def, {
|
|
1761
|
+
fromBlock,
|
|
1762
|
+
toBlock,
|
|
1763
|
+
status: "reindexing",
|
|
1764
|
+
isCatchup: false,
|
|
1765
|
+
apiKeyId: null,
|
|
1766
|
+
subgraphId: row.id,
|
|
1767
|
+
signal: opts.signal
|
|
1768
|
+
});
|
|
1769
|
+
if (result.aborted) {
|
|
1770
|
+
const reason = String(opts.signal?.reason ?? "unknown");
|
|
1771
|
+
if (reason === "user-cancelled") {
|
|
1772
|
+
await updateSubgraphStatus2(targetDb, subgraphName, "active");
|
|
1773
|
+
await clearReindexMetadata(targetDb, subgraphName);
|
|
1774
|
+
logger5.info("Resume cancelled by user", { subgraph: subgraphName });
|
|
1775
|
+
} else {
|
|
1776
|
+
logger5.info("Resume interrupted by shutdown, will resume again", {
|
|
1777
|
+
subgraph: subgraphName
|
|
1778
|
+
});
|
|
1779
|
+
}
|
|
1780
|
+
return { processed: result.blocksProcessed };
|
|
1781
|
+
}
|
|
1782
|
+
await updateSubgraphStatus2(targetDb, subgraphName, "active", toBlock);
|
|
1783
|
+
await clearReindexMetadata(targetDb, subgraphName);
|
|
1784
|
+
logger5.info("Resumed reindex complete", {
|
|
1785
|
+
subgraph: subgraphName,
|
|
1786
|
+
blocks: result.blocksProcessed
|
|
1787
|
+
});
|
|
1788
|
+
return { processed: result.blocksProcessed };
|
|
1789
|
+
} catch (err) {
|
|
1790
|
+
logger5.error("Resumed reindex failed", {
|
|
1791
|
+
subgraph: subgraphName,
|
|
1792
|
+
error: getErrorMessage2(err)
|
|
1793
|
+
});
|
|
1794
|
+
await updateSubgraphStatus2(targetDb, subgraphName, "error");
|
|
1795
|
+
throw err;
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
async function backfillSubgraph(def, opts) {
|
|
1799
|
+
const targetDb = getTargetDb2();
|
|
1800
|
+
const subgraphName = def.name;
|
|
1801
|
+
logger5.info("Backfill starting", {
|
|
1802
|
+
subgraph: subgraphName,
|
|
1803
|
+
from: opts.fromBlock,
|
|
1804
|
+
to: opts.toBlock
|
|
1805
|
+
});
|
|
1806
|
+
try {
|
|
1807
|
+
const subgraphRow = await targetDb.selectFrom("subgraphs").select(["id"]).where("name", "=", subgraphName).executeTakeFirst();
|
|
1808
|
+
const result = await processBlockRange(def, {
|
|
1809
|
+
fromBlock: opts.fromBlock,
|
|
1810
|
+
toBlock: opts.toBlock,
|
|
1811
|
+
status: "active",
|
|
1812
|
+
isCatchup: false,
|
|
1813
|
+
apiKeyId: null,
|
|
1814
|
+
subgraphId: subgraphRow?.id,
|
|
1815
|
+
signal: opts.signal
|
|
1816
|
+
});
|
|
1817
|
+
if (result.aborted) {
|
|
1818
|
+
logger5.info("Backfill aborted", { subgraph: subgraphName });
|
|
1819
|
+
return { processed: result.blocksProcessed };
|
|
1820
|
+
}
|
|
1821
|
+
const resolved = await resolveGaps(targetDb, subgraphName, opts.fromBlock, opts.toBlock).catch(() => 0);
|
|
1822
|
+
if (resolved > 0) {
|
|
1823
|
+
logger5.info("Resolved subgraph gaps via backfill", {
|
|
1824
|
+
subgraph: subgraphName,
|
|
1825
|
+
resolved
|
|
1826
|
+
});
|
|
1827
|
+
}
|
|
1828
|
+
logger5.info("Backfill complete", {
|
|
1829
|
+
subgraph: subgraphName,
|
|
1830
|
+
blocks: result.blocksProcessed,
|
|
1831
|
+
events: result.totalEventsProcessed,
|
|
1832
|
+
errors: result.totalErrors
|
|
1833
|
+
});
|
|
1834
|
+
return { processed: result.blocksProcessed };
|
|
1835
|
+
} catch (err) {
|
|
1836
|
+
logger5.error("Backfill failed", {
|
|
1837
|
+
subgraph: subgraphName,
|
|
1838
|
+
error: getErrorMessage2(err)
|
|
1839
|
+
});
|
|
1840
|
+
throw err;
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
// src/runtime/catchup.ts
|
|
1845
|
+
import { getSourceDb as getSourceDb3, getTargetDb as getTargetDb3 } from "@secondlayer/shared/db";
|
|
1846
|
+
import {
|
|
1847
|
+
recordGapBatch as recordGapBatch2
|
|
1848
|
+
} from "@secondlayer/shared/db/queries/subgraph-gaps";
|
|
1849
|
+
import { getSubgraph } from "@secondlayer/shared/db/queries/subgraphs";
|
|
1850
|
+
import { logger as logger6 } from "@secondlayer/shared/logger";
|
|
1851
|
+
var LOG_INTERVAL2 = 1000;
|
|
1354
1852
|
var DEFAULT_BATCH_SIZE = 500;
|
|
1355
1853
|
var MIN_BATCH_SIZE = 100;
|
|
1356
1854
|
var MAX_BATCH_SIZE = 1000;
|
|
@@ -1389,8 +1887,8 @@ async function catchUpSubgraph(subgraph, subgraphName) {
|
|
|
1389
1887
|
return 0;
|
|
1390
1888
|
catchingUp.add(subgraphName);
|
|
1391
1889
|
try {
|
|
1392
|
-
const sourceDb =
|
|
1393
|
-
const targetDb =
|
|
1890
|
+
const sourceDb = getSourceDb3();
|
|
1891
|
+
const targetDb = getTargetDb3();
|
|
1394
1892
|
const subgraphRow = await getSubgraph(targetDb, subgraphName);
|
|
1395
1893
|
if (!subgraphRow)
|
|
1396
1894
|
return 0;
|
|
@@ -1404,7 +1902,7 @@ async function catchUpSubgraph(subgraph, subgraphName) {
|
|
|
1404
1902
|
const subgraphStart = Number(subgraphRow.start_block) || 1;
|
|
1405
1903
|
const startBlock = Math.max(lastProcessedBlock + 1, subgraphStart);
|
|
1406
1904
|
const totalBlocks = chainTip - lastProcessedBlock;
|
|
1407
|
-
|
|
1905
|
+
logger6.info("Subgraph catch-up starting", {
|
|
1408
1906
|
subgraph: subgraphName,
|
|
1409
1907
|
from: startBlock,
|
|
1410
1908
|
to: chainTip,
|
|
@@ -1419,7 +1917,7 @@ async function catchUpSubgraph(subgraph, subgraphName) {
|
|
|
1419
1917
|
while (currentHeight <= chainTip) {
|
|
1420
1918
|
const currentRow = await getSubgraph(targetDb, subgraphName);
|
|
1421
1919
|
if (!currentRow || currentRow.status !== "active") {
|
|
1422
|
-
|
|
1920
|
+
logger6.info("Subgraph status changed, stopping catch-up", {
|
|
1423
1921
|
subgraph: subgraphName,
|
|
1424
1922
|
status: currentRow?.status ?? "deleted"
|
|
1425
1923
|
});
|
|
@@ -1446,14 +1944,14 @@ async function catchUpSubgraph(subgraph, subgraphName) {
|
|
|
1446
1944
|
preloaded: blockData
|
|
1447
1945
|
});
|
|
1448
1946
|
} catch (err) {
|
|
1449
|
-
|
|
1947
|
+
logger6.error("Block processing error during catch-up", {
|
|
1450
1948
|
subgraph: subgraphName,
|
|
1451
1949
|
blockHeight: height,
|
|
1452
1950
|
error: err instanceof Error ? err.message : String(err)
|
|
1453
1951
|
});
|
|
1454
1952
|
batchFailedBlocks.push({ height, reason: "processing_error" });
|
|
1455
|
-
const { updateSubgraphStatus:
|
|
1456
|
-
await
|
|
1953
|
+
const { updateSubgraphStatus: updateSubgraphStatus3 } = await import("@secondlayer/shared/db/queries/subgraphs");
|
|
1954
|
+
await updateSubgraphStatus3(targetDb, subgraphName, "active", height).catch(() => {});
|
|
1457
1955
|
processed++;
|
|
1458
1956
|
continue;
|
|
1459
1957
|
}
|
|
@@ -1464,8 +1962,8 @@ async function catchUpSubgraph(subgraph, subgraphName) {
|
|
|
1464
1962
|
await stats.flush(targetDb);
|
|
1465
1963
|
}
|
|
1466
1964
|
}
|
|
1467
|
-
if (processed %
|
|
1468
|
-
|
|
1965
|
+
if (processed % LOG_INTERVAL2 === 0) {
|
|
1966
|
+
logger6.info("Subgraph catch-up progress", {
|
|
1469
1967
|
subgraph: subgraphName,
|
|
1470
1968
|
processed,
|
|
1471
1969
|
total: totalBlocks,
|
|
@@ -1476,8 +1974,8 @@ async function catchUpSubgraph(subgraph, subgraphName) {
|
|
|
1476
1974
|
}
|
|
1477
1975
|
if (batchFailedBlocks.length > 0) {
|
|
1478
1976
|
const gaps = coalesceGaps(batchFailedBlocks);
|
|
1479
|
-
await
|
|
1480
|
-
|
|
1977
|
+
await recordGapBatch2(targetDb, subgraphRow.id, subgraphName, gaps).catch((err) => {
|
|
1978
|
+
logger6.warn("Failed to record subgraph gaps", {
|
|
1481
1979
|
subgraph: subgraphName,
|
|
1482
1980
|
error: err instanceof Error ? err.message : String(err)
|
|
1483
1981
|
});
|
|
@@ -1488,7 +1986,7 @@ async function catchUpSubgraph(subgraph, subgraphName) {
|
|
|
1488
1986
|
currentHeight = batchEnd + 1;
|
|
1489
1987
|
}
|
|
1490
1988
|
await stats.flush(targetDb);
|
|
1491
|
-
|
|
1989
|
+
logger6.info("Subgraph catch-up complete", {
|
|
1492
1990
|
subgraph: subgraphName,
|
|
1493
1991
|
processed
|
|
1494
1992
|
});
|
|
@@ -1499,17 +1997,17 @@ async function catchUpSubgraph(subgraph, subgraphName) {
|
|
|
1499
1997
|
}
|
|
1500
1998
|
|
|
1501
1999
|
// src/runtime/reorg.ts
|
|
1502
|
-
import { getErrorMessage as
|
|
1503
|
-
import { getRawClient, getTargetDb as
|
|
2000
|
+
import { getErrorMessage as getErrorMessage3 } from "@secondlayer/shared";
|
|
2001
|
+
import { getRawClient as getRawClient2, getTargetDb as getTargetDb4 } from "@secondlayer/shared/db";
|
|
1504
2002
|
import { listSubgraphs } from "@secondlayer/shared/db/queries/subgraphs";
|
|
1505
|
-
import { logger as
|
|
2003
|
+
import { logger as logger7 } from "@secondlayer/shared/logger";
|
|
1506
2004
|
async function handleSubgraphReorg(blockHeight, loadSubgraphDef) {
|
|
1507
|
-
const targetDb =
|
|
1508
|
-
const client =
|
|
2005
|
+
const targetDb = getTargetDb4();
|
|
2006
|
+
const client = getRawClient2("target");
|
|
1509
2007
|
const activeSubgraphs = (await listSubgraphs(targetDb)).filter((v) => v.status === "active");
|
|
1510
2008
|
if (activeSubgraphs.length === 0)
|
|
1511
2009
|
return;
|
|
1512
|
-
|
|
2010
|
+
logger7.info("Propagating reorg to subgraphs", {
|
|
1513
2011
|
blockHeight,
|
|
1514
2012
|
subgraphCount: activeSubgraphs.length
|
|
1515
2013
|
});
|
|
@@ -1522,51 +2020,65 @@ async function handleSubgraphReorg(blockHeight, loadSubgraphDef) {
|
|
|
1522
2020
|
for (const tableName of Object.keys(schema)) {
|
|
1523
2021
|
await client.unsafe(`DELETE FROM "${schemaName}"."${tableName}" WHERE "_block_height" = $1`, [blockHeight]);
|
|
1524
2022
|
}
|
|
1525
|
-
|
|
2023
|
+
logger7.info("Subgraph reorg cleanup done", {
|
|
1526
2024
|
subgraph: sg.name,
|
|
1527
2025
|
blockHeight
|
|
1528
2026
|
});
|
|
1529
2027
|
const def = await loadSubgraphDef(sg);
|
|
1530
2028
|
await processBlock(def, sg.name, blockHeight);
|
|
1531
|
-
|
|
2029
|
+
logger7.info("Subgraph reorg reprocessed", {
|
|
1532
2030
|
subgraph: sg.name,
|
|
1533
2031
|
blockHeight
|
|
1534
2032
|
});
|
|
1535
2033
|
} catch (err) {
|
|
1536
|
-
|
|
2034
|
+
logger7.error("Subgraph reorg handling failed", {
|
|
1537
2035
|
subgraph: sg.name,
|
|
1538
2036
|
blockHeight,
|
|
1539
|
-
error:
|
|
2037
|
+
error: getErrorMessage3(err)
|
|
1540
2038
|
});
|
|
1541
2039
|
}
|
|
1542
2040
|
}
|
|
1543
2041
|
}
|
|
1544
2042
|
|
|
1545
2043
|
// src/runtime/processor.ts
|
|
2044
|
+
import { randomUUID } from "node:crypto";
|
|
2045
|
+
import { hostname } from "node:os";
|
|
1546
2046
|
import { resolve } from "node:path";
|
|
1547
2047
|
import { pathToFileURL } from "node:url";
|
|
1548
|
-
import { getErrorMessage as
|
|
1549
|
-
import { getTargetDb as
|
|
2048
|
+
import { getErrorMessage as getErrorMessage4 } from "@secondlayer/shared";
|
|
2049
|
+
import { getTargetDb as getTargetDb6 } from "@secondlayer/shared/db";
|
|
2050
|
+
import {
|
|
2051
|
+
cancelSubgraphOperation,
|
|
2052
|
+
claimSubgraphOperation,
|
|
2053
|
+
completeSubgraphOperation,
|
|
2054
|
+
createSubgraphOperation,
|
|
2055
|
+
failSubgraphOperation,
|
|
2056
|
+
findActiveSubgraphOperation,
|
|
2057
|
+
getSubgraphOperation,
|
|
2058
|
+
heartbeatSubgraphOperation,
|
|
2059
|
+
isActiveSubgraphOperationConflict
|
|
2060
|
+
} from "@secondlayer/shared/db/queries/subgraph-operations";
|
|
1550
2061
|
import {
|
|
1551
2062
|
listSubgraphs as listSubgraphs2,
|
|
1552
|
-
|
|
2063
|
+
pgSchemaName as pgSchemaName2,
|
|
2064
|
+
updateSubgraphStatus as updateSubgraphStatus3
|
|
1553
2065
|
} from "@secondlayer/shared/db/queries/subgraphs";
|
|
1554
|
-
import { logger as
|
|
2066
|
+
import { logger as logger10 } from "@secondlayer/shared/logger";
|
|
1555
2067
|
import { listen as listen2 } from "@secondlayer/shared/queue/listener";
|
|
1556
2068
|
|
|
1557
2069
|
// src/runtime/emitter.ts
|
|
1558
2070
|
import {
|
|
1559
|
-
getTargetDb as
|
|
2071
|
+
getTargetDb as getTargetDb5
|
|
1560
2072
|
} from "@secondlayer/shared/db";
|
|
1561
2073
|
import {
|
|
1562
2074
|
getSubscriptionSigningSecret
|
|
1563
2075
|
} from "@secondlayer/shared/db/queries/subscriptions";
|
|
1564
|
-
import { logger as
|
|
2076
|
+
import { logger as logger9 } from "@secondlayer/shared/logger";
|
|
1565
2077
|
import { listen } from "@secondlayer/shared/queue/listener";
|
|
1566
2078
|
import { sql as sql4 } from "kysely";
|
|
1567
2079
|
|
|
1568
2080
|
// src/runtime/formats/index.ts
|
|
1569
|
-
import { logger as
|
|
2081
|
+
import { logger as logger8 } from "@secondlayer/shared/logger";
|
|
1570
2082
|
|
|
1571
2083
|
// src/runtime/formats/cloudevents.ts
|
|
1572
2084
|
function buildCloudEvents(outboxRow, _sub) {
|
|
@@ -1713,7 +2225,7 @@ function buildForFormat(outboxRow, sub, signingSecret) {
|
|
|
1713
2225
|
case "standard-webhooks":
|
|
1714
2226
|
return buildStandardWebhooks(outboxRow, signingSecret);
|
|
1715
2227
|
default:
|
|
1716
|
-
|
|
2228
|
+
logger8.warn("Unknown subscription format, falling back to standard-webhooks", {
|
|
1717
2229
|
format: sub.format,
|
|
1718
2230
|
subscriptionId: sub.id
|
|
1719
2231
|
});
|
|
@@ -1796,7 +2308,7 @@ async function dispatchOne(db, outboxRow, sub) {
|
|
|
1796
2308
|
let responseHeaders = {};
|
|
1797
2309
|
if (isPrivateEgress(sub.url) && !allowPrivateEgress()) {
|
|
1798
2310
|
error = "refused private egress (set SECONDLAYER_ALLOW_PRIVATE_EGRESS=true to allow)";
|
|
1799
|
-
|
|
2311
|
+
logger9.warn("[emitter] refused private egress", {
|
|
1800
2312
|
subscription: sub.name,
|
|
1801
2313
|
url: sub.url
|
|
1802
2314
|
});
|
|
@@ -1892,7 +2404,7 @@ async function settleFailed(db, outboxRow, sub, errText) {
|
|
|
1892
2404
|
circuit_opened_at: new Date,
|
|
1893
2405
|
updated_at: new Date
|
|
1894
2406
|
}).where("id", "=", sub.id).execute();
|
|
1895
|
-
|
|
2407
|
+
logger9.warn("Subscription circuit tripped — paused after consecutive failures", {
|
|
1896
2408
|
subscription: sub.name,
|
|
1897
2409
|
failures: newFailures
|
|
1898
2410
|
});
|
|
@@ -1980,7 +2492,7 @@ async function drainForSub(db, state, sub, rows) {
|
|
|
1980
2492
|
await settleFailed(db, row, sub, err);
|
|
1981
2493
|
}
|
|
1982
2494
|
} catch (err) {
|
|
1983
|
-
|
|
2495
|
+
logger9.error("Emitter dispatch crashed", {
|
|
1984
2496
|
outboxId: row.id,
|
|
1985
2497
|
error: err instanceof Error ? err.message : String(err)
|
|
1986
2498
|
});
|
|
@@ -2009,7 +2521,7 @@ async function runRetention(db) {
|
|
|
2009
2521
|
}
|
|
2010
2522
|
async function startEmitter(opts) {
|
|
2011
2523
|
const emitterId = `emitter-${Math.random().toString(36).slice(2, 10)}`;
|
|
2012
|
-
const db =
|
|
2524
|
+
const db = getTargetDb5();
|
|
2013
2525
|
const state = {
|
|
2014
2526
|
running: true,
|
|
2015
2527
|
inFlightBySub: new Map,
|
|
@@ -2017,7 +2529,7 @@ async function startEmitter(opts) {
|
|
|
2017
2529
|
};
|
|
2018
2530
|
const pollIntervalMs = opts?.pollIntervalMs ?? 120000;
|
|
2019
2531
|
const retentionIntervalMs = opts?.retentionIntervalMs ?? 60 * 60000;
|
|
2020
|
-
|
|
2532
|
+
logger9.info("[emitter] started", { id: emitterId });
|
|
2021
2533
|
const MATCHER_BOOT_ATTEMPTS = 5;
|
|
2022
2534
|
let lastErr = null;
|
|
2023
2535
|
for (let i = 0;i < MATCHER_BOOT_ATTEMPTS; i++) {
|
|
@@ -2028,7 +2540,7 @@ async function startEmitter(opts) {
|
|
|
2028
2540
|
} catch (err) {
|
|
2029
2541
|
lastErr = err;
|
|
2030
2542
|
const delayMs = 500 * 2 ** i;
|
|
2031
|
-
|
|
2543
|
+
logger9.warn("[emitter] matcher refresh failed, retrying", {
|
|
2032
2544
|
attempt: i + 1,
|
|
2033
2545
|
delayMs,
|
|
2034
2546
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -2042,21 +2554,21 @@ async function startEmitter(opts) {
|
|
|
2042
2554
|
const stopNew = await listen("subscriptions:new_outbox", () => {
|
|
2043
2555
|
if (!state.running)
|
|
2044
2556
|
return;
|
|
2045
|
-
claimAndDrain(db, state, emitterId).catch((err) =>
|
|
2557
|
+
claimAndDrain(db, state, emitterId).catch((err) => logger9.error("[emitter] claim failed", {
|
|
2046
2558
|
error: err instanceof Error ? err.message : String(err)
|
|
2047
2559
|
}));
|
|
2048
2560
|
});
|
|
2049
2561
|
const stopChanged = await listen("subscriptions:changed", () => {
|
|
2050
2562
|
if (!state.running)
|
|
2051
2563
|
return;
|
|
2052
|
-
refreshMatcher(db).catch((err) =>
|
|
2564
|
+
refreshMatcher(db).catch((err) => logger9.error("[emitter] matcher refresh failed", {
|
|
2053
2565
|
error: err instanceof Error ? err.message : String(err)
|
|
2054
2566
|
}));
|
|
2055
2567
|
});
|
|
2056
2568
|
const poll = setInterval(() => {
|
|
2057
2569
|
if (!state.running)
|
|
2058
2570
|
return;
|
|
2059
|
-
claimAndDrain(db, state, emitterId).catch((err) =>
|
|
2571
|
+
claimAndDrain(db, state, emitterId).catch((err) => logger9.error("[emitter] poll claim failed", {
|
|
2060
2572
|
error: err instanceof Error ? err.message : String(err)
|
|
2061
2573
|
}));
|
|
2062
2574
|
}, pollIntervalMs);
|
|
@@ -2064,7 +2576,7 @@ async function startEmitter(opts) {
|
|
|
2064
2576
|
const retention = setInterval(() => {
|
|
2065
2577
|
if (!state.running)
|
|
2066
2578
|
return;
|
|
2067
|
-
runRetention(db).catch((err) =>
|
|
2579
|
+
runRetention(db).catch((err) => logger9.error("[emitter] retention failed", {
|
|
2068
2580
|
error: err instanceof Error ? err.message : String(err)
|
|
2069
2581
|
}));
|
|
2070
2582
|
}, retentionIntervalMs);
|
|
@@ -2074,20 +2586,27 @@ async function startEmitter(opts) {
|
|
|
2074
2586
|
clearInterval(retention);
|
|
2075
2587
|
await stopNew();
|
|
2076
2588
|
await stopChanged();
|
|
2077
|
-
|
|
2589
|
+
logger9.info("[emitter] stopped", { id: emitterId });
|
|
2078
2590
|
};
|
|
2079
2591
|
}
|
|
2080
2592
|
|
|
2081
2593
|
// src/runtime/processor.ts
|
|
2082
2594
|
var CHANNEL_NEW_BLOCK = "indexer:new_block";
|
|
2595
|
+
var CHANNEL_SUBGRAPH_OPERATIONS = "subgraph_operations:new";
|
|
2083
2596
|
var DEFAULT_CONCURRENCY = 5;
|
|
2597
|
+
var DEFAULT_OPERATION_CONCURRENCY = 1;
|
|
2084
2598
|
var POLL_INTERVAL_MS = 5000;
|
|
2599
|
+
var HEARTBEAT_INTERVAL_MS = 15000;
|
|
2600
|
+
var CANCEL_POLL_INTERVAL_MS = 1000;
|
|
2085
2601
|
function handlerImportUrl(handlerPath, cacheBust = Date.now()) {
|
|
2086
2602
|
return `${pathToFileURL(resolve(handlerPath)).href}?t=${cacheBust}`;
|
|
2087
2603
|
}
|
|
2088
2604
|
function sourceListenerUrl() {
|
|
2089
2605
|
return process.env.SOURCE_DATABASE_URL ?? process.env.DATABASE_URL;
|
|
2090
2606
|
}
|
|
2607
|
+
function targetListenerUrl() {
|
|
2608
|
+
return process.env.TARGET_DATABASE_URL ?? process.env.DATABASE_URL;
|
|
2609
|
+
}
|
|
2091
2610
|
function isHandlerNotFoundError(err) {
|
|
2092
2611
|
if (!(err instanceof Error))
|
|
2093
2612
|
return false;
|
|
@@ -2115,7 +2634,7 @@ async function loadSubgraphDefinition(sg) {
|
|
|
2115
2634
|
knownVersions.set(sg.name, sg.version);
|
|
2116
2635
|
definitionCache.set(sg.name, def);
|
|
2117
2636
|
if (prevVersion && prevVersion !== sg.version) {
|
|
2118
|
-
|
|
2637
|
+
logger10.info("Subgraph handler reloaded", {
|
|
2119
2638
|
subgraph: sg.name,
|
|
2120
2639
|
from: prevVersion,
|
|
2121
2640
|
to: sg.version
|
|
@@ -2132,22 +2651,215 @@ function cleanupCaches(active) {
|
|
|
2132
2651
|
}
|
|
2133
2652
|
}
|
|
2134
2653
|
}
|
|
2654
|
+
async function synthesizeLegacyReindexOperations() {
|
|
2655
|
+
const db = getTargetDb6();
|
|
2656
|
+
const stale = (await listSubgraphs2(db)).filter((sg) => sg.status === "reindexing");
|
|
2657
|
+
for (const sg of stale) {
|
|
2658
|
+
const active = await findActiveSubgraphOperation(db, sg.id);
|
|
2659
|
+
if (active)
|
|
2660
|
+
continue;
|
|
2661
|
+
try {
|
|
2662
|
+
await createSubgraphOperation(db, {
|
|
2663
|
+
subgraphId: sg.id,
|
|
2664
|
+
subgraphName: sg.name,
|
|
2665
|
+
accountId: sg.account_id,
|
|
2666
|
+
kind: "reindex",
|
|
2667
|
+
fromBlock: sg.reindex_from_block == null ? undefined : Number(sg.reindex_from_block),
|
|
2668
|
+
toBlock: sg.reindex_to_block == null ? undefined : Number(sg.reindex_to_block)
|
|
2669
|
+
});
|
|
2670
|
+
logger10.info("Queued legacy reindex resume operation", {
|
|
2671
|
+
subgraph: sg.name
|
|
2672
|
+
});
|
|
2673
|
+
} catch (err) {
|
|
2674
|
+
if (isActiveSubgraphOperationConflict(err))
|
|
2675
|
+
continue;
|
|
2676
|
+
throw err;
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
async function runSubgraphOperation(operation, signal) {
|
|
2681
|
+
if (operation.cancel_requested) {
|
|
2682
|
+
return 0;
|
|
2683
|
+
}
|
|
2684
|
+
const db = getTargetDb6();
|
|
2685
|
+
const subgraph = await db.selectFrom("subgraphs").selectAll().where("id", "=", operation.subgraph_id).executeTakeFirst();
|
|
2686
|
+
if (!subgraph)
|
|
2687
|
+
throw new Error(`Subgraph not found: ${operation.subgraph_id}`);
|
|
2688
|
+
const def = await loadSubgraphDefinition(subgraph);
|
|
2689
|
+
const schemaName = subgraph.schema_name ?? pgSchemaName2(subgraph.name);
|
|
2690
|
+
if (operation.kind === "backfill") {
|
|
2691
|
+
if (operation.from_block == null || operation.to_block == null) {
|
|
2692
|
+
throw new Error("Backfill operation is missing from_block or to_block");
|
|
2693
|
+
}
|
|
2694
|
+
const result2 = await backfillSubgraph(def, {
|
|
2695
|
+
fromBlock: Number(operation.from_block),
|
|
2696
|
+
toBlock: Number(operation.to_block),
|
|
2697
|
+
schemaName,
|
|
2698
|
+
signal
|
|
2699
|
+
});
|
|
2700
|
+
return result2.processed;
|
|
2701
|
+
}
|
|
2702
|
+
const hasResumeMetadata = subgraph.status === "reindexing" && subgraph.reindex_from_block != null && subgraph.reindex_to_block != null;
|
|
2703
|
+
if (hasResumeMetadata) {
|
|
2704
|
+
const result2 = await resumeReindex(def, { schemaName, signal });
|
|
2705
|
+
return result2.processed;
|
|
2706
|
+
}
|
|
2707
|
+
const result = await reindexSubgraph(def, {
|
|
2708
|
+
fromBlock: operation.from_block == null ? undefined : Number(operation.from_block),
|
|
2709
|
+
toBlock: operation.to_block == null ? undefined : Number(operation.to_block),
|
|
2710
|
+
schemaName,
|
|
2711
|
+
signal
|
|
2712
|
+
});
|
|
2713
|
+
return result.processed;
|
|
2714
|
+
}
|
|
2715
|
+
async function startSubgraphOperationRunner(opts) {
|
|
2716
|
+
const concurrency = opts?.concurrency ?? DEFAULT_OPERATION_CONCURRENCY;
|
|
2717
|
+
const db = getTargetDb6();
|
|
2718
|
+
const lockedBy = `${hostname()}:${process.pid}:${randomUUID()}`;
|
|
2719
|
+
const active = new Map;
|
|
2720
|
+
const activeRuns = new Map;
|
|
2721
|
+
let running = true;
|
|
2722
|
+
let draining = false;
|
|
2723
|
+
logger10.info("Starting subgraph operation runner", { concurrency, lockedBy });
|
|
2724
|
+
const startOperation = (operation) => {
|
|
2725
|
+
const controller = new AbortController;
|
|
2726
|
+
active.set(operation.id, controller);
|
|
2727
|
+
const heartbeat = setInterval(() => {
|
|
2728
|
+
if (!running)
|
|
2729
|
+
return;
|
|
2730
|
+
heartbeatSubgraphOperation(db, operation.id, lockedBy).catch((err) => {
|
|
2731
|
+
logger10.warn("Subgraph operation heartbeat failed", {
|
|
2732
|
+
operationId: operation.id,
|
|
2733
|
+
error: getErrorMessage4(err)
|
|
2734
|
+
});
|
|
2735
|
+
});
|
|
2736
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
2737
|
+
const cancelPoll = setInterval(() => {
|
|
2738
|
+
getSubgraphOperation(db, operation.id).then((row) => {
|
|
2739
|
+
if (row?.cancel_requested && !controller.signal.aborted) {
|
|
2740
|
+
controller.abort("user-cancelled");
|
|
2741
|
+
}
|
|
2742
|
+
}).catch((err) => {
|
|
2743
|
+
logger10.warn("Subgraph operation cancel poll failed", {
|
|
2744
|
+
operationId: operation.id,
|
|
2745
|
+
error: getErrorMessage4(err)
|
|
2746
|
+
});
|
|
2747
|
+
});
|
|
2748
|
+
}, CANCEL_POLL_INTERVAL_MS);
|
|
2749
|
+
const run = (async () => {
|
|
2750
|
+
let processed = 0;
|
|
2751
|
+
try {
|
|
2752
|
+
if (operation.cancel_requested) {
|
|
2753
|
+
controller.abort("user-cancelled");
|
|
2754
|
+
} else {
|
|
2755
|
+
processed = await runSubgraphOperation(operation, controller.signal);
|
|
2756
|
+
}
|
|
2757
|
+
const reason = String(controller.signal.reason ?? "");
|
|
2758
|
+
if (controller.signal.aborted && reason === "user-cancelled") {
|
|
2759
|
+
await cancelSubgraphOperation(db, operation.id, lockedBy, processed);
|
|
2760
|
+
logger10.info("Subgraph operation cancelled", {
|
|
2761
|
+
operationId: operation.id,
|
|
2762
|
+
subgraph: operation.subgraph_name
|
|
2763
|
+
});
|
|
2764
|
+
return;
|
|
2765
|
+
}
|
|
2766
|
+
if (controller.signal.aborted) {
|
|
2767
|
+
logger10.info("Subgraph operation interrupted", {
|
|
2768
|
+
operationId: operation.id,
|
|
2769
|
+
subgraph: operation.subgraph_name,
|
|
2770
|
+
reason
|
|
2771
|
+
});
|
|
2772
|
+
return;
|
|
2773
|
+
}
|
|
2774
|
+
await completeSubgraphOperation(db, operation.id, lockedBy, processed);
|
|
2775
|
+
logger10.info("Subgraph operation completed", {
|
|
2776
|
+
operationId: operation.id,
|
|
2777
|
+
subgraph: operation.subgraph_name,
|
|
2778
|
+
processed
|
|
2779
|
+
});
|
|
2780
|
+
} catch (err) {
|
|
2781
|
+
const reason = String(controller.signal.reason ?? "");
|
|
2782
|
+
if (controller.signal.aborted && reason === "shutdown") {
|
|
2783
|
+
logger10.info("Subgraph operation interrupted by shutdown", {
|
|
2784
|
+
operationId: operation.id,
|
|
2785
|
+
subgraph: operation.subgraph_name
|
|
2786
|
+
});
|
|
2787
|
+
return;
|
|
2788
|
+
}
|
|
2789
|
+
if (controller.signal.aborted && reason === "user-cancelled") {
|
|
2790
|
+
await cancelSubgraphOperation(db, operation.id, lockedBy, processed);
|
|
2791
|
+
return;
|
|
2792
|
+
}
|
|
2793
|
+
await failSubgraphOperation(db, operation.id, lockedBy, getErrorMessage4(err), processed);
|
|
2794
|
+
logger10.error("Subgraph operation failed", {
|
|
2795
|
+
operationId: operation.id,
|
|
2796
|
+
subgraph: operation.subgraph_name,
|
|
2797
|
+
error: getErrorMessage4(err)
|
|
2798
|
+
});
|
|
2799
|
+
} finally {
|
|
2800
|
+
clearInterval(heartbeat);
|
|
2801
|
+
clearInterval(cancelPoll);
|
|
2802
|
+
active.delete(operation.id);
|
|
2803
|
+
activeRuns.delete(operation.id);
|
|
2804
|
+
if (running)
|
|
2805
|
+
drain();
|
|
2806
|
+
}
|
|
2807
|
+
})();
|
|
2808
|
+
activeRuns.set(operation.id, run);
|
|
2809
|
+
};
|
|
2810
|
+
const drain = async () => {
|
|
2811
|
+
if (!running || draining)
|
|
2812
|
+
return;
|
|
2813
|
+
draining = true;
|
|
2814
|
+
try {
|
|
2815
|
+
while (running && active.size < concurrency) {
|
|
2816
|
+
const operation = await claimSubgraphOperation(db, lockedBy);
|
|
2817
|
+
if (!operation)
|
|
2818
|
+
break;
|
|
2819
|
+
startOperation(operation);
|
|
2820
|
+
}
|
|
2821
|
+
} finally {
|
|
2822
|
+
draining = false;
|
|
2823
|
+
}
|
|
2824
|
+
};
|
|
2825
|
+
await synthesizeLegacyReindexOperations();
|
|
2826
|
+
await drain();
|
|
2827
|
+
const stopListening = await listen2(CHANNEL_SUBGRAPH_OPERATIONS, () => {
|
|
2828
|
+
drain();
|
|
2829
|
+
}, { connectionString: targetListenerUrl() });
|
|
2830
|
+
const pollInterval = setInterval(() => {
|
|
2831
|
+
drain();
|
|
2832
|
+
}, POLL_INTERVAL_MS);
|
|
2833
|
+
return async () => {
|
|
2834
|
+
running = false;
|
|
2835
|
+
clearInterval(pollInterval);
|
|
2836
|
+
await stopListening();
|
|
2837
|
+
for (const controller of active.values()) {
|
|
2838
|
+
controller.abort("shutdown");
|
|
2839
|
+
}
|
|
2840
|
+
await Promise.allSettled(activeRuns.values());
|
|
2841
|
+
logger10.info("Subgraph operation runner stopped");
|
|
2842
|
+
};
|
|
2843
|
+
}
|
|
2135
2844
|
async function startSubgraphProcessor(opts) {
|
|
2136
2845
|
const concurrency = opts?.concurrency ?? DEFAULT_CONCURRENCY;
|
|
2137
2846
|
let running = true;
|
|
2138
|
-
|
|
2139
|
-
const
|
|
2847
|
+
logger10.info("Starting subgraph processor", { concurrency });
|
|
2848
|
+
const stopOperations = await startSubgraphOperationRunner({
|
|
2849
|
+
concurrency: Number.parseInt(process.env.SUBGRAPH_OPERATION_CONCURRENCY ?? String(DEFAULT_OPERATION_CONCURRENCY))
|
|
2850
|
+
});
|
|
2851
|
+
const targetDb = getTargetDb6();
|
|
2140
2852
|
const activeSubgraphs = (await listSubgraphs2(targetDb)).filter((v) => v.status === "active");
|
|
2141
2853
|
for (const sg of activeSubgraphs) {
|
|
2142
2854
|
try {
|
|
2143
2855
|
const def = await loadSubgraphDefinition(sg);
|
|
2144
2856
|
await catchUpSubgraph(def, sg.name);
|
|
2145
2857
|
} catch (err) {
|
|
2146
|
-
const msg =
|
|
2858
|
+
const msg = getErrorMessage4(err);
|
|
2147
2859
|
if (isHandlerNotFoundError(err)) {
|
|
2148
|
-
await
|
|
2860
|
+
await updateSubgraphStatus3(targetDb, sg.name, "error");
|
|
2149
2861
|
}
|
|
2150
|
-
|
|
2862
|
+
logger10.error("Subgraph catch-up failed on startup", {
|
|
2151
2863
|
subgraph: sg.name,
|
|
2152
2864
|
error: msg
|
|
2153
2865
|
});
|
|
@@ -2156,7 +2868,7 @@ async function startSubgraphProcessor(opts) {
|
|
|
2156
2868
|
const stopListening = await listen2(CHANNEL_NEW_BLOCK, async () => {
|
|
2157
2869
|
if (!running)
|
|
2158
2870
|
return;
|
|
2159
|
-
const db =
|
|
2871
|
+
const db = getTargetDb6();
|
|
2160
2872
|
const subgraphs = (await listSubgraphs2(db)).filter((v) => v.status === "active");
|
|
2161
2873
|
cleanupCaches(subgraphs);
|
|
2162
2874
|
for (const sg of subgraphs) {
|
|
@@ -2164,11 +2876,11 @@ async function startSubgraphProcessor(opts) {
|
|
|
2164
2876
|
const def = await loadSubgraphDefinition(sg);
|
|
2165
2877
|
await catchUpSubgraph(def, sg.name);
|
|
2166
2878
|
} catch (err) {
|
|
2167
|
-
const msg =
|
|
2879
|
+
const msg = getErrorMessage4(err);
|
|
2168
2880
|
if (isHandlerNotFoundError(err)) {
|
|
2169
|
-
await
|
|
2881
|
+
await updateSubgraphStatus3(db, sg.name, "error");
|
|
2170
2882
|
}
|
|
2171
|
-
|
|
2883
|
+
logger10.error("Subgraph processing failed", {
|
|
2172
2884
|
subgraph: sg.name,
|
|
2173
2885
|
error: msg
|
|
2174
2886
|
});
|
|
@@ -2185,15 +2897,15 @@ async function startSubgraphProcessor(opts) {
|
|
|
2185
2897
|
await handleSubgraphReorg(blockHeight, loadSubgraphDefinition);
|
|
2186
2898
|
}
|
|
2187
2899
|
} catch (err) {
|
|
2188
|
-
|
|
2189
|
-
error:
|
|
2900
|
+
logger10.error("Subgraph reorg handling failed", {
|
|
2901
|
+
error: getErrorMessage4(err)
|
|
2190
2902
|
});
|
|
2191
2903
|
}
|
|
2192
2904
|
}, { connectionString: sourceListenerUrl() });
|
|
2193
2905
|
const pollInterval = setInterval(async () => {
|
|
2194
2906
|
if (!running)
|
|
2195
2907
|
return;
|
|
2196
|
-
const db =
|
|
2908
|
+
const db = getTargetDb6();
|
|
2197
2909
|
const subgraphs = (await listSubgraphs2(db)).filter((v) => v.status === "active");
|
|
2198
2910
|
cleanupCaches(subgraphs);
|
|
2199
2911
|
for (const sg of subgraphs) {
|
|
@@ -2201,27 +2913,29 @@ async function startSubgraphProcessor(opts) {
|
|
|
2201
2913
|
const def = await loadSubgraphDefinition(sg);
|
|
2202
2914
|
await catchUpSubgraph(def, sg.name);
|
|
2203
2915
|
} catch (err) {
|
|
2204
|
-
|
|
2916
|
+
logger10.error("Subgraph poll processing failed", {
|
|
2205
2917
|
subgraph: sg.name,
|
|
2206
|
-
error:
|
|
2918
|
+
error: getErrorMessage4(err)
|
|
2207
2919
|
});
|
|
2208
2920
|
}
|
|
2209
2921
|
}
|
|
2210
2922
|
}, POLL_INTERVAL_MS);
|
|
2211
2923
|
const stopEmitter = await startEmitter();
|
|
2212
|
-
|
|
2924
|
+
logger10.info("Subgraph processor ready");
|
|
2213
2925
|
return async () => {
|
|
2214
2926
|
running = false;
|
|
2215
2927
|
clearInterval(pollInterval);
|
|
2216
2928
|
await stopListening();
|
|
2217
2929
|
await stopReorgListening();
|
|
2930
|
+
await stopOperations();
|
|
2218
2931
|
await stopEmitter();
|
|
2219
|
-
|
|
2932
|
+
logger10.info("Subgraph processor stopped");
|
|
2220
2933
|
};
|
|
2221
2934
|
}
|
|
2222
2935
|
export {
|
|
2223
|
-
startSubgraphProcessor
|
|
2936
|
+
startSubgraphProcessor,
|
|
2937
|
+
startSubgraphOperationRunner
|
|
2224
2938
|
};
|
|
2225
2939
|
|
|
2226
|
-
//# debugId=
|
|
2940
|
+
//# debugId=1929DF522C4FE6AB64756E2164756E21
|
|
2227
2941
|
//# sourceMappingURL=processor.js.map
|