feat: implement feed staleness monitoring in LighterGateway to enhance connection reliability and update handling

This commit is contained in:
discountry
2025-11-12 23:57:21 +08:00
parent f48371ccd8
commit 486b911bed
+76 -1
View File
@@ -120,6 +120,8 @@ const DEFAULT_TICKER_POLL_MS = 3000;
const DEFAULT_KLINE_POLL_MS = 15000; const DEFAULT_KLINE_POLL_MS = 15000;
const WS_HEARTBEAT_INTERVAL_MS = 5_000; const WS_HEARTBEAT_INTERVAL_MS = 5_000;
const WS_STALE_TIMEOUT_MS = 20_000; const WS_STALE_TIMEOUT_MS = 20_000;
const FEED_STALE_TIMEOUT_MS = 30_000;
const STALE_CHECK_INTERVAL_MS = 5_000;
const ACCOUNT_POLL_INTERVAL_MS = 5_000; const ACCOUNT_POLL_INTERVAL_MS = 5_000;
const ACCOUNT_HTTP_EMPTY_CONFIRM_MS = 15_000; const ACCOUNT_HTTP_EMPTY_CONFIRM_MS = 15_000;
const POSITION_EPSILON = 1e-12; const POSITION_EPSILON = 1e-12;
@@ -207,6 +209,12 @@ export class LighterGateway {
private readonly tickerPollMs: number; private readonly tickerPollMs: number;
private readonly klinePollMs: number; private readonly klinePollMs: number;
private lastDepthUpdateAt = Date.now();
private lastOrdersUpdateAt = Date.now();
private lastAccountUpdateAt = Date.now();
private lastTickerUpdateAt = Date.now();
private staleReason: string | null = null;
private staleMonitor: ReturnType<typeof setInterval> | null = null;
// Track last applied order book sequence to drop stale WS messages // Track last applied order book sequence to drop stale WS messages
private lastOrderBookOffset: number = 0; private lastOrderBookOffset: number = 0;
@@ -252,6 +260,11 @@ export class LighterGateway {
this.klinePollMs = options.klinePollMs ?? DEFAULT_KLINE_POLL_MS; this.klinePollMs = options.klinePollMs ?? DEFAULT_KLINE_POLL_MS;
this.l1Address = options.l1Address ?? null; this.l1Address = options.l1Address ?? null;
this.logTxInfo = process.env.LIGHTER_LOG_TX === "1" || process.env.LIGHTER_LOG_TX === "true"; this.logTxInfo = process.env.LIGHTER_LOG_TX === "1" || process.env.LIGHTER_LOG_TX === "true";
const now = Date.now();
this.lastDepthUpdateAt = now;
this.lastOrdersUpdateAt = now;
this.lastAccountUpdateAt = now;
this.lastTickerUpdateAt = now;
} }
async ensureInitialized(): Promise<void> { async ensureInitialized(): Promise<void> {
@@ -389,6 +402,7 @@ export class LighterGateway {
// orders until there is activity. // orders until there is activity.
this.emitOrders(); this.emitOrders();
this.startPolling(); this.startPolling();
this.startStaleMonitor();
} }
private async loadMetadata(): Promise<void> { private async loadMetadata(): Promise<void> {
@@ -602,6 +616,19 @@ export class LighterGateway {
}, 2000); }, 2000);
} }
private forceReconnect(reason: string): void {
if (this.staleReason) return;
this.staleReason = reason;
this.logger("ws:stale", reason);
try {
this.ws?.terminate();
} catch (error) {
this.logger("ws:terminate", error);
}
this.stopHeartbeat();
this.scheduleReconnect();
}
private startHeartbeat(): void { private startHeartbeat(): void {
if (this.heartbeatTimer) return; if (this.heartbeatTimer) return;
this.heartbeatTimer = setInterval(() => { this.heartbeatTimer = setInterval(() => {
@@ -688,6 +715,7 @@ export class LighterGateway {
this.lastOrderBookOffset = snapshot.offset ?? incomingOffset ?? this.lastOrderBookOffset; this.lastOrderBookOffset = snapshot.offset ?? incomingOffset ?? this.lastOrderBookOffset;
this.lastOrderBookTimestamp = incomingTs || Date.now(); this.lastOrderBookTimestamp = incomingTs || Date.now();
this.emitDepth(); this.emitDepth();
this.markDepthUpdate();
} }
private handleOrderBookUpdate(message: any): void { private handleOrderBookUpdate(message: any): void {
@@ -714,6 +742,7 @@ export class LighterGateway {
this.lastOrderBookOffset = Number(this.orderBook.offset ?? incomingOffset ?? this.lastOrderBookOffset); this.lastOrderBookOffset = Number(this.orderBook.offset ?? incomingOffset ?? this.lastOrderBookOffset);
this.lastOrderBookTimestamp = incomingTs || Date.now(); this.lastOrderBookTimestamp = incomingTs || Date.now();
this.emitDepth(); this.emitDepth();
this.markDepthUpdate();
} }
private handleAccountAll(message: any): void { private handleAccountAll(message: any): void {
@@ -916,7 +945,7 @@ export class LighterGateway {
} }
const key = orderIndex ?? clientIndex; const key = orderIndex ?? clientIndex;
if (!key) return; if (!key) return;
const status = (order.status ?? "").toLowerCase(); const status = String(order.status ?? "").toLowerCase();
if (TERMINAL_ORDER_STATUSES.has(status)) { if (TERMINAL_ORDER_STATUSES.has(status)) {
const existing = this.orderMap.get(key); const existing = this.orderMap.get(key);
this.orderMap.delete(key); this.orderMap.delete(key);
@@ -989,6 +1018,13 @@ export class LighterGateway {
this.emitSyntheticTicker(); this.emitSyntheticTicker();
} }
private markDepthUpdate(): void {
this.lastDepthUpdateAt = Date.now();
if (this.staleReason && this.staleReason.startsWith("depth")) {
this.staleReason = null;
}
}
private emitAccount(): void { private emitAccount(): void {
if (!this.accountDetails) return; if (!this.accountDetails) return;
const snapshot = toAccountSnapshot( const snapshot = toAccountSnapshot(
@@ -999,11 +1035,19 @@ export class LighterGateway {
{ marketSymbol: this.marketSymbol, marketId: this.marketId } { marketSymbol: this.marketSymbol, marketId: this.marketId }
); );
this.accountEvent.emit(snapshot); this.accountEvent.emit(snapshot);
this.lastAccountUpdateAt = Date.now();
if (this.staleReason && this.staleReason.startsWith("account")) {
this.staleReason = null;
}
} }
private emitOrders(): void { private emitOrders(): void {
const mapped = toOrders(this.displaySymbol, this.orders ?? []); const mapped = toOrders(this.displaySymbol, this.orders ?? []);
this.ordersEvent.emit(mapped); this.ordersEvent.emit(mapped);
this.lastOrdersUpdateAt = Date.now();
if (this.staleReason && this.staleReason.startsWith("orders")) {
this.staleReason = null;
}
} }
private resolveOrderIndex(orderId: string): string { private resolveOrderIndex(orderId: string): string {
@@ -1077,6 +1121,33 @@ export class LighterGateway {
} }
} }
private startStaleMonitor(): void {
if (this.staleMonitor) return;
this.staleMonitor = setInterval(() => this.checkFeedStaleness(), STALE_CHECK_INTERVAL_MS);
}
private stopStaleMonitor(): void {
if (!this.staleMonitor) return;
clearInterval(this.staleMonitor);
this.staleMonitor = null;
}
private checkFeedStaleness(): void {
if (this.staleReason) return;
const now = Date.now();
if (now - this.lastDepthUpdateAt > FEED_STALE_TIMEOUT_MS) {
this.forceReconnect("depth stale");
return;
}
if (now - this.lastOrdersUpdateAt > FEED_STALE_TIMEOUT_MS) {
this.forceReconnect("orders stale");
return;
}
if (now - this.lastAccountUpdateAt > FEED_STALE_TIMEOUT_MS * 2) {
this.forceReconnect("account stale");
}
}
private async refreshTicker(): Promise<void> { private async refreshTicker(): Promise<void> {
try { try {
const stats = await this.http.getExchangeStats(); const stats = await this.http.getExchangeStats();
@@ -1089,6 +1160,10 @@ export class LighterGateway {
const ticker = toTicker(this.displaySymbol, match); const ticker = toTicker(this.displaySymbol, match);
this.tickerEvent.emit(ticker); this.tickerEvent.emit(ticker);
this.loggedCreateOrderPayload = false; this.loggedCreateOrderPayload = false;
this.lastTickerUpdateAt = Date.now();
if (this.staleReason && this.staleReason.startsWith("ticker")) {
this.staleReason = null;
}
} catch (error) { } catch (error) {
this.logger("refreshTicker", error); this.logger("refreshTicker", error);
} }