2026-08-17 16:31:15 +00:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""Versioned HTTP and maintenance loop for the execution-pool coordinator."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
|
import os
|
|
|
|
|
import sqlite3
|
|
|
|
|
import sys
|
|
|
|
|
import threading
|
|
|
|
|
import time
|
hermes: make pool lease recovery and release isolation safe
Independent review t_5975c06a blocked this branch on a P1: a Kanban write that
failed while a lease expired left a `lease_failed` row that was invisible to
every pass, immortal to garbage collection, and fatal to the coordinator. It
poisoned `reconcile()` forever with a conflicting-duplicate primary key,
produced a spurious capability `block_task` from `dispatch()`, and -- because
startup maintenance ran unguarded before the port bound, against a store on a
PVC -- crash-looped the coordinator with no automatic recovery.
`lease_failed` is now a retryable state that every maintenance pass drains, and
a row only reaches a terminal state on authoritative evidence about its exact
Kanban run, so nothing is collected before its outcome is known and nothing is
silently dropped. Each row, task, and board is processed in isolation, and a
coordinator-side fault is never converted into a Kanban mutation. Startup runs
through the same guarded cycle as the steady-state loop.
The wire protocol and the durable store are now separate modules, and the
maintenance passes moved out of the coordinator, so each file stays under the
managed line ceiling with room for the recovery logic.
Also closes three consequential handoff risks the same review raised:
* mediator-N pinned itself hard to worker-N while sharing a ReadWriteOnce
claim, so a drain or preemption that moved only the lower-priority worker
deadlocked the ordinal on Multi-Attach until an operator deleted a Pod. The
shared workspace is now ReadWriteMany (as the hermes-chat tenant workspaces
already are on the same class), colocation is a preference, and the mediator
shares the worker's preemption priority, so each Pod reschedules on its own.
* the broker permits only branch creation, so a retry that added commits could
never submit and the run's work was discarded with the failure. Submission
now targets a fresh attempt- or content-scoped ref in the same reviewed
namespace -- never an update -- and is idempotent under replay. A refused
submission downgrades the result and says why instead of unwinding the run.
* the provider CLIs were reinstalled into an emptyDir on every Pod start inside
the 10m Flux health window for the whole hermes app. They now install once
per pinned version onto a durable volume, re-verified against the real
binaries and time-bounded, and the best-effort pool no longer gates the
health of the app its dependents wait on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 16:31:15 +00:00
|
|
|
from collections.abc import Iterable
|
2026-08-17 16:31:15 +00:00
|
|
|
from http.server import BaseHTTPRequestHandler
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
|
|
from execution_pool_protocol import (
|
|
|
|
|
MAX_WIRE_BYTES,
|
|
|
|
|
PROTOCOL_VERSION,
|
|
|
|
|
BoundedHTTPServer,
|
|
|
|
|
ProtocolError,
|
|
|
|
|
canonical_json,
|
|
|
|
|
parse_wire,
|
|
|
|
|
read_key,
|
|
|
|
|
)
|
hermes: make pool lease recovery and release isolation safe
Independent review t_5975c06a blocked this branch on a P1: a Kanban write that
failed while a lease expired left a `lease_failed` row that was invisible to
every pass, immortal to garbage collection, and fatal to the coordinator. It
poisoned `reconcile()` forever with a conflicting-duplicate primary key,
produced a spurious capability `block_task` from `dispatch()`, and -- because
startup maintenance ran unguarded before the port bound, against a store on a
PVC -- crash-looped the coordinator with no automatic recovery.
`lease_failed` is now a retryable state that every maintenance pass drains, and
a row only reaches a terminal state on authoritative evidence about its exact
Kanban run, so nothing is collected before its outcome is known and nothing is
silently dropped. Each row, task, and board is processed in isolation, and a
coordinator-side fault is never converted into a Kanban mutation. Startup runs
through the same guarded cycle as the steady-state loop.
The wire protocol and the durable store are now separate modules, and the
maintenance passes moved out of the coordinator, so each file stays under the
managed line ceiling with room for the recovery logic.
Also closes three consequential handoff risks the same review raised:
* mediator-N pinned itself hard to worker-N while sharing a ReadWriteOnce
claim, so a drain or preemption that moved only the lower-priority worker
deadlocked the ordinal on Multi-Attach until an operator deleted a Pod. The
shared workspace is now ReadWriteMany (as the hermes-chat tenant workspaces
already are on the same class), colocation is a preference, and the mediator
shares the worker's preemption priority, so each Pod reschedules on its own.
* the broker permits only branch creation, so a retry that added commits could
never submit and the run's work was discarded with the failure. Submission
now targets a fresh attempt- or content-scoped ref in the same reviewed
namespace -- never an update -- and is idempotent under replay. A refused
submission downgrades the result and says why instead of unwinding the run.
* the provider CLIs were reinstalled into an emptyDir on every Pod start inside
the 10m Flux health window for the whole hermes app. They now install once
per pinned version onto a durable volume, re-verified against the real
binaries and time-bounded, and the best-effort pool no longer gates the
health of the app its dependents wait on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 16:31:15 +00:00
|
|
|
from execution_pool_store import PoolStore
|
2026-08-17 16:31:15 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
DATA_ROOT = Path(os.environ.get("HERMES_HOME", "/opt/data"))
|
|
|
|
|
STATE_ROOT = DATA_ROOT / "execution-pool"
|
|
|
|
|
KEY_PATH = Path(
|
|
|
|
|
os.environ.get(
|
|
|
|
|
"HERMES_EXECUTION_POOL_KEY_FILE", "/runtime-access/execution-pool-key"
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
PORT = int(os.environ.get("HERMES_EXECUTION_POOL_PORT", "9007"))
|
|
|
|
|
RETENTION_SECONDS = int(
|
|
|
|
|
os.environ.get("HERMES_EXECUTION_POOL_RETENTION_SECONDS", "1209600")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def handler_factory(coordinator: Any) -> type[BaseHTTPRequestHandler]:
|
|
|
|
|
class Handler(BaseHTTPRequestHandler):
|
|
|
|
|
server_version = f"hermes-execution-pool/{PROTOCOL_VERSION}"
|
|
|
|
|
|
|
|
|
|
def _reply(self, status: int, value: dict[str, Any]) -> None:
|
|
|
|
|
body = canonical_json(value)
|
|
|
|
|
self.send_response(status)
|
|
|
|
|
self.send_header("Content-Type", "application/json")
|
|
|
|
|
self.send_header("Content-Length", str(len(body)))
|
|
|
|
|
self.send_header("Cache-Control", "no-store")
|
|
|
|
|
self.end_headers()
|
|
|
|
|
self.wfile.write(body)
|
|
|
|
|
|
|
|
|
|
def do_GET(self) -> None: # noqa: N802
|
|
|
|
|
if self.path != "/ready":
|
|
|
|
|
self._reply(404, {"error": "not found"})
|
|
|
|
|
return
|
|
|
|
|
try:
|
|
|
|
|
coordinator.store.available_ordinals()
|
|
|
|
|
self._reply(200, {"ready": True, "version": PROTOCOL_VERSION})
|
|
|
|
|
except (OSError, sqlite3.Error):
|
|
|
|
|
self._reply(503, {"ready": False, "version": PROTOCOL_VERSION})
|
|
|
|
|
|
|
|
|
|
def do_POST(self) -> None: # noqa: N802
|
|
|
|
|
try:
|
|
|
|
|
length = int(self.headers.get("Content-Length", "0"))
|
|
|
|
|
if length <= 0 or length > MAX_WIRE_BYTES:
|
|
|
|
|
raise ProtocolError("invalid content length")
|
|
|
|
|
envelope = parse_wire(self.rfile.read(length))
|
|
|
|
|
routes = {
|
|
|
|
|
"/v1/poll": coordinator.poll,
|
|
|
|
|
"/v1/heartbeat": coordinator.heartbeat,
|
|
|
|
|
"/v1/result": coordinator.result,
|
|
|
|
|
}
|
|
|
|
|
if self.path not in routes:
|
|
|
|
|
self._reply(404, {"error": "not found"})
|
|
|
|
|
return
|
|
|
|
|
self._reply(200, routes[self.path](envelope))
|
|
|
|
|
except ProtocolError as error:
|
|
|
|
|
self._reply(409, {"error": str(error)})
|
|
|
|
|
except Exception as error:
|
|
|
|
|
print(
|
|
|
|
|
f"pool request failed: {type(error).__name__}: {error}",
|
|
|
|
|
file=sys.stderr,
|
|
|
|
|
flush=True,
|
|
|
|
|
)
|
|
|
|
|
self._reply(503, {"error": "coordinator unavailable"})
|
|
|
|
|
|
|
|
|
|
def log_message(self, _format: str, *_arguments: Any) -> None:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
return Handler
|
|
|
|
|
|
|
|
|
|
|
hermes: make pool lease recovery and release isolation safe
Independent review t_5975c06a blocked this branch on a P1: a Kanban write that
failed while a lease expired left a `lease_failed` row that was invisible to
every pass, immortal to garbage collection, and fatal to the coordinator. It
poisoned `reconcile()` forever with a conflicting-duplicate primary key,
produced a spurious capability `block_task` from `dispatch()`, and -- because
startup maintenance ran unguarded before the port bound, against a store on a
PVC -- crash-looped the coordinator with no automatic recovery.
`lease_failed` is now a retryable state that every maintenance pass drains, and
a row only reaches a terminal state on authoritative evidence about its exact
Kanban run, so nothing is collected before its outcome is known and nothing is
silently dropped. Each row, task, and board is processed in isolation, and a
coordinator-side fault is never converted into a Kanban mutation. Startup runs
through the same guarded cycle as the steady-state loop.
The wire protocol and the durable store are now separate modules, and the
maintenance passes moved out of the coordinator, so each file stays under the
managed line ceiling with room for the recovery logic.
Also closes three consequential handoff risks the same review raised:
* mediator-N pinned itself hard to worker-N while sharing a ReadWriteOnce
claim, so a drain or preemption that moved only the lower-priority worker
deadlocked the ordinal on Multi-Attach until an operator deleted a Pod. The
shared workspace is now ReadWriteMany (as the hermes-chat tenant workspaces
already are on the same class), colocation is a preference, and the mediator
shares the worker's preemption priority, so each Pod reschedules on its own.
* the broker permits only branch creation, so a retry that added commits could
never submit and the run's work was discarded with the failure. Submission
now targets a fresh attempt- or content-scoped ref in the same reviewed
namespace -- never an update -- and is idempotent under replay. A refused
submission downgrades the result and says why instead of unwinding the run.
* the provider CLIs were reinstalled into an emptyDir on every Pod start inside
the 10m Flux health window for the whole hermes app. They now install once
per pinned version onto a durable volume, re-verified against the real
binaries and time-bounded, and the best-effort pool no longer gates the
health of the app its dependents wait on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 16:31:15 +00:00
|
|
|
def maintenance_cycle(operations: Iterable[Any]) -> int:
|
|
|
|
|
"""Run one maintenance pass; a failing operation never aborts the others.
|
|
|
|
|
|
|
|
|
|
Startup uses this too. A durable row that one pass cannot process must not
|
|
|
|
|
stop the coordinator from binding its port, because the store lives on a PVC
|
|
|
|
|
and would otherwise turn a single bad row into a permanent crash loop.
|
|
|
|
|
"""
|
|
|
|
|
deferred = 0
|
|
|
|
|
for operation in operations:
|
|
|
|
|
try:
|
|
|
|
|
operation()
|
|
|
|
|
except Exception as error: # noqa: BLE001 - the loop is the recovery mechanism
|
|
|
|
|
deferred += 1
|
|
|
|
|
print(
|
|
|
|
|
f"pool maintenance deferred: {type(error).__name__}: {error}",
|
|
|
|
|
file=sys.stderr,
|
|
|
|
|
flush=True,
|
|
|
|
|
)
|
|
|
|
|
return deferred
|
|
|
|
|
|
|
|
|
|
|
2026-08-17 16:31:15 +00:00
|
|
|
def run(coordinator_type: type[Any]) -> int:
|
|
|
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
|
|
|
parser.add_argument("--once", action="store_true")
|
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
store = PoolStore(STATE_ROOT / "assignments.db")
|
|
|
|
|
coordinator = coordinator_type(read_key(KEY_PATH), store)
|
hermes: make pool lease recovery and release isolation safe
Independent review t_5975c06a blocked this branch on a P1: a Kanban write that
failed while a lease expired left a `lease_failed` row that was invisible to
every pass, immortal to garbage collection, and fatal to the coordinator. It
poisoned `reconcile()` forever with a conflicting-duplicate primary key,
produced a spurious capability `block_task` from `dispatch()`, and -- because
startup maintenance ran unguarded before the port bound, against a store on a
PVC -- crash-looped the coordinator with no automatic recovery.
`lease_failed` is now a retryable state that every maintenance pass drains, and
a row only reaches a terminal state on authoritative evidence about its exact
Kanban run, so nothing is collected before its outcome is known and nothing is
silently dropped. Each row, task, and board is processed in isolation, and a
coordinator-side fault is never converted into a Kanban mutation. Startup runs
through the same guarded cycle as the steady-state loop.
The wire protocol and the durable store are now separate modules, and the
maintenance passes moved out of the coordinator, so each file stays under the
managed line ceiling with room for the recovery logic.
Also closes three consequential handoff risks the same review raised:
* mediator-N pinned itself hard to worker-N while sharing a ReadWriteOnce
claim, so a drain or preemption that moved only the lower-priority worker
deadlocked the ordinal on Multi-Attach until an operator deleted a Pod. The
shared workspace is now ReadWriteMany (as the hermes-chat tenant workspaces
already are on the same class), colocation is a preference, and the mediator
shares the worker's preemption priority, so each Pod reschedules on its own.
* the broker permits only branch creation, so a retry that added commits could
never submit and the run's work was discarded with the failure. Submission
now targets a fresh attempt- or content-scoped ref in the same reviewed
namespace -- never an update -- and is idempotent under replay. A refused
submission downgrades the result and says why instead of unwinding the run.
* the provider CLIs were reinstalled into an emptyDir on every Pod start inside
the 10m Flux health window for the whole hermes app. They now install once
per pinned version onto a durable volume, re-verified against the real
binaries and time-bounded, and the best-effort pool no longer gates the
health of the app its dependents wait on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 16:31:15 +00:00
|
|
|
operations = (
|
2026-08-17 16:31:15 +00:00
|
|
|
coordinator.expire_leases,
|
|
|
|
|
coordinator.recover_results,
|
|
|
|
|
coordinator.reconcile,
|
|
|
|
|
coordinator.dispatch,
|
hermes: make pool lease recovery and release isolation safe
Independent review t_5975c06a blocked this branch on a P1: a Kanban write that
failed while a lease expired left a `lease_failed` row that was invisible to
every pass, immortal to garbage collection, and fatal to the coordinator. It
poisoned `reconcile()` forever with a conflicting-duplicate primary key,
produced a spurious capability `block_task` from `dispatch()`, and -- because
startup maintenance ran unguarded before the port bound, against a store on a
PVC -- crash-looped the coordinator with no automatic recovery.
`lease_failed` is now a retryable state that every maintenance pass drains, and
a row only reaches a terminal state on authoritative evidence about its exact
Kanban run, so nothing is collected before its outcome is known and nothing is
silently dropped. Each row, task, and board is processed in isolation, and a
coordinator-side fault is never converted into a Kanban mutation. Startup runs
through the same guarded cycle as the steady-state loop.
The wire protocol and the durable store are now separate modules, and the
maintenance passes moved out of the coordinator, so each file stays under the
managed line ceiling with room for the recovery logic.
Also closes three consequential handoff risks the same review raised:
* mediator-N pinned itself hard to worker-N while sharing a ReadWriteOnce
claim, so a drain or preemption that moved only the lower-priority worker
deadlocked the ordinal on Multi-Attach until an operator deleted a Pod. The
shared workspace is now ReadWriteMany (as the hermes-chat tenant workspaces
already are on the same class), colocation is a preference, and the mediator
shares the worker's preemption priority, so each Pod reschedules on its own.
* the broker permits only branch creation, so a retry that added commits could
never submit and the run's work was discarded with the failure. Submission
now targets a fresh attempt- or content-scoped ref in the same reviewed
namespace -- never an update -- and is idempotent under replay. A refused
submission downgrades the result and says why instead of unwinding the run.
* the provider CLIs were reinstalled into an emptyDir on every Pod start inside
the 10m Flux health window for the whole hermes app. They now install once
per pinned version onto a durable volume, re-verified against the real
binaries and time-bounded, and the best-effort pool no longer gates the
health of the app its dependents wait on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 16:31:15 +00:00
|
|
|
lambda: store.garbage_collect(RETENTION_SECONDS),
|
|
|
|
|
)
|
|
|
|
|
maintenance_cycle(operations)
|
2026-08-17 16:31:15 +00:00
|
|
|
if args.once:
|
|
|
|
|
return 0
|
|
|
|
|
server = BoundedHTTPServer(
|
|
|
|
|
("0.0.0.0", PORT), handler_factory(coordinator), max_workers=8
|
|
|
|
|
)
|
|
|
|
|
threading.Thread(target=server.serve_forever, daemon=True).start()
|
|
|
|
|
while True:
|
hermes: make pool lease recovery and release isolation safe
Independent review t_5975c06a blocked this branch on a P1: a Kanban write that
failed while a lease expired left a `lease_failed` row that was invisible to
every pass, immortal to garbage collection, and fatal to the coordinator. It
poisoned `reconcile()` forever with a conflicting-duplicate primary key,
produced a spurious capability `block_task` from `dispatch()`, and -- because
startup maintenance ran unguarded before the port bound, against a store on a
PVC -- crash-looped the coordinator with no automatic recovery.
`lease_failed` is now a retryable state that every maintenance pass drains, and
a row only reaches a terminal state on authoritative evidence about its exact
Kanban run, so nothing is collected before its outcome is known and nothing is
silently dropped. Each row, task, and board is processed in isolation, and a
coordinator-side fault is never converted into a Kanban mutation. Startup runs
through the same guarded cycle as the steady-state loop.
The wire protocol and the durable store are now separate modules, and the
maintenance passes moved out of the coordinator, so each file stays under the
managed line ceiling with room for the recovery logic.
Also closes three consequential handoff risks the same review raised:
* mediator-N pinned itself hard to worker-N while sharing a ReadWriteOnce
claim, so a drain or preemption that moved only the lower-priority worker
deadlocked the ordinal on Multi-Attach until an operator deleted a Pod. The
shared workspace is now ReadWriteMany (as the hermes-chat tenant workspaces
already are on the same class), colocation is a preference, and the mediator
shares the worker's preemption priority, so each Pod reschedules on its own.
* the broker permits only branch creation, so a retry that added commits could
never submit and the run's work was discarded with the failure. Submission
now targets a fresh attempt- or content-scoped ref in the same reviewed
namespace -- never an update -- and is idempotent under replay. A refused
submission downgrades the result and says why instead of unwinding the run.
* the provider CLIs were reinstalled into an emptyDir on every Pod start inside
the 10m Flux health window for the whole hermes app. They now install once
per pinned version onto a durable volume, re-verified against the real
binaries and time-bounded, and the best-effort pool no longer gates the
health of the app its dependents wait on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 16:31:15 +00:00
|
|
|
maintenance_cycle(operations)
|
2026-08-17 16:31:15 +00:00
|
|
|
time.sleep(5)
|