@@ -90,37 +90,26 @@ def _set_last_fetch_time(when: float) -> None:
9090
9191
9292def 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
101100def _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
120111def _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
143131def 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 (
0 commit comments