Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions backend/src/services/sse.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { randomUUID } from 'crypto';
import type { Response } from 'express';
import logger from '../logger.js';
import logger, { requestContext } from '../logger.js';
import { isRedisAvailable, getPublisher, getSubscriber } from '../lib/redis.js';

const HEARTBEAT_INTERVAL_MS = 30_000;
Expand Down Expand Up @@ -30,6 +31,13 @@
private shuttingDown = false;
private perIpPeakConnections = 0;

/**
* Stable id attached to every log line emitted by the heartbeat
* setInterval callback, since it fires outside of any HTTP request and
* would otherwise have no requestContext (and thus no correlation id).
*/
private readonly heartbeatWorkerId = `sse-heartbeat:${randomUUID()}`;

private readonly maxConnections: number = (() => {
const parsed = Number.parseInt(process.env.MAX_SSE_CONNECTIONS ?? '10000', 10);
if (!Number.isFinite(parsed) || parsed <= 0) return 10000;
Expand Down Expand Up @@ -130,7 +138,7 @@
}

broadcast(event: string, data: unknown, filter?: (client: SSEClient) => boolean): void {
const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;

Check failure on line 141 in backend/src/services/sse.service.ts

View workflow job for this annotation

GitHub Actions / Backend npm test

tests/integration/stream-lifecycle.test.ts > Stream Lifecycle Integration Tests > SSE client receives broadcast for each stream event > receives broadcast for stream_created event

TypeError: Do not know how to serialize a BigInt ❯ SSEService.broadcast src/services/sse.service.ts:141:52 ❯ SSEService._localBroadcastToStream src/services/sse.service.ts:174:10 ❯ SSEService.broadcastToStream src/services/sse.service.ts:154:12 ❯ SorobanEventWorker.handleStreamCreated src/workers/soroban-event-worker.ts:574:16 ❯ SorobanEventWorker.processEvent src/workers/soroban-event-worker.ts:320:9 ❯ tests/integration/stream-lifecycle.test.ts:762:7

Check failure on line 141 in backend/src/services/sse.service.ts

View workflow job for this annotation

GitHub Actions / Backend npm test

tests/integration/stream-lifecycle.test.ts > Stream Lifecycle Integration Tests > StreamEvent @@unique([transactionHash, eventType]) deduplication > dedupes worker replay of the same event without creating a second row

TypeError: Do not know how to serialize a BigInt ❯ SSEService.broadcast src/services/sse.service.ts:141:52 ❯ SSEService._localBroadcastToStream src/services/sse.service.ts:174:10 ❯ SSEService.broadcastToStream src/services/sse.service.ts:154:12 ❯ SorobanEventWorker.handleStreamCreated src/workers/soroban-event-worker.ts:574:16 ❯ SorobanEventWorker.processEvent src/workers/soroban-event-worker.ts:320:9 ❯ tests/integration/stream-lifecycle.test.ts:613:7

Check failure on line 141 in backend/src/services/sse.service.ts

View workflow job for this annotation

GitHub Actions / Backend npm test

tests/integration/stream-lifecycle.test.ts > Stream Lifecycle Integration Tests > Indexer → stream_created: stream appears in GET /v1/streams/:id > processes stream_created event and makes stream available via API

TypeError: Do not know how to serialize a BigInt ❯ SSEService.broadcast src/services/sse.service.ts:141:52 ❯ SSEService._localBroadcastToStream src/services/sse.service.ts:174:10 ❯ SSEService.broadcastToStream src/services/sse.service.ts:154:12 ❯ SorobanEventWorker.handleStreamCreated src/workers/soroban-event-worker.ts:574:16 ❯ SorobanEventWorker.processEvent src/workers/soroban-event-worker.ts:320:9 ❯ tests/integration/stream-lifecycle.test.ts:241:7

for (const client of this.clients.values()) {
if (!filter || filter(client)) {
Expand Down Expand Up @@ -207,7 +215,9 @@
}

this.heartbeatTimer = setInterval(() => {
this.sendHeartbeat();
requestContext.run({ requestId: this.heartbeatWorkerId }, () => {
this.sendHeartbeat();
});
}, HEARTBEAT_INTERVAL_MS);
}

Expand Down
33 changes: 24 additions & 9 deletions backend/src/workers/soroban-event-worker.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { randomUUID } from "crypto";
import { rpc, xdr, StrKey } from "@stellar/stellar-sdk";
import { prisma } from "../lib/prisma.js";
import { INDEXER_STATE_ID } from "../lib/indexer-state.js";
import { sseService } from "../services/sse.service.js";
import logger from "../logger.js";
import logger, { requestContext } from "../logger.js";
import { Prisma } from "../generated/prisma/index.js";

// ─── Config ──────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -33,7 +34,7 @@
* Full value = hi * 2^64 + lo.
*/
export function decodeI128(val: xdr.ScVal): string {
const parts = val.i128();

Check failure on line 37 in backend/src/workers/soroban-event-worker.ts

View workflow job for this annotation

GitHub Actions / Backend npm test

tests/integration/stream-lifecycle.test.ts > Stream Lifecycle Integration Tests > Full lifecycle: create → top up → partial withdraw → cancel > walks a single stream through every phase and verifies indexer state

TypeError: i128 not set ❯ ChildUnion.get ../node_modules/@stellar/js-xdr/lib/webpack:/XDR/src/union.js:27:13 ❯ ChildUnion.get [as i128] ../node_modules/@stellar/js-xdr/lib/webpack:/XDR/src/union.js:171:25 ❯ decodeI128 src/workers/soroban-event-worker.ts:37:21 ❯ SorobanEventWorker.handleStreamCreated src/workers/soroban-event-worker.ts:488:27 ❯ SorobanEventWorker.processEvent src/workers/soroban-event-worker.ts:320:20 ❯ tests/integration/stream-lifecycle.test.ts:635:20
const hi = BigInt.asIntN(64, BigInt(parts.hi().toString()));
const lo = BigInt.asUintN(64, BigInt(parts.lo().toString()));
return ((hi << 64n) | lo).toString();
Expand Down Expand Up @@ -79,6 +80,13 @@
private pollTimer: NodeJS.Timeout | undefined;
private activeBatch: Promise<void> | null = null;

/**
* Stable id attached to every log line emitted by the background poll
* loop, since these callbacks fire outside of any HTTP request and would
* otherwise have no requestContext (and thus no correlation id) at all.
*/
private readonly workerId = `soroban-worker:${randomUUID()}`;

constructor() {
const rpcUrl =
process.env.SOROBAN_RPC_URL ?? "https://soroban-testnet.stellar.org";
Expand Down Expand Up @@ -172,15 +180,22 @@
}

private async poll(): Promise<void> {
this.activeBatch = this.fetchAndProcessEvents().catch((err) => {
logger.error("[SorobanWorker] Unhandled error during poll:", err);
// Run every poll cycle inside its own AsyncLocalStorage context so that
// logs emitted from this setTimeout-driven callback (and anything it
// awaits, including the DB/RPC calls in fetchAndProcessEvents) carry a
// stable worker-scoped correlation id instead of silently dropping out
// of context.
await requestContext.run({ requestId: this.workerId }, async () => {
this.activeBatch = this.fetchAndProcessEvents().catch((err) => {
logger.error("[SorobanWorker] Unhandled error during poll:", err);
});
try {
await this.activeBatch;
} finally {
this.activeBatch = null;
this.scheduleNext();
}
});
try {
await this.activeBatch;
} finally {
this.activeBatch = null;
this.scheduleNext();
}
}

/**
Expand Down Expand Up @@ -1026,7 +1041,7 @@
// Calculate the duration of this pause interval
let additionalPausedDuration = 0;
if (currentStream.pausedAt) {
additionalPausedDuration = timestamp - currentStream.pausedAt;

Check failure on line 1044 in backend/src/workers/soroban-event-worker.ts

View workflow job for this annotation

GitHub Actions / Backend npm test

tests/integration/stream-lifecycle.test.ts > Stream Lifecycle Integration Tests > Indexer → stream_resumed: isPaused = false, accrual resumes > sets isPaused=false and accrual resumes correctly

TypeError: Cannot mix BigInt and other types, use explicit conversions ❯ src/workers/soroban-event-worker.ts:1044:36 ❯ Proxy._transactionWithCallback src/generated/prisma/runtime/client.js:103:4800 ❯ SorobanEventWorker.handleStreamResumed src/workers/soroban-event-worker.ts:1034:5 ❯ SorobanEventWorker.processEvent src/workers/soroban-event-worker.ts:332:9 ❯ tests/integration/stream-lifecycle.test.ts:404:7
}

const newTotalPausedDuration =
Expand Down
Loading