From b113ab6de604f53a36fddc6e76df1c31a1feb0de Mon Sep 17 00:00:00 2001 From: Forkbert Date: Sun, 20 Sep 2026 15:51:00 -0400 Subject: [PATCH] fix(photon): use GUID for liveness probe message id (cherry picked from commit cdb6ccadb045e599db795823a100ae7b61d377ec) --- plugins/platforms/photon/sidecar/index.mjs | 6 +++--- .../photon/sidecar/stream-staleness.mjs | 6 ++++++ .../photon/test_zombie_stream_watchdog.py | 19 ++++++++++++++++++- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/plugins/platforms/photon/sidecar/index.mjs b/plugins/platforms/photon/sidecar/index.mjs index 1c68c666b0..27109508ea 100644 --- a/plugins/platforms/photon/sidecar/index.mjs +++ b/plugins/platforms/photon/sidecar/index.mjs @@ -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( diff --git a/plugins/platforms/photon/sidecar/stream-staleness.mjs b/plugins/platforms/photon/sidecar/stream-staleness.mjs index a05472c205..751d65e7ea 100644 --- a/plugins/platforms/photon/sidecar/stream-staleness.mjs +++ b/plugins/platforms/photon/sidecar/stream-staleness.mjs @@ -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. diff --git a/tests/plugins/platforms/photon/test_zombie_stream_watchdog.py b/tests/plugins/platforms/photon/test_zombie_stream_watchdog.py index 7a4482b136..ba0a60509e 100644 --- a/tests/plugins/platforms/photon/test_zombie_stream_watchdog.py +++ b/tests/plugins/platforms/photon/test_zombie_stream_watchdog.py @@ -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