fix: improve egress and job diagnostics (#969)
Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
// SPEC: PJ2026-01060509 出站诊断 draft-2026-06-26-p8-egress-job-friction.
|
||||
// Provider-side egress TCP proxy with lifecycle logs for tunnel diagnosis.
|
||||
import { createServer, type Server, type Socket } from "node:net";
|
||||
import type {
|
||||
CoreEgressTcpCloseMessage,
|
||||
@@ -33,6 +35,8 @@ interface Tunnel {
|
||||
closed: boolean;
|
||||
createdAt: number;
|
||||
lastActivityAt: number;
|
||||
targetHost: string;
|
||||
targetPort: number;
|
||||
openTimer: ReturnType<typeof setTimeout> | null;
|
||||
idleTimer: ReturnType<typeof setTimeout> | null;
|
||||
}
|
||||
@@ -116,6 +120,18 @@ function proxyUrlFor(host: string, port: number): string {
|
||||
return `http://${host}:${port}`;
|
||||
}
|
||||
|
||||
function egressFailureKind(error: string | undefined): string {
|
||||
if (error === undefined || error.length === 0) return "normal-close";
|
||||
const normalized = error.toLowerCase();
|
||||
if (normalized.includes("open timeout")) return "open-timeout";
|
||||
if (normalized.includes("idle timeout")) return "idle-timeout";
|
||||
if (normalized.includes("pending buffer")) return "pending-buffer-exceeded";
|
||||
if (normalized.includes("not connected")) return "core-channel-not-connected";
|
||||
if (normalized.includes("timeout")) return "timeout";
|
||||
if (normalized.includes("closed")) return "closed";
|
||||
return "remote-or-socket-error";
|
||||
}
|
||||
|
||||
const tunnelOpenTimeoutMs = 15_000;
|
||||
const tunnelIdleTimeoutMs = 600_000;
|
||||
const maxPendingBytes = 4 * 1024 * 1024;
|
||||
@@ -163,14 +179,19 @@ export function startProviderEgressProxy(options: ProviderEgressProxyOptions): P
|
||||
tunnel.pendingBytes = 0;
|
||||
if (!tunnel.client.destroyed) tunnel.client.destroy();
|
||||
if (notifyCore) send({ type: "egress_tcp_close", providerId: options.providerId, connectionId: tunnel.id, at: nowIso() });
|
||||
if (error !== undefined) {
|
||||
options.logger("warn", "egress_proxy_tunnel_closed", {
|
||||
connectionId: tunnel.id,
|
||||
opened: tunnel.opened,
|
||||
ageMs: Date.now() - tunnel.createdAt,
|
||||
error,
|
||||
});
|
||||
}
|
||||
const event = {
|
||||
providerId: options.providerId,
|
||||
connectionId: tunnel.id,
|
||||
method: tunnel.method,
|
||||
targetHost: tunnel.targetHost,
|
||||
targetPort: tunnel.targetPort,
|
||||
opened: tunnel.opened,
|
||||
ageMs: Date.now() - tunnel.createdAt,
|
||||
pendingBytes: tunnel.pendingBytes,
|
||||
failureKind: egressFailureKind(error),
|
||||
...(error === undefined ? {} : { error }),
|
||||
};
|
||||
options.logger(error === undefined ? "info" : "warn", "egress_proxy_tunnel_closed", event);
|
||||
};
|
||||
|
||||
const closeTunnel = (id: string, error?: string): void => {
|
||||
@@ -206,6 +227,15 @@ export function startProviderEgressProxy(options: ProviderEgressProxyOptions): P
|
||||
tunnel.openTimer = null;
|
||||
}
|
||||
refreshTunnelIdle(tunnel);
|
||||
options.logger("info", "egress_proxy_tunnel_opened", {
|
||||
providerId: options.providerId,
|
||||
connectionId: tunnel.id,
|
||||
method: tunnel.method,
|
||||
targetHost: tunnel.targetHost,
|
||||
targetPort: tunnel.targetPort,
|
||||
ageMs: Date.now() - tunnel.createdAt,
|
||||
pendingBytes: tunnel.pendingBytes,
|
||||
});
|
||||
if (tunnel.method === "CONNECT") {
|
||||
tunnel.client.write("HTTP/1.1 200 Connection Established\r\nProxy-Agent: UniDesk-ProviderGateway\r\n\r\n");
|
||||
}
|
||||
@@ -226,7 +256,14 @@ export function startProviderEgressProxy(options: ProviderEgressProxyOptions): P
|
||||
destroyTunnel(tunnel, false, message.error);
|
||||
}
|
||||
if (message.error !== undefined && message.error.length > 0) {
|
||||
options.logger("warn", "egress_proxy_remote_close", { connectionId: message.connectionId, error: message.error });
|
||||
options.logger("warn", "egress_proxy_remote_close", {
|
||||
providerId: options.providerId,
|
||||
connectionId: message.connectionId,
|
||||
targetHost: tunnel?.targetHost ?? null,
|
||||
targetPort: tunnel?.targetPort ?? null,
|
||||
failureKind: egressFailureKind(message.error),
|
||||
error: message.error,
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -295,10 +332,20 @@ export function startProviderEgressProxy(options: ProviderEgressProxyOptions): P
|
||||
closed: false,
|
||||
createdAt,
|
||||
lastActivityAt: createdAt,
|
||||
targetHost: target.host,
|
||||
targetPort: target.port,
|
||||
openTimer: null,
|
||||
idleTimer: null,
|
||||
};
|
||||
tunnels.set(id, tunnel);
|
||||
options.logger("info", "egress_proxy_tunnel_open_request", {
|
||||
providerId: options.providerId,
|
||||
connectionId: id,
|
||||
method: parsed.method,
|
||||
targetHost: target.host,
|
||||
targetPort: target.port,
|
||||
firstPayloadBytes: firstPayload === null ? 0 : firstPayload.byteLength,
|
||||
});
|
||||
client.on("data", (nextChunk) => {
|
||||
const nextBuffer = Buffer.isBuffer(nextChunk) ? nextChunk : Buffer.from(nextChunk);
|
||||
if (!tunnel.opened) {
|
||||
@@ -315,6 +362,14 @@ export function startProviderEgressProxy(options: ProviderEgressProxyOptions): P
|
||||
tunnels.delete(id);
|
||||
tunnel.closed = true;
|
||||
clearTunnelTimers(tunnel);
|
||||
options.logger("warn", "egress_proxy_tunnel_open_not_sent", {
|
||||
providerId: options.providerId,
|
||||
connectionId: id,
|
||||
method: parsed.method,
|
||||
targetHost: target.host,
|
||||
targetPort: target.port,
|
||||
failureKind: "core-channel-not-connected",
|
||||
});
|
||||
fail("503 Service Unavailable", "provider-gateway core channel is not connected\n");
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user