fix(photon): use GUID for liveness probe message id

(cherry picked from commit cdb6ccadb045e599db795823a100ae7b61d377ec)
This commit is contained in:
Forkbert
2026-09-20 15:51:00 -04:00
committed by Teknium
parent fafa2e215c
commit b113ab6de6
3 changed files with 27 additions and 4 deletions

View File

@@ -69,6 +69,7 @@ import { patchSpectrumTs } from "./patch-spectrum-mixed-attachments.mjs";
import { chooseSendFormat } from "./send-format.mjs";
import {
classifyProbeRejection,
createProbeMessageId,
shouldProbe,
isZombieSuspect,
} from "./stream-staleness.mjs";
@@ -267,7 +268,7 @@ console.log = (...args) => {
// half-open ("zombie") one. `space.get` is purely local in shared/dedicated
// mode (no chat is created or messaged); only the message read hits the wire.
const PROBE_SPACE_ID = process.env.PHOTON_PROBE_SPACE_ID || "any;-;+10000000000";
const PROBE_MSG_PREFIX = "hermes-liveness-probe-";
if (!projectId || !projectSecret || !sharedToken) {
console.error(
@@ -730,8 +731,7 @@ async function probeUpstream() {
if (typeof app?.stop !== "function") {
return { alive: false, hung: false, reason: "spectrum app not constructed" };
}
const probeId =
PROBE_MSG_PREFIX + Date.now() + "-" + Math.random().toString(36).slice(2);
const probeId = createProbeMessageId();
let timer = null;
const timeout = new Promise((resolve) => {
timer = setTimeout(

View File

@@ -23,9 +23,15 @@
// These helpers are pure (no SDK, no timers) so tests can execute them under
// node — see tests/plugins/platforms/photon/test_zombie_stream_watchdog.py.
import { randomUUID } from "node:crypto";
// gRPC NOT_FOUND is code 5; SDKs also surface it as "not found" / "NotFound"
// message text. Anything not clearly not-found is inconclusive.
const NOT_FOUND_RE = /not[\s_-]?found/i;
/** Return a unique message id accepted by Spectrum's message-id parser. */
export function createProbeMessageId() {
return randomUUID();
}
/**
* Classify the rejection of the synthetic-id probe read.

View File

@@ -25,12 +25,14 @@ from __future__ import annotations
import asyncio
import json
import re
import subprocess
from pathlib import Path
from typing import Any, Dict
import pytest
from gateway.config import PlatformConfig
from plugins.platforms.photon.adapter import PhotonAdapter
@@ -48,7 +50,8 @@ def _make_adapter(monkeypatch: pytest.MonkeyPatch) -> PhotonAdapter:
def _run_staleness_harness(script: str) -> Dict[str, Any]:
harness = (
"import { classifyProbeRejection, shouldProbe, isZombieSuspect } "
"import { classifyProbeRejection, shouldProbe, isZombieSuspect, "
"createProbeMessageId } "
f"from {json.dumps(_MODULE.as_uri())};\n"
+ script
)
@@ -63,6 +66,20 @@ def _run_staleness_harness(script: str) -> Dict[str, Any]:
return json.loads(run.stdout)
def test_probe_message_id_is_guid_shaped_and_unique() -> None:
out = _run_staleness_harness(
"""
const first = createProbeMessageId();
const second = createProbeMessageId();
process.stdout.write(JSON.stringify({ first, second }));
"""
)
guid_re = r"^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
assert re.fullmatch(guid_re, out["first"])
assert re.fullmatch(guid_re, out["second"])
assert out["first"] != out["second"]
def test_probe_rejection_classification_is_strict() -> None:
"""Only not-found-shaped rejections prove liveness; everything else is
inconclusive — a rejected probe is NEVER treated as alive (#45580's