




















@@ -1,10 +1,11 @@
11import { spawnSync } from "node:child_process";
2+import { EventEmitter } from "node:events";
23import fs from "node:fs";
34import { createServer } from "node:http";
45import os from "node:os";
56import path from "node:path";
67import { performance } from "node:perf_hooks";
7-import { describe, expect, it } from "vitest";
8+import { describe, expect, it, vi } from "vitest";
89import { testing } from "../../scripts/bench-gateway-startup.ts";
9101011async function listenOnLoopback(handler: Parameters<typeof createServer>[0]) {
@@ -154,6 +155,176 @@ describe("gateway startup benchmark script", () => {
154155]);
155156});
156157158+it("flags samples that become ready and then exit nonzero", () => {
159+const result = testing.summarizeCase({ config: {}, id: "demo", name: "demo" }, [
160+{
161+cpuCoreRatio: 0.5,
162+cpuMs: 100,
163+exitedBeforeTeardown: true,
164+exitCode: 1,
165+firstOutputMs: 1,
166+gatewayReadyLogLine: "[gateway] ready",
167+gatewayReadyLogMs: 20,
168+healthz: {
169+firstErrorKind: "econnrefused",
170+firstRecoveryMs: 10,
171+ms: 10,
172+status: 200,
173+transitions: [],
174+},
175+httpListenLogLine: "[gateway] http server listening (0 plugins)",
176+httpListenLogMs: 5,
177+maxRssMb: 120,
178+outputTail: "ready\\nError: startup sidecar crashed",
179+readyz: {
180+firstErrorKind: "http-503",
181+firstRecoveryMs: 18,
182+ms: 18,
183+status: 200,
184+transitions: [],
185+},
186+signal: null,
187+startupTrace: {},
188+},
189+]);
190+191+expect(testing.collectResultFailures([result], { processMetricsRequired: true })).toEqual([
192+{
193+id: "demo",
194+reason: "child exited 1",
195+sampleIndex: 1,
196+},
197+]);
198+});
199+200+it("does not flag nonzero exits from intentional teardown", () => {
201+const result = testing.summarizeCase({ config: {}, id: "demo", name: "demo" }, [
202+{
203+cpuCoreRatio: 0.5,
204+cpuMs: 100,
205+exitedBeforeTeardown: false,
206+exitCode: 1,
207+firstOutputMs: 1,
208+gatewayReadyLogLine: "[gateway] ready",
209+gatewayReadyLogMs: 20,
210+healthz: {
211+firstErrorKind: "econnrefused",
212+firstRecoveryMs: 10,
213+ms: 10,
214+status: 200,
215+transitions: [],
216+},
217+httpListenLogLine: "[gateway] http server listening (0 plugins)",
218+httpListenLogMs: 5,
219+maxRssMb: 120,
220+outputTail: "",
221+readyz: {
222+firstErrorKind: "http-503",
223+firstRecoveryMs: 18,
224+ms: 18,
225+status: 200,
226+transitions: [],
227+},
228+signal: null,
229+startupTrace: {},
230+},
231+]);
232+233+expect(testing.collectResultFailures([result], { processMetricsRequired: true })).toEqual([]);
234+});
235+236+it("flags samples that become ready and then die from a signal", () => {
237+const result = testing.summarizeCase({ config: {}, id: "demo", name: "demo" }, [
238+{
239+cpuCoreRatio: 0.5,
240+cpuMs: 100,
241+exitedBeforeTeardown: true,
242+exitCode: null,
243+firstOutputMs: 1,
244+gatewayReadyLogLine: "[gateway] ready",
245+gatewayReadyLogMs: 20,
246+healthz: {
247+firstErrorKind: "econnrefused",
248+firstRecoveryMs: 10,
249+ms: 10,
250+status: 200,
251+transitions: [],
252+},
253+httpListenLogLine: "[gateway] http server listening (0 plugins)",
254+httpListenLogMs: 5,
255+maxRssMb: 120,
256+outputTail: "ready\\nsegmentation fault",
257+readyz: {
258+firstErrorKind: "http-503",
259+firstRecoveryMs: 18,
260+ms: 18,
261+status: 200,
262+transitions: [],
263+},
264+signal: "SIGSEGV",
265+startupTrace: {},
266+},
267+]);
268+269+expect(testing.collectResultFailures([result], { processMetricsRequired: true })).toEqual([
270+{
271+id: "demo",
272+reason: "child exited by SIGSEGV",
273+sampleIndex: 1,
274+},
275+]);
276+});
277+278+it("classifies queued child exits before sending teardown signals", async () => {
279+const child = new EventEmitter() as EventEmitter & {
280+exitCode: number | null;
281+kill: ReturnType<typeof vi.fn>;
282+signalCode: NodeJS.Signals | null;
283+};
284+child.exitCode = null;
285+child.signalCode = null;
286+child.kill = vi.fn(() => true);
287+288+const stopped = testing.stopChild(child as unknown as Parameters<typeof testing.stopChild>[0]);
289+queueMicrotask(() => {
290+child.exitCode = 7;
291+child.emit("exit", 7, null);
292+});
293+294+await expect(stopped).resolves.toEqual({
295+exitedBeforeTeardown: true,
296+exitCode: 7,
297+signal: null,
298+});
299+expect(child.kill).not.toHaveBeenCalled();
300+});
301+302+it("classifies failed teardown signaling as a pre-teardown child exit", async () => {
303+const child = new EventEmitter() as EventEmitter & {
304+exitCode: number | null;
305+kill: ReturnType<typeof vi.fn>;
306+signalCode: NodeJS.Signals | null;
307+};
308+child.exitCode = null;
309+child.signalCode = null;
310+child.kill = vi.fn(() => {
311+setImmediate(() => {
312+child.exitCode = 8;
313+child.emit("exit", 8, null);
314+});
315+return false;
316+});
317+318+await expect(
319+testing.stopChild(child as unknown as Parameters<typeof testing.stopChild>[0]),
320+).resolves.toEqual({
321+exitedBeforeTeardown: true,
322+exitCode: 8,
323+signal: null,
324+});
325+expect(child.kill).toHaveBeenCalledWith("SIGTERM");
326+});
327+157328it("collects Count-suffixed startup trace metrics", () => {
158329const startupTrace: Record<string, number> = {};
159330此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。