mirror of
https://github.com/tiennm99/miti99bot-js.git
synced 2026-05-16 03:53:10 +00:00
55c873965c
Code prerequisites for the Phase 06 cold-start soak gate. The 24-72h soak
itself is operator-run; this commit ships the instrumentation + analysis
tools needed to make the PROCEED-or-PIVOT decision.
Telemetry
- src/util/timing.js: startTiming(cmd) returns {mark, end} that emits a
structured cmd_timing log. takeColdFlag() returns {cold, isolateAgeMs}
using a module-scoped boolean — first request in an isolate is cold,
subsequent are warm. This replaces the originally-planned
isolate_age_ms < 200ms classifier (broken because Mongo cold-connect
itself is ~1500ms; cold requests would always bucket as warm —
code-reviewer #11).
- src/util/request-context.js: setLastCold/getLastCold shared state
bridges fetch-level cold detection into the dispatcher middleware
without a circular import.
- src/index.js: takeColdFlag at the top of fetch() emits a request log
and primes the request context for the dispatcher.
- src/modules/dispatcher.js: bot.use() middleware times every command.
Chosen over per-handler wrapping to preserve the existing identity
assertion in tests (handler === reg.allCommands.get(name).cmd.handler)
— single instrumentation point, no contract change.
Soak tools (operator-run)
- scripts/analyze-soak.js: parses CF Logs export (NDJSON or CSV), filters
cmd_timing events, computes p50/p95/p99 per (cmd, cold/warm). Counts
dual-write secondary failures, mongo connection errors, CPU-time
exceeded events. Writes markdown report.
- scripts/synthetic-burst.js: fires N parallel synthetic Telegram updates
at the deployed Worker URL with cache-busting tokens. Used for the
pre-deploy connection-cap stress test (debugger #2 — 20 parallel cold
requests, abort if Atlas peak > 60% of 500-conn cap).
- package.json: analyze:soak + burst:synthetic scripts wired.
Tests
- tests/util/timing.test.js: 8 tests — timing semantics, cold flag flip.
- tests/scripts/analyze-soak.test.js: 22 tests — percentile math, NDJSON
+ CSV parse, aggregation, markdown formatting.
Tests: 667 → 697 (+30). Lint clean.
Operator runbook for Phase 06 (NOT executed by this commit):
1. Verify telemetry live via wrangler tail.
2. Run synthetic burst test: npm run burst:synthetic -- --url <prod>
3. Configure Atlas + CF Observability email alerts.
4. 24h soak (extend to 72h on stop-conditions per phase plan).
5. Daily npm run verify:mongo.
6. npm run analyze:soak -- --input <cf-logs.json> → soak-decision.md.
7. PROCEED to Phase 07 if cold-start P95 ≤ 2.5 × BASELINE_COLD_PING_MS;
else execute phase-07-alt-pivot.md (Upstash standby).
105 lines
3.8 KiB
JavaScript
105 lines
3.8 KiB
JavaScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { startTiming, takeColdFlag } from "../../src/util/timing.js";
|
|
|
|
// ── startTiming ──────────────────────────────────────────────────────────────
|
|
|
|
describe("startTiming", () => {
|
|
let consoleSpy;
|
|
|
|
beforeEach(() => {
|
|
consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it("end() emits a cmd_timing JSON line via console.log", () => {
|
|
const t = startTiming("/wordle");
|
|
t.end();
|
|
|
|
expect(consoleSpy).toHaveBeenCalledOnce();
|
|
const raw = consoleSpy.mock.calls[0][0];
|
|
const obj = JSON.parse(raw);
|
|
expect(obj.event).toBe("cmd_timing");
|
|
expect(obj.cmd).toBe("/wordle");
|
|
expect(typeof obj.total).toBe("number");
|
|
expect(obj.total).toBeGreaterThanOrEqual(0);
|
|
expect(Array.isArray(obj.marks)).toBe(true);
|
|
});
|
|
|
|
it("end() merges extra fields into the log object", () => {
|
|
const t = startTiming("/loldle");
|
|
t.end({ cold: true, isolateAgeMs: 42 });
|
|
|
|
const obj = JSON.parse(consoleSpy.mock.calls[0][0]);
|
|
expect(obj.cold).toBe(true);
|
|
expect(obj.isolateAgeMs).toBe(42);
|
|
});
|
|
|
|
it("mark() records named checkpoints with dt >= 0", () => {
|
|
const t = startTiming("/help");
|
|
t.mark("mongo-read");
|
|
t.mark("handler-done");
|
|
t.end();
|
|
|
|
const obj = JSON.parse(consoleSpy.mock.calls[0][0]);
|
|
expect(obj.marks).toHaveLength(2);
|
|
expect(obj.marks[0].label).toBe("mongo-read");
|
|
expect(obj.marks[1].label).toBe("handler-done");
|
|
expect(obj.marks[0].dt).toBeGreaterThanOrEqual(0);
|
|
expect(obj.marks[1].dt).toBeGreaterThanOrEqual(obj.marks[0].dt);
|
|
});
|
|
|
|
it("marks array is empty when no mark() calls made", () => {
|
|
const t = startTiming("/ping");
|
|
t.end();
|
|
const obj = JSON.parse(consoleSpy.mock.calls[0][0]);
|
|
expect(obj.marks).toEqual([]);
|
|
});
|
|
|
|
it("marks is declared at top — no ReferenceError if mark() called before any await", () => {
|
|
// This would throw if `marks` were declared after a conditional or inside a closure.
|
|
expect(() => {
|
|
const t = startTiming("/fortytwo");
|
|
t.mark("immediate");
|
|
t.end();
|
|
}).not.toThrow();
|
|
});
|
|
|
|
it("total reflects elapsed time (>= 0ms)", () => {
|
|
const t = startTiming("/trade");
|
|
t.end();
|
|
const obj = JSON.parse(consoleSpy.mock.calls[0][0]);
|
|
expect(obj.total).toBeGreaterThanOrEqual(0);
|
|
});
|
|
});
|
|
|
|
// ── takeColdFlag ─────────────────────────────────────────────────────────────
|
|
// NOTE: takeColdFlag uses module-scope state that persists across tests in the
|
|
// same vitest worker. We cannot reset it between tests — so we capture the
|
|
// baseline once and assert relative behaviour only.
|
|
|
|
describe("takeColdFlag", () => {
|
|
it("returns an object with cold (boolean) and isolateAgeMs (number)", () => {
|
|
const result = takeColdFlag();
|
|
expect(typeof result.cold).toBe("boolean");
|
|
expect(typeof result.isolateAgeMs).toBe("number");
|
|
expect(result.isolateAgeMs).toBeGreaterThanOrEqual(0);
|
|
});
|
|
|
|
it("second call in same isolate returns cold=false", () => {
|
|
// The first call may have already flipped the flag in a prior test.
|
|
// We call it once here just to ensure it returns cold=false on subsequent invocations.
|
|
takeColdFlag(); // may be first or subsequent
|
|
const second = takeColdFlag();
|
|
expect(second.cold).toBe(false);
|
|
});
|
|
|
|
it("isolateAgeMs is non-decreasing across successive calls", () => {
|
|
const a = takeColdFlag();
|
|
const b = takeColdFlag();
|
|
expect(b.isolateAgeMs).toBeGreaterThanOrEqual(a.isolateAgeMs);
|
|
});
|
|
});
|