添加全局错误处理机制,优化运行时错误日志记录,提升应用稳定性和错误追踪能力。

This commit is contained in:
discountry
2025-09-27 00:01:39 +08:00
parent 49d5d4f3b8
commit 2c62d2ed3e
4 changed files with 181 additions and 30 deletions
+13 -8
View File
@@ -25,7 +25,7 @@ import {
unlockOperating,
} from "./order-coordinator";
import type { OrderLockMap, OrderPendingMap, OrderTimerMap } from "./order-coordinator";
import { isUnknownOrderError } from "../utils/errors";
import { extractMessage, isUnknownOrderError } from "../utils/errors";
import { roundDownToTick } from "../utils/math";
import { createTradeLog, type TradeLogEntry } from "../state/trade-log";
import { decryptCopyright } from "../utils/copyright";
@@ -153,7 +153,7 @@ export class TrendEngine {
this.updateSessionVolume(position);
this.emitUpdate();
} catch (err) {
this.tradeLog.push("error", `账户推送处理异常: ${String(err)}`);
this.tradeLog.push("error", `账户推送处理异常: ${extractMessage(err)}`);
}
});
} catch (err) {
@@ -178,7 +178,7 @@ export class TrendEngine {
this.ordersSnapshotReady = true;
this.emitUpdate();
} catch (err) {
this.tradeLog.push("error", `订单推送处理异常: ${String(err)}`);
this.tradeLog.push("error", `订单推送处理异常: ${extractMessage(err)}`);
}
});
} catch (err) {
@@ -190,7 +190,7 @@ export class TrendEngine {
this.depthSnapshot = depth;
this.emitUpdate();
} catch (err) {
this.tradeLog.push("error", `深度推送处理异常: ${String(err)}`);
this.tradeLog.push("error", `深度推送处理异常: ${extractMessage(err)}`);
}
});
} catch (err) {
@@ -202,7 +202,7 @@ export class TrendEngine {
this.tickerSnapshot = ticker;
this.emitUpdate();
} catch (err) {
this.tradeLog.push("error", `价格推送处理异常: ${String(err)}`);
this.tradeLog.push("error", `价格推送处理异常: ${extractMessage(err)}`);
}
});
} catch (err) {
@@ -214,7 +214,7 @@ export class TrendEngine {
this.klineSnapshot = Array.isArray(klines) ? klines : [];
this.emitUpdate();
} catch (err) {
this.tradeLog.push("error", `K线推送处理异常: ${String(err)}`);
this.tradeLog.push("error", `K线推送处理异常: ${extractMessage(err)}`);
}
});
} catch (err) {
@@ -300,8 +300,13 @@ export class TrendEngine {
}
this.emitUpdate();
} finally {
this.rateLimit.onCycleComplete(hadRateLimit);
this.processing = false;
try {
this.rateLimit.onCycleComplete(hadRateLimit);
} catch (rateLimitError) {
this.tradeLog.push("error", `限频控制器状态更新失败: ${String(rateLimitError)}`);
} finally {
this.processing = false;
}
}
}
+78 -21
View File
@@ -7,6 +7,7 @@ import type {
TickerListener,
} from "./adapter";
import type { AsterOrder, CreateOrderParams, AsterDepth, AsterTicker, AsterKline } from "./types";
import { extractMessage } from "../utils/errors";
import { AsterGateway } from "./aster/client";
export interface AsterCredentials {
@@ -20,71 +21,127 @@ export class AsterExchangeAdapter implements ExchangeAdapter {
private readonly gateway: AsterGateway;
private readonly symbol: string;
private initPromise: Promise<void> | null = null;
private lastInitErrorAt = 0;
private readonly initContexts = new Set<string>();
private retryTimer: ReturnType<typeof setTimeout> | null = null;
private retryDelayMs = 3000;
constructor(credentials: AsterCredentials = {}) {
this.gateway = new AsterGateway({ apiKey: credentials.apiKey, apiSecret: credentials.apiSecret });
this.symbol = (credentials.symbol ?? process.env.TRADE_SYMBOL ?? "BTCUSDT").toUpperCase();
}
private ensureInitialized(): Promise<void> {
private safeInvoke<T extends (...args: any[]) => void>(context: string, cb: T): T {
const wrapped = ((...args: any[]) => {
try {
cb(...args);
} catch (error) {
console.error(`[AsterExchangeAdapter] ${context} handler failed: ${extractMessage(error)}`);
}
}) as T;
return wrapped;
}
private ensureInitialized(context?: string): Promise<void> {
if (!this.initPromise) {
this.initPromise = this.gateway.ensureInitialized(this.symbol);
this.initContexts.clear();
this.initPromise = this.gateway.ensureInitialized(this.symbol).then((value) => {
this.clearRetry();
return value;
}).catch((error) => {
this.handleInitError("initialize", error);
this.initPromise = null;
this.scheduleRetry();
throw error;
});
}
if (context && !this.initContexts.has(context)) {
this.initContexts.add(context);
this.initPromise.catch((error) => {
this.handleInitError(context, error);
this.scheduleRetry();
});
}
return this.initPromise;
}
private scheduleRetry(): void {
if (this.retryTimer) return;
this.retryTimer = setTimeout(() => {
this.retryTimer = null;
if (this.initPromise) return;
this.retryDelayMs = Math.min(this.retryDelayMs * 2, 60_000);
void this.ensureInitialized("retry");
}, this.retryDelayMs);
}
private clearRetry(): void {
if (this.retryTimer) {
clearTimeout(this.retryTimer);
this.retryTimer = null;
}
this.retryDelayMs = 3000;
}
private handleInitError(context: string, error: unknown): void {
const now = Date.now();
if (now - this.lastInitErrorAt < 5000) return;
this.lastInitErrorAt = now;
console.error(`[AsterExchangeAdapter] ${context} failed`, error);
}
watchAccount(cb: AccountListener): void {
void this.ensureInitialized();
this.gateway.onAccount((snapshot) => {
void this.ensureInitialized("watchAccount");
this.gateway.onAccount(this.safeInvoke("watchAccount", (snapshot) => {
cb(snapshot);
});
}));
}
watchOrders(cb: OrderListener): void {
void this.ensureInitialized();
this.gateway.onOrders((orders) => {
void this.ensureInitialized("watchOrders");
this.gateway.onOrders(this.safeInvoke("watchOrders", (orders) => {
cb(orders);
});
}));
}
watchDepth(symbol: string, cb: DepthListener): void {
void this.ensureInitialized();
this.gateway.onDepth(symbol, (depth: AsterDepth) => {
void this.ensureInitialized("watchDepth");
this.gateway.onDepth(symbol, this.safeInvoke("watchDepth", (depth: AsterDepth) => {
cb(depth);
});
}));
}
watchTicker(symbol: string, cb: TickerListener): void {
void this.ensureInitialized();
this.gateway.onTicker(symbol, (ticker: AsterTicker) => {
void this.ensureInitialized("watchTicker");
this.gateway.onTicker(symbol, this.safeInvoke("watchTicker", (ticker: AsterTicker) => {
cb(ticker);
});
}));
}
watchKlines(symbol: string, interval: string, cb: KlineListener): void {
void this.ensureInitialized();
this.gateway.onKlines(symbol, interval, (klines: AsterKline[]) => {
void this.ensureInitialized("watchKlines");
this.gateway.onKlines(symbol, interval, this.safeInvoke("watchKlines", (klines: AsterKline[]) => {
cb(klines);
});
}));
}
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
await this.ensureInitialized();
await this.ensureInitialized("createOrder");
return this.gateway.createOrder(params);
}
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
await this.ensureInitialized();
await this.ensureInitialized("cancelOrder");
await this.gateway.cancelOrder({ symbol: params.symbol, orderId: Number(params.orderId) });
}
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
await this.ensureInitialized();
await this.ensureInitialized("cancelOrders");
await this.gateway.cancelOrders({ symbol: params.symbol, orderIdList: params.orderIdList });
}
async cancelAllOrders(params: { symbol: string }): Promise<void> {
await this.ensureInitialized();
await this.ensureInitialized("cancelAllOrders");
await this.gateway.cancelAllOrders(params);
}
}
+3 -1
View File
@@ -1,6 +1,8 @@
import "dotenv/config";
import React from "react";
import { render } from "ink";
import { App } from "./ui/App";
import { setupGlobalErrorHandlers } from "./runtime-errors";
setupGlobalErrorHandlers();
render(<App />);
+87
View File
@@ -0,0 +1,87 @@
import { extractMessage } from "./utils/errors";
declare const Bun: { on?: (event: string, listener: (payload: any) => void) => void } | undefined;
type Handler = (error: unknown) => void;
let installed = false;
const lastLogAt = new Map<string, number>();
function logRuntimeIssue(kind: string, error: unknown): void {
const message = extractMessage(error);
const now = Date.now();
const key = `${kind}:${message}`;
const previous = lastLogAt.get(key) ?? 0;
if (now - previous < 1000) return;
lastLogAt.set(key, now);
console.error(`[RuntimeGuard] ${kind}: ${message}`);
if (error instanceof Error && error.stack) {
console.error(error.stack);
}
}
function bindProcessEvent(event: string, handler: Handler): void {
if (typeof process === "undefined" || typeof process.on !== "function") return;
process.on(event as any, (error: unknown) => {
try {
handler(error);
} catch (loggingError) {
console.error(`[RuntimeGuard] Failed to log ${event}:`, loggingError);
}
});
}
export function setupGlobalErrorHandlers(): void {
if (installed) return;
installed = true;
bindProcessEvent("uncaughtException", (error) => {
logRuntimeIssue("uncaughtException", error);
});
bindProcessEvent("unhandledRejection", (reason) => {
logRuntimeIssue("unhandledRejection", reason);
});
bindProcessEvent("multipleResolves", (payload) => {
logRuntimeIssue("multipleResolves", payload);
});
const globalWithEvents = globalThis as unknown as {
addEventListener?: (type: string, listener: (event: any) => void) => void;
};
if (typeof globalWithEvents.addEventListener === "function") {
try {
globalWithEvents.addEventListener("unhandledrejection", (event: any) => {
logRuntimeIssue("unhandledRejection", event?.reason);
if (event && typeof event.preventDefault === "function") {
event.preventDefault();
}
});
globalWithEvents.addEventListener("error", (event: any) => {
logRuntimeIssue("unhandledError", event?.error ?? event);
if (event && typeof event.preventDefault === "function") {
event.preventDefault();
}
});
} catch (handlerError) {
console.error("[RuntimeGuard] Failed to bind global listeners", handlerError);
}
}
if (typeof Bun !== "undefined" && typeof Bun?.on === "function") {
try {
Bun.on("unhandledRejection", (reason: unknown) => {
logRuntimeIssue("bun.unhandledRejection", reason);
});
Bun.on("error", (error: unknown) => {
logRuntimeIssue("bun.error", error);
});
} catch (bunHandlerError) {
console.error("[RuntimeGuard] Failed to bind Bun listeners", bunHandlerError);
}
}
}