Skip to content

Commit e3306f5

Browse files
aapelivclaude
andcommitted
Backend/experimentation: trim comments and simplify staleness gauge
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 33f24b3 commit e3306f5

2 files changed

Lines changed: 9 additions & 24 deletions

File tree

app/backend/src/couchers/experimentation.py

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -90,37 +90,26 @@ def _set_last_fetch_time(when: float) -> None:
9090

9191

9292
def seconds_since_last_fetch() -> float | None:
93-
"""Seconds since features were last successfully pulled from GrowthBook, or None if never pulled
94-
(e.g. experimentation disabled). Drives the staleness metric so a stalled refresh is observable."""
93+
"""Seconds since the last successful pull, or None if never pulled. Drives the staleness metric."""
9594
when = _last_fetch_time
9695
if when is None:
9796
return None
9897
return max(0.0, time.time() - when)
9998

10099

101100
def _write_cache(response: dict[str, Any]) -> None:
102-
"""Persist a freshly fetched payload to disk for use as a cold-start fallback.
103-
104-
Written to a temp file in the same directory and then renamed (Path.replace is atomic), so a
105-
concurrent reader never sees a partial file and concurrent writers from the api/worker/scheduler
106-
processes resolve to a last-writer-wins of identical content. Failures are not swallowed: a disk
107-
problem here is real and must surface.
108-
"""
109101
path = Path(config["GROWTHBOOK_CACHE_PATH"])
110102
data = json.dumps({"fetched_at": time.time(), "response": response})
111-
# Write to a temp file in the same directory, then rename over the target: rename is atomic only
112-
# within a filesystem, so the temp must live next to the target, and a reader either sees the whole
113-
# old file or the whole new one - never a half-written cache that would fail our strict parse.
103+
# Temp file alongside the target then rename: rename is atomic within a filesystem, so a reader
104+
# never sees a half-written cache.
114105
with NamedTemporaryFile("w", dir=path.parent, prefix=".growthbook-cache-", suffix=".tmp", delete=False) as f:
115106
f.write(data)
116107
tmp = Path(f.name)
117108
tmp.replace(path)
118109

119110

120111
def _read_cache() -> tuple[dict[str, Any], float] | None:
121-
"""Load the persisted payload as (response, fetched_at). Returns None only when no cache file
122-
exists yet (e.g. the very first deploy). A file that exists but can't be parsed raises - a corrupt
123-
cache is a hard failure, not something to paper over by falling through to in-code defaults."""
112+
"""(response, fetched_at), or None if no cache file exists yet. A corrupt file raises."""
124113
path = Path(config["GROWTHBOOK_CACHE_PATH"])
125114
if not path.exists():
126115
return None
@@ -136,8 +125,7 @@ def _refresh_loop() -> None:
136125
_write_cache(response)
137126
_set_last_fetch_time(time.time())
138127
logger.debug("GrowthBook features refreshed")
139-
# On a failed fetch, keep last-known-good state and try again next tick. seconds_since_last_fetch
140-
# climbs until a fetch succeeds, so the degradation surfaces via the staleness metric.
128+
# On a failed fetch, keep last-known-good state and retry next tick; the staleness metric climbs.
141129

142130

143131
def setup_experimentation() -> None:
@@ -168,8 +156,7 @@ def setup_experimentation() -> None:
168156
_set_last_fetch_time(time.time())
169157
logger.info("GrowthBook features loaded from API")
170158
else:
171-
# GrowthBook is unreachable at startup. Fall back to the last-known-good snapshot from disk
172-
# rather than coming up healthy on in-code defaults. With no usable cache either, fail loudly.
159+
# Unreachable at startup: fall back to the disk cache rather than booting on in-code defaults.
173160
cached = _read_cache()
174161
if cached is None:
175162
raise GrowthBookUnavailableError(

app/backend/src/couchers/metrics.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -634,12 +634,10 @@ def observe_moderation_queue_resolution_time(
634634
)
635635

636636

637-
# Seconds since feature flags were last successfully pulled from GrowthBook. Recomputed at scrape time
638-
# via the hacky-gauge mechanism so it reflects live age, not the value at the last refresh. 0 when
639-
# experimentation is disabled or flags have never been pulled.
637+
# Recomputed at scrape time via the hacky-gauge mechanism, so it reflects live age. 0 when disabled
638+
# or never pulled.
640639
def _feature_flags_staleness_seconds() -> float:
641-
age = experimentation.seconds_since_last_fetch()
642-
return age if age is not None else 0.0
640+
return experimentation.seconds_since_last_fetch() or 0.0
643641

644642

645643
feature_flags_staleness_gauge: Gauge = Gauge(

0 commit comments

Comments
 (0)