Skip to content

Commit 3313534

Browse files
paddymulclaude
andauthored
fix(stats): surface summary-stat cache write side via telemetry (#951) (#952)
The xorq snapshot cache did its I/O with no operator-visible signal. The firstpull.summary_stats span (#943) carried only cache_status/hits/misses/ secs, and _log_cache_stats emitted the write side (snapshots/bytes/ write_errors) at log.info to a server log file no deployment reads — so a cache that stopped writing, or a run with write_errors > 0 (#910), was invisible without counting .parquet files by hand. - xorq_buckaroo: add cache_snapshots/cache_bytes/cache_write_errors to the firstpull.summary_stats span attrs (already available from cache_run_stats()), so a write_errors > 0 run reaches a telemetry consumer. - xorq_stat_pipeline: attach the per-run cache outcome to the stat.xorq.total telemetry span so every process_table run — not just the firstpull — routes its cache outcome through a bound sink, independent of log level. The log.info summary line stays for local perf/debug sessions. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 1cbf982 commit 3313534

4 files changed

Lines changed: 64 additions & 11 deletions

File tree

buckaroo/pluggable_analysis_framework/xorq_stat_pipeline.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -225,11 +225,27 @@ def _write_snapshot(self, path, result_df):
225225
self._cache_stats["write_errors"] += 1
226226
log.warning("xorq stat snapshot write failed for %s: %s", path, e)
227227

228-
def _log_cache_stats(self):
229-
"""One summary line per run when a snapshot cache is in play (#910)."""
228+
def _log_cache_stats(self, span=None):
229+
"""Surface the per-run snapshot-cache outcome (#910, #951).
230+
231+
Two channels, so the write side is never invisible:
232+
233+
* ``span`` — attach the hit/miss/snapshot/byte/write-error counts to the
234+
run's ``stat.xorq.total`` telemetry span. A bound telemetry sink then
235+
carries the cache outcome (including ``write_errors``) to the operator
236+
without a debug log build — the ``log.info`` line below lands in a
237+
server log file no deployment reads (#951).
238+
* ``log`` — one summary line per run for a local perf/debug session."""
230239
if self.cache_storage is None:
231240
return
232241
s = self._cache_stats
242+
if span is not None:
243+
cs = self.cache_run_stats()
244+
span.set_attr(
245+
cache_status=cs["status"], cache_hits=cs["hits"],
246+
cache_misses=cs["misses"], cache_secs=cs["secs"],
247+
cache_snapshots=cs["snapshots"], cache_bytes=cs["bytes"],
248+
cache_write_errors=cs["write_errors"])
233249
base_path = getattr(getattr(self.cache_storage, "storage", None), "base_path", "?")
234250
log.info(
235251
"xorq stat cache [%s]: %d hit(s), %d miss(es), %d snapshot(s) "
@@ -318,12 +334,12 @@ def process_table(self, table, skip_columns=None) -> Tuple[SDType, List[StatErro
318334
self._perf = (perf_log.PerfRecorder()
319335
if perf_log.enabled() and not self._suppress_perf_summary else None)
320336
_t0 = time.perf_counter()
321-
with self._span("stat.xorq.total"):
337+
with self._span("stat.xorq.total") as span:
322338
try:
323339
return self._process_table_impl(table, skip_columns=skip_columns)
324340
finally:
325341
self._cache_stats["secs"] = round(time.perf_counter() - _t0, 4)
326-
self._log_cache_stats()
342+
self._log_cache_stats(span)
327343
if self._perf is not None:
328344
self._perf.label = (
329345
f"xorq cols={len(table.columns)} "

buckaroo/xorq_buckaroo.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -173,16 +173,22 @@ def _get_summary_sd(self, processed_df):
173173
processed_df, self.analysis_klasses, self.df_name,
174174
debug=self.debug, cache_storage=cache_storage,
175175
skip_columns=getattr(self, 'skip_stat_columns', None))
176-
# Attach the summary-stat cache hit/miss/timing signal (#944) to the
177-
# span so a telemetry consumer learns whether the stats were cached
178-
# — the one signal only the server observes (#943). The pandas branch
179-
# above already returned, so stats here is always an XorqDfStatsV2,
180-
# which always exposes cache_run_stats(); call it unconditionally so
181-
# a missing signal fails loudly rather than silently vanishing.
176+
# Attach the summary-stat cache signal (#944) to the span so a
177+
# telemetry consumer learns whether the stats were cached — the one
178+
# signal only the server observes (#943). Carries the write side too
179+
# (snapshots/bytes/write_errors, #951) so a cache that stops writing —
180+
# or a run with write_errors > 0 — is visible to a telemetry consumer
181+
# rather than only to a server log no deployment reads. The pandas
182+
# branch above already returned, so stats here is always an
183+
# XorqDfStatsV2, which always exposes cache_run_stats(); call it
184+
# unconditionally so a missing signal fails loudly rather than
185+
# silently vanishing.
182186
cs = stats.cache_run_stats()
183187
span.set_attr(
184188
cache_status=cs.get("status"), cache_hits=cs.get("hits"),
185-
cache_misses=cs.get("misses"), cache_secs=cs.get("secs"))
189+
cache_misses=cs.get("misses"), cache_secs=cs.get("secs"),
190+
cache_snapshots=cs.get("snapshots"), cache_bytes=cs.get("bytes"),
191+
cache_write_errors=cs.get("write_errors"))
186192
sdf = stats.sdf
187193
if stats.errs:
188194
if self.debug:

tests/unit/server/test_load_expr.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,12 @@ async def test_load_expr_telemetry_emits_session_correlated_spans(self):
445445
self.assertEqual(attrs["cache_status"], "miss")
446446
self.assertEqual(attrs["cache_hits"], 0)
447447
self.assertGreater(attrs["cache_misses"], 0)
448+
# The write side rides the span too (#951): the cold miss writes
449+
# snapshots with no write errors, so a cache that stops writing — or a
450+
# run with write_errors > 0 — is visible to the telemetry consumer.
451+
self.assertGreater(attrs["cache_snapshots"], 0)
452+
self.assertGreater(attrs["cache_bytes"], 0)
453+
self.assertEqual(attrs["cache_write_errors"], 0)
448454
finally:
449455
shutil.rmtree(builds_root, ignore_errors=True)
450456
shutil.rmtree(cache_root, ignore_errors=True)

tests/unit/test_xorq_stats_v2.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -831,6 +831,31 @@ def test_internal_spans_emit_to_telemetry_sink_with_perf_off(self):
831831
f"stat.xorq.total span must emit to the sink with perf logging off; "
832832
f"got {names}")
833833

834+
def test_cache_outcome_rides_total_span_to_telemetry(self, tmp_path):
835+
"""#951: the per-run snapshot-cache outcome — status, snapshots, bytes,
836+
write_errors — rides the stat.xorq.total telemetry span, so a telemetry
837+
consumer sees the write side without reading a server log.
838+
839+
Before the fix _log_cache_stats emitted only a log.info line the server
840+
never surfaced, so a cache that stopped writing was invisible."""
841+
records: list = []
842+
saved = perf_log._ENABLED
843+
perf_log._ENABLED = False
844+
try:
845+
pipeline = XorqStatPipeline(
846+
XORQ_STATS_V2, unit_test=False, cache_storage=self._cache(tmp_path))
847+
with perf_log.telemetry_context("sess-cache-write", records.append):
848+
pipeline.process_table(self._filter_chain_table())
849+
finally:
850+
perf_log._ENABLED = saved
851+
total = next(r for r in records if r["name"] == "stat.xorq.total")
852+
attrs = total["attrs"]
853+
# Cold run against a fresh cache dir → a pure miss that writes snapshots.
854+
assert attrs["cache_status"] == "miss"
855+
assert attrs["cache_snapshots"] > 0
856+
assert attrs["cache_bytes"] > 0
857+
assert attrs["cache_write_errors"] == 0
858+
834859
def test_cached_stats_match_uncached(self, tmp_path):
835860
"""The cold (compute + write) and warm (snapshot-read) cache paths
836861
produce the same stats as a plain uncached run — the cache is a perf

0 commit comments

Comments
 (0)