@secondlayer/subgraphs 3.23.2 → 3.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1627,9 +1627,11 @@ import { resolveTraitContractIds } from "@secondlayer/shared/db/queries/contract
1627
1627
  import { advanceOperationCursor } from "@secondlayer/shared/db/queries/subgraph-operations";
1628
1628
  import {
1629
1629
  isByoSubgraph,
1630
+ recordLiveError,
1630
1631
  recordLiveProgress,
1631
1632
  recordSubgraphProcessed,
1632
1633
  resolveSubgraphDb,
1634
+ rewindLiveProgress,
1633
1635
  updateSubgraphStatus
1634
1636
  } from "@secondlayer/shared/db/queries/subgraphs";
1635
1637
  import { logger as logger6 } from "@secondlayer/shared/logger";
@@ -2337,10 +2339,20 @@ class CursorRaceLostError extends Error {
2337
2339
  this.name = "CursorRaceLostError";
2338
2340
  }
2339
2341
  }
2342
+
2343
+ class LiveCursorRaceLostError extends Error {
2344
+ constructor(subgraphName, height) {
2345
+ super(`live cursor race lost for ${subgraphName} at block ${height}`);
2346
+ this.name = "LiveCursorRaceLostError";
2347
+ }
2348
+ }
2340
2349
  function opCursorMode(opts) {
2341
2350
  const ap = opts?.atomicProgress;
2342
2351
  return ap && "operationId" in ap ? ap : undefined;
2343
2352
  }
2353
+ function isReorgRewind(opts) {
2354
+ return opts?.reorgRewind === true;
2355
+ }
2344
2356
  function statusMode(opts) {
2345
2357
  const ap = opts?.atomicProgress;
2346
2358
  return ap && "status" in ap ? ap : undefined;
@@ -2410,7 +2422,11 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
2410
2422
  result.matched = matched.length;
2411
2423
  if (matched.length === 0) {
2412
2424
  if (!opts?.skipProgressUpdate) {
2413
- await recordLiveProgress(targetDb, subgraphName, blockHeight);
2425
+ if (isReorgRewind(opts)) {
2426
+ await rewindLiveProgress(targetDb, subgraphName, blockHeight);
2427
+ } else {
2428
+ await recordLiveProgress(targetDb, subgraphName, blockHeight);
2429
+ }
2414
2430
  }
2415
2431
  return result;
2416
2432
  }
@@ -2429,13 +2445,23 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
2429
2445
  };
2430
2446
  let handlerMs = 0;
2431
2447
  let flushMs = 0;
2432
- const applyProgress = async (tx, rr) => {
2448
+ const applyProgress = async (tx, rr, flushedWrites) => {
2433
2449
  if (opts?.skipProgressUpdate)
2434
2450
  return;
2451
+ const rewind = isReorgRewind(opts);
2435
2452
  if (rr.errors > 0 && rr.processed === 0) {
2436
- await updateSubgraphStatus(tx, subgraphName, "error", blockHeight);
2453
+ if (rewind) {
2454
+ await updateSubgraphStatus(tx, subgraphName, "error", blockHeight);
2455
+ } else {
2456
+ await recordLiveError(tx, subgraphName, blockHeight);
2457
+ }
2458
+ } else if (rewind) {
2459
+ await rewindLiveProgress(tx, subgraphName, blockHeight);
2437
2460
  } else {
2438
- await recordLiveProgress(tx, subgraphName, blockHeight);
2461
+ const advanced = await recordLiveProgress(tx, subgraphName, blockHeight);
2462
+ if (!advanced && flushedWrites && !route.byo) {
2463
+ throw new LiveCursorRaceLostError(subgraphName, blockHeight);
2464
+ }
2439
2465
  }
2440
2466
  if (rr.processed > 0 || rr.errors > 0) {
2441
2467
  const lastError = rr.errors > 0 ? `${rr.errors} error(s) at block ${blockHeight}` : undefined;
@@ -2484,13 +2510,15 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
2484
2510
  } else if (byoOm && manifest && manifest.count > 0) {
2485
2511
  await advanceOperationCursor(tx, byoOm.operationId, blockHeight);
2486
2512
  }
2487
- await applyProgress(tx, runResult);
2513
+ await applyProgress(tx, runResult, manifest ? manifest.count > 0 : false);
2488
2514
  });
2489
2515
  } else {
2490
2516
  try {
2491
2517
  await targetDb.transaction().execute(async (tx) => {
2492
2518
  const opMode = opCursorMode(opts);
2493
- if (statusMode(opts)) {
2519
+ const sm = statusMode(opts);
2520
+ const rewind = isReorgRewind(opts);
2521
+ if (sm) {
2494
2522
  const row = await tx.selectFrom("subgraphs").select("last_processed_block").where("name", "=", subgraphName).executeTakeFirst();
2495
2523
  if (row && Number(row.last_processed_block) >= blockHeight) {
2496
2524
  result.skipped = true;
@@ -2502,6 +2530,12 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
2502
2530
  result.skipped = true;
2503
2531
  return;
2504
2532
  }
2533
+ } else if (!rewind) {
2534
+ const row = await tx.selectFrom("subgraphs").select("last_processed_block").where("name", "=", subgraphName).executeTakeFirst();
2535
+ if (row && Number(row.last_processed_block) >= blockHeight) {
2536
+ result.skipped = true;
2537
+ return;
2538
+ }
2505
2539
  }
2506
2540
  const ctx = new SubgraphContext(tx, schemaName, subgraph.schema, blockMeta, initialTx, false, journalEnabled(opts));
2507
2541
  const handlerStart = performance.now();
@@ -2519,7 +2553,6 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
2519
2553
  }
2520
2554
  flushMs = performance.now() - flushStart;
2521
2555
  }
2522
- const sm = statusMode(opts);
2523
2556
  if (sm && flushedWrites) {
2524
2557
  await updateSubgraphStatus(tx, subgraphName, sm.status, blockHeight);
2525
2558
  } else if (opMode && flushedWrites) {
@@ -2528,10 +2561,10 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
2528
2561
  throw new CursorRaceLostError(opMode.operationId, blockHeight);
2529
2562
  }
2530
2563
  }
2531
- await applyProgress(tx, runResult);
2564
+ await applyProgress(tx, runResult, flushedWrites);
2532
2565
  });
2533
2566
  } catch (err) {
2534
- if (err instanceof CursorRaceLostError) {
2567
+ if (err instanceof CursorRaceLostError || err instanceof LiveCursorRaceLostError) {
2535
2568
  logger6.warn("cursor race lost — block already covered", {
2536
2569
  subgraph: subgraphName,
2537
2570
  blockHeight,
@@ -2544,11 +2577,13 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
2544
2577
  }
2545
2578
  }
2546
2579
  const totalMs = performance.now() - blockStart;
2547
- result.timing = {
2548
- totalMs: Math.round(totalMs),
2549
- handlerMs: Math.round(handlerMs),
2550
- flushMs: Math.round(flushMs)
2551
- };
2580
+ if (!result.skipped) {
2581
+ result.timing = {
2582
+ totalMs: Math.round(totalMs),
2583
+ handlerMs: Math.round(handlerMs),
2584
+ flushMs: Math.round(flushMs)
2585
+ };
2586
+ }
2552
2587
  if (blockHeight % 1000 === 0) {
2553
2588
  try {
2554
2589
  const tables = Object.keys(subgraph.schema);
@@ -2643,6 +2678,33 @@ class StatsAccumulator {
2643
2678
  import { getTargetDb as getTargetDb2 } from "@secondlayer/shared/db";
2644
2679
  import { getSubgraph } from "@secondlayer/shared/db/queries/subgraphs";
2645
2680
  import { logger as logger7 } from "@secondlayer/shared/logger";
2681
+
2682
+ // src/runtime/catchup-leader.ts
2683
+ import {
2684
+ SUBGRAPH_CATCHUP_LOCK_KEY,
2685
+ createPostgresLeaderBackend,
2686
+ withLeaderLock
2687
+ } from "@secondlayer/shared/leader";
2688
+ import { targetListenerUrl } from "@secondlayer/shared/queue/listener";
2689
+ var catchUpLeader = false;
2690
+ function isCatchUpLeader() {
2691
+ return catchUpLeader;
2692
+ }
2693
+ function startCatchUpLeader(opts = {}) {
2694
+ return withLeaderLock(SUBGRAPH_CATCHUP_LOCK_KEY, async () => {
2695
+ catchUpLeader = true;
2696
+ await opts.onAcquire?.();
2697
+ return () => {
2698
+ catchUpLeader = false;
2699
+ };
2700
+ }, {
2701
+ pollMs: opts.pollMs,
2702
+ heartbeatMs: opts.heartbeatMs,
2703
+ createBackend: opts.createBackend ?? (() => createPostgresLeaderBackend(targetListenerUrl()))
2704
+ });
2705
+ }
2706
+
2707
+ // src/runtime/catchup.ts
2646
2708
  var LOG_INTERVAL = 1000;
2647
2709
  var STANDARD_CATCHUP_BATCH_CONFIG = {
2648
2710
  defaultBatchSize: 500,
@@ -2764,6 +2826,13 @@ async function catchUpSubgraph(subgraph, subgraphName) {
2764
2826
  let prefetchedBatchEnd = Math.min(currentHeight + batchSize - 1, chainTip);
2765
2827
  let nextBatchPromise = batchConfig.prefetch ? source.loadBlockRange(currentHeight, prefetchedBatchEnd) : undefined;
2766
2828
  while (currentHeight <= chainTip) {
2829
+ if (!isCatchUpLeader()) {
2830
+ logger7.info("Lost catch-up leadership, stopping catch-up", {
2831
+ subgraph: subgraphName,
2832
+ processed
2833
+ });
2834
+ break;
2835
+ }
2767
2836
  const currentRow = await getSubgraph(targetDb, subgraphName);
2768
2837
  if (!currentRow || currentRow.status !== "active") {
2769
2838
  logger7.info("Subgraph status changed, stopping catch-up", {
@@ -2873,6 +2942,7 @@ import {
2873
2942
  resolveSubgraphRawClient
2874
2943
  } from "@secondlayer/shared/db/queries/subgraphs";
2875
2944
  import { logger as logger8 } from "@secondlayer/shared/logger";
2945
+ import { sql as sql4 } from "kysely";
2876
2946
  async function handleSubgraphReorg(blockHeight, loadSubgraphDef) {
2877
2947
  const targetDb = getTargetDb3();
2878
2948
  const activeSubgraphs = (await listSubgraphs(targetDb)).filter((v) => v.status === "active");
@@ -2889,96 +2959,9 @@ async function handleSubgraphReorg(blockHeight, loadSubgraphDef) {
2889
2959
  continue;
2890
2960
  bumpReorgEpoch(sg.name);
2891
2961
  await withSubgraphBlockLock(sg.name, async () => {
2892
- const schemaName = sg.schema_name ?? pgSchemaName(sg.name);
2893
- const client = resolveSubgraphRawClient(sg);
2894
- const tableNames = Object.keys(schema);
2895
- const revertedByTable = {};
2896
- for (const tableName of tableNames) {
2897
- const rows = await client.unsafe(`SELECT * FROM "${schemaName}"."${tableName}" WHERE "_block_height" >= $1 LIMIT 1000`, [blockHeight]);
2898
- if (rows.length > 0)
2899
- revertedByTable[tableName] = rows;
2900
- }
2901
- const hasJournal = (await client.unsafe(`SELECT to_regclass('"${schemaName}"."_journal"') AS r`))[0]?.r;
2902
- if (hasJournal) {
2903
- await client.begin(async (tx) => {
2904
- for (const tableName of tableNames) {
2905
- const earliest = `
2906
- SELECT DISTINCT ON (row_key) row_key, prev_row
2907
- FROM "${schemaName}"."_journal"
2908
- WHERE block_height >= $1 AND table_name = $2
2909
- ORDER BY row_key, _jid ASC`;
2910
- await tx.unsafe(`DELETE FROM "${schemaName}"."${tableName}" t USING (${earliest}) e WHERE to_jsonb(t.*) @> e.row_key`, [blockHeight, tableName]);
2911
- await tx.unsafe(`DELETE FROM "${schemaName}"."${tableName}" WHERE "_block_height" >= $1`, [blockHeight]);
2912
- await tx.unsafe(`INSERT INTO "${schemaName}"."${tableName}"
2913
- SELECT r.* FROM (${earliest}) e
2914
- CROSS JOIN LATERAL jsonb_populate_record(NULL::"${schemaName}"."${tableName}", e.prev_row) r
2915
- WHERE e.prev_row IS NOT NULL`, [blockHeight, tableName]);
2916
- }
2917
- await tx.unsafe(`DELETE FROM "${schemaName}"."_journal" WHERE block_height >= $1`, [blockHeight]);
2918
- });
2919
- } else {
2920
- logger8.warn("Subgraph has no revert journal — falling back to height delete (accumulator rows may lose history)", { subgraph: sg.name, blockHeight });
2921
- for (const tableName of tableNames) {
2922
- await client.unsafe(`DELETE FROM "${schemaName}"."${tableName}" WHERE "_block_height" >= $1`, [blockHeight]);
2923
- }
2924
- }
2925
- await client.unsafe(`DO $$ BEGIN
2926
- IF to_regclass('"${schemaName}"."_factory_addresses"') IS NOT NULL THEN
2927
- DELETE FROM "${schemaName}"."_factory_addresses" WHERE block_height >= ${Number(blockHeight)};
2928
- END IF;
2929
- END $$`);
2930
- for (const [tableName, rows] of Object.entries(revertedByTable)) {
2931
- if (rows.length === 0)
2932
- continue;
2933
- await targetDb.insertInto("subscription_outbox").columns([
2934
- "subscription_id",
2935
- "subgraph_name",
2936
- "table_name",
2937
- "block_height",
2938
- "event_type",
2939
- "payload",
2940
- "dedup_key",
2941
- "row_pk",
2942
- "status"
2943
- ]).expression((eb) => eb.selectFrom("subscriptions").select((eb2) => [
2944
- "id as subscription_id",
2945
- eb2.val(sg.name).as("subgraph_name"),
2946
- eb2.val(tableName).as("table_name"),
2947
- eb2.val(blockHeight).as("block_height"),
2948
- eb2.val(`${sg.name}.${tableName}.reverted`).as("event_type"),
2949
- eb2.val(JSON.stringify({
2950
- subgraph: sg.name,
2951
- table: tableName,
2952
- reorgFromHeight: blockHeight,
2953
- rows
2954
- })).as("payload"),
2955
- eb2.val(`reorg:${sg.name}:${tableName}:${blockHeight}`).as("dedup_key"),
2956
- eb2.val({
2957
- subgraph: sg.name,
2958
- table: tableName,
2959
- reorgRoot: blockHeight
2960
- }).as("row_pk"),
2961
- eb2.val("pending").as("status")
2962
- ]).where("subgraph_name", "=", sg.name).where("table_name", "=", tableName).where("status", "=", "active")).onConflict((oc) => oc.columns(["subscription_id", "dedup_key"]).doNothing()).execute().catch((err) => {
2963
- logger8.error("Failed to emit revert event for subscriptions", {
2964
- event: "reorg_revert_emit_dropped",
2965
- subgraph: sg.name,
2966
- table: tableName,
2967
- blockHeight,
2968
- error: getErrorMessage2(err)
2969
- });
2970
- });
2971
- }
2972
- logger8.info("Subgraph reorg cleanup done", {
2973
- subgraph: sg.name,
2974
- blockHeight,
2975
- tablesAffected: Object.keys(revertedByTable).length
2976
- });
2977
- const def = await loadSubgraphDef(sg);
2978
- await processBlock(def, sg.name, blockHeight);
2979
- logger8.info("Subgraph reorg reprocessed", {
2980
- subgraph: sg.name,
2981
- blockHeight
2962
+ await getTargetDb3().transaction().execute(async (lockTx) => {
2963
+ await sql4`SELECT pg_advisory_xact_lock(hashtext(${`subgraph-reorg:${sg.name}`}))`.execute(lockTx);
2964
+ await reorgOneSubgraph(sg, schema, blockHeight, loadSubgraphDef);
2982
2965
  });
2983
2966
  });
2984
2967
  } catch (err) {
@@ -2990,9 +2973,103 @@ async function handleSubgraphReorg(blockHeight, loadSubgraphDef) {
2990
2973
  }
2991
2974
  }
2992
2975
  }
2976
+ async function reorgOneSubgraph(sg, schema, blockHeight, loadSubgraphDef) {
2977
+ const targetDb = getTargetDb3();
2978
+ const schemaName = sg.schema_name ?? pgSchemaName(sg.name);
2979
+ const client = resolveSubgraphRawClient(sg);
2980
+ const tableNames = Object.keys(schema);
2981
+ const revertedByTable = {};
2982
+ for (const tableName of tableNames) {
2983
+ const rows = await client.unsafe(`SELECT * FROM "${schemaName}"."${tableName}" WHERE "_block_height" >= $1 LIMIT 1000`, [blockHeight]);
2984
+ if (rows.length > 0)
2985
+ revertedByTable[tableName] = rows;
2986
+ }
2987
+ const hasJournal = (await client.unsafe(`SELECT to_regclass('"${schemaName}"."_journal"') AS r`))[0]?.r;
2988
+ if (hasJournal) {
2989
+ await client.begin(async (tx) => {
2990
+ for (const tableName of tableNames) {
2991
+ const earliest = `
2992
+ SELECT DISTINCT ON (row_key) row_key, prev_row
2993
+ FROM "${schemaName}"."_journal"
2994
+ WHERE block_height >= $1 AND table_name = $2
2995
+ ORDER BY row_key, _jid ASC`;
2996
+ await tx.unsafe(`DELETE FROM "${schemaName}"."${tableName}" t USING (${earliest}) e WHERE to_jsonb(t.*) @> e.row_key`, [blockHeight, tableName]);
2997
+ await tx.unsafe(`DELETE FROM "${schemaName}"."${tableName}" WHERE "_block_height" >= $1`, [blockHeight]);
2998
+ await tx.unsafe(`INSERT INTO "${schemaName}"."${tableName}"
2999
+ SELECT r.* FROM (${earliest}) e
3000
+ CROSS JOIN LATERAL jsonb_populate_record(NULL::"${schemaName}"."${tableName}", e.prev_row) r
3001
+ WHERE e.prev_row IS NOT NULL`, [blockHeight, tableName]);
3002
+ }
3003
+ await tx.unsafe(`DELETE FROM "${schemaName}"."_journal" WHERE block_height >= $1`, [blockHeight]);
3004
+ });
3005
+ } else {
3006
+ logger8.warn("Subgraph has no revert journal — falling back to height delete (accumulator rows may lose history)", { subgraph: sg.name, blockHeight });
3007
+ for (const tableName of tableNames) {
3008
+ await client.unsafe(`DELETE FROM "${schemaName}"."${tableName}" WHERE "_block_height" >= $1`, [blockHeight]);
3009
+ }
3010
+ }
3011
+ await client.unsafe(`DO $$ BEGIN
3012
+ IF to_regclass('"${schemaName}"."_factory_addresses"') IS NOT NULL THEN
3013
+ DELETE FROM "${schemaName}"."_factory_addresses" WHERE block_height >= ${Number(blockHeight)};
3014
+ END IF;
3015
+ END $$`);
3016
+ for (const [tableName, rows] of Object.entries(revertedByTable)) {
3017
+ if (rows.length === 0)
3018
+ continue;
3019
+ await targetDb.insertInto("subscription_outbox").columns([
3020
+ "subscription_id",
3021
+ "subgraph_name",
3022
+ "table_name",
3023
+ "block_height",
3024
+ "event_type",
3025
+ "payload",
3026
+ "dedup_key",
3027
+ "row_pk",
3028
+ "status"
3029
+ ]).expression((eb) => eb.selectFrom("subscriptions").select((eb2) => [
3030
+ "id as subscription_id",
3031
+ eb2.val(sg.name).as("subgraph_name"),
3032
+ eb2.val(tableName).as("table_name"),
3033
+ eb2.val(blockHeight).as("block_height"),
3034
+ eb2.val(`${sg.name}.${tableName}.reverted`).as("event_type"),
3035
+ eb2.val(JSON.stringify({
3036
+ subgraph: sg.name,
3037
+ table: tableName,
3038
+ reorgFromHeight: blockHeight,
3039
+ rows
3040
+ })).as("payload"),
3041
+ eb2.val(`reorg:${sg.name}:${tableName}:${blockHeight}`).as("dedup_key"),
3042
+ eb2.val({
3043
+ subgraph: sg.name,
3044
+ table: tableName,
3045
+ reorgRoot: blockHeight
3046
+ }).as("row_pk"),
3047
+ eb2.val("pending").as("status")
3048
+ ]).where("subgraph_name", "=", sg.name).where("table_name", "=", tableName).where("status", "=", "active")).onConflict((oc) => oc.columns(["subscription_id", "dedup_key"]).doNothing()).execute().catch((err) => {
3049
+ logger8.error("Failed to emit revert event for subscriptions", {
3050
+ event: "reorg_revert_emit_dropped",
3051
+ subgraph: sg.name,
3052
+ table: tableName,
3053
+ blockHeight,
3054
+ error: getErrorMessage2(err)
3055
+ });
3056
+ });
3057
+ }
3058
+ logger8.info("Subgraph reorg cleanup done", {
3059
+ subgraph: sg.name,
3060
+ blockHeight,
3061
+ tablesAffected: Object.keys(revertedByTable).length
3062
+ });
3063
+ const def = await loadSubgraphDef(sg);
3064
+ await processBlock(def, sg.name, blockHeight, { reorgRewind: true });
3065
+ logger8.info("Subgraph reorg reprocessed", {
3066
+ subgraph: sg.name,
3067
+ blockHeight
3068
+ });
3069
+ }
2993
3070
  export {
2994
3071
  handleSubgraphReorg
2995
3072
  };
2996
3073
 
2997
- //# debugId=33E820804CBE066864756E2164756E21
3074
+ //# debugId=314C75DB8B9D518F64756E2164756E21
2998
3075
  //# sourceMappingURL=reorg.js.map