Skip to content

LAB-744: fix Review Panel findings (data race, ZK checkpoint leak, restore boot-loop, publish ordering) - #5

Merged
27Bslash6 merged 5 commits into
mainfrom
lab-744-review-fixes
Jul 28, 2026
Merged

LAB-744: fix Review Panel findings (data race, ZK checkpoint leak, restore boot-loop, publish ordering)#5
27Bslash6 merged 5 commits into
mainfrom
lab-744-review-fixes

Conversation

@27Bslash6

@27Bslash6 27Bslash6 commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Follow-up to the merged PR #3, applying the surviving findings from the expert-panel review on this Stage-2 py ingester. Closes LAB-744.

Findings fixed

CRIT — WindowStore data race. consume() mutates the store on the event-loop thread while the publish/checkpoint loops read it via asyncio.to_thread; unsynchronised, iterating _buckets during a concurrent add() raised RuntimeError: dictionary changed size during iteration under load (masked by a try/except into silent cache misses on the metered-miss product). Now guarded by a threading.Lock across every _buckets/_memo access.

MAJ — zero-knowledge checkpoint leak. The unencrypted checkpoint persisted the per-language sentiment totals that the @cache.secure cache encrypts, so the backend could reconstruct the protected value (avg = sum / count). snapshot() no longer writes sent; the secure 1h window repopulates within an hour of a restart and the restart-critical counts are unaffected.

MAJ — restore boot-loop. A malformed or partial checkpoint crashed restore() and, via the one unguarded startup path, propagated through asyncio.run into a permanent boot loop. restore() now validates and skips bad entries, and the startup read is guarded.

MAJ — publish ordering. Publish invalidated the key before recomputing, so a failed recompute left it deleted until the next tick (a miss on the metered-miss path). It now recomputes first and invalidates + writes only on success (publish, secure, and checkpoint paths).

Cleanup. Collapsed the field-identical Merged into Bucket; anchored _prune to the added minute so a single bogus far-future timestamp can no longer permanently zero the window (it now self-heals).

Verification

  • 39 tests green (34 existing + 5 new: one regression test per finding). The concurrency test forces aggressive GIL switching and fails without the lock — verified by defeating the lock.
  • ruff check + ruff format --check clean.
  • README secure/checkpoint sections updated to state the zero-knowledge boundary (documentation gate).

Review gate

The crypto/zero-knowledge gate is satisfied by the expert panel that identified these findings; the ZK fix is a strict reduction of persisted plaintext and cannot introduce a new crypto path. No SDK internals touched.

Ticket

Closes LAB-952 (review-debt closure; panel rounds originated on LAB-744).

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The ingester now rejects far-future events, synchronises sliding-window access, excludes sentiment from checkpoints, tolerates malformed restores, and centralises cache refresh error handling. Startup checkpoint failures are handled safely, with regression tests and README documentation updated.

Changes

Ingester resilience

Layer / File(s) Summary
Timestamp filtering and window synchronisation
ingester/src/skyline_ingester/jetstream.py, ingester/src/skyline_ingester/windows.py, ingester/tests/test_extract.py, ingester/tests/test_windows.py
Future-dated events are discarded, while window operations use locking, memoisation, chunked copying, and regression coverage for pruning and concurrent access.
Checkpoint capture and trusted restoration
ingester/src/skyline_ingester/windows.py, ingester/tests/test_checkpoint.py, ingester/README.md
Snapshots omit sentiment totals; restore validates malformed and poisoned data while retaining valid buckets, with matching zero-knowledge and restart documentation.
Cache refresh and startup recovery
ingester/src/skyline_ingester/publisher.py, ingester/tests/test_publisher.py, ingester/README.md
Publishing probes recomputation before invalidation, checkpoint reads fail safely during startup, and failed recomputation is tested against live-key deletion.

Sequence Diagram(s)

sequenceDiagram
  participant Publisher
  participant WindowStore
  participant CacheKit
  participant Backend
  Publisher->>WindowStore: Probe recomputation
  Publisher->>CacheKit: Invalidate cache wrapper
  Publisher->>CacheKit: Recompute and write
  CacheKit->>Backend: Store refreshed value
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title accurately reflects the main fixes: data race, checkpoint leak, restore handling, and publish ordering.
Description check ✅ Passed The description matches the changeset and summarises the same review-panel fixes and verification.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
ingester/tests/test_windows.py (1)

134-150: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fence the global switch interval so it can't leak.

sys.setswitchinterval(1e-9) is process-global, and the main-thread start.wait() on Line 138 sits outside the finally that restores it — anything raising there leaves every later test running at a 1 ns switch interval. A timeout on the barrier also turns a hang into a failure.

🧯 Move the guard to cover thread start and the barrier
     old_interval = sys.getswitchinterval()
     sys.setswitchinterval(1e-9)
-    t = threading.Thread(target=writer)
-    t.start()
-    start.wait()
     try:
+        t = threading.Thread(target=writer)
+        t.start()
+        start.wait(timeout=10)
         for i in range(2000):
             now = (20_000_400 + i) * 60.0
             store.snapshot(now)
             store.merged("24h", now)
     except Exception as exc:
         errors.append(repr(exc))
     finally:
         t.join(timeout=10)
         sys.setswitchinterval(old_interval)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ingester/tests/test_windows.py` around lines 134 - 150, Move the try/finally
boundary in the test around setting the switch interval, starting the writer
thread, and waiting on start, so any exception or barrier timeout still restores
the original interval. Keep the snapshot/merged loop and error assertion
behavior unchanged, and ensure the writer thread is joined before restoring the
interval.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ingester/src/skyline_ingester/windows.py`:
- Around line 244-275: Update the restore logic around Bucket and
_coerced_counter to reject or clamp negative checkpoint counts, including n and
counter values, so restored totals cannot be reduced. Also enforce an upper
bound on checkpoint minute values relative to the current time, skipping
far-future buckets while preserving valid recent buckets.
- Around line 116-135: Update the memoization flow in merged around _memo writes
and add to maintain a generation counter: increment it whenever add invalidates
_memo, capture the generation before _copy_range, and only store the computed
result if the generation is unchanged. Prevent pre-add results from being
reinserted while preserving existing memo lookup and clearing behavior.

In `@ingester/tests/test_extract.py`:
- Around line 82-86: Update the window-preservation assertion in the test around
ingest_raw so merged("24h", NOW) cannot reuse the cached memoized Bucket;
advance the test time or inspect the underlying _buckets directly, while
retaining the expected healthy count and checkpoint assertions.

---

Nitpick comments:
In `@ingester/tests/test_windows.py`:
- Around line 134-150: Move the try/finally boundary in the test around setting
the switch interval, starting the writer thread, and waiting on start, so any
exception or barrier timeout still restores the original interval. Keep the
snapshot/merged loop and error assertion behavior unchanged, and ensure the
writer thread is joined before restoring the interval.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7127d79f-10fa-4d1c-93a5-22b7afc59ac3

📥 Commits

Reviewing files that changed from the base of the PR and between 91e628b and 67f45a1.

📒 Files selected for processing (8)
  • ingester/README.md
  • ingester/src/skyline_ingester/jetstream.py
  • ingester/src/skyline_ingester/publisher.py
  • ingester/src/skyline_ingester/windows.py
  • ingester/tests/test_checkpoint.py
  • ingester/tests/test_extract.py
  • ingester/tests/test_publisher.py
  • ingester/tests/test_windows.py

Comment thread ingester/src/skyline_ingester/windows.py
Comment thread ingester/src/skyline_ingester/windows.py Outdated
Comment thread ingester/tests/test_extract.py
Applies the surviving Review Panel findings from PR #3.

- CRIT concurrency: WindowStore was mutated by consume() on the event-loop thread while the publish/checkpoint loops read it via asyncio.to_thread, unsynchronised ("RuntimeError: dictionary changed size during iteration" under load). Guard all _buckets/_memo access with a threading.Lock.
- MAJ zero-knowledge: the unencrypted checkpoint persisted the per-language sentiment totals the @cache.secure cache encrypts, letting the backend reconstruct avg = sum/count. Drop sent from snapshot(); the secure 1h window repopulates within an hour of a restart.
- MAJ boot-loop: a malformed/partial checkpoint crashed restore() and, via the one unguarded startup path, propagated through asyncio.run into a permanent boot loop. restore() now skips bad entries; the startup read is guarded.
- MAJ publish ordering: publish invalidated the key before recomputing, so a failed recompute left it deleted (a miss on the metered-miss path). Recompute first, invalidate + write only on success.
- Cleanup: collapse the field-identical Merged into Bucket; anchor _prune retention floor to the added minute so one bogus far-future timestamp can no longer permanently zero the window.

Docs: README secure/checkpoint sections state the zero-knowledge boundary. Tests: one regression test per finding; ruff + 39 tests green.
- NEW-1 CRIT: drop future-dated Jetstream events at the ingest boundary
  (> 300 s past wall-clock). One untrusted time_us could set a _prune
  retention floor that wiped the whole 24h window, poisoned the next
  checkpoint, and — as a resume cursor — skipped every real event after
  reconnect. Dropped frames advance nothing.
- NEW-3 MAJ: merged()/snapshot() no longer hold the store lock across the
  O(window) merge/serialize. Buckets are copied under the lock in 16-bucket
  chunks (dict.update C-copy — Counter(mapping) copies via a per-key Python
  loop) and merged outside it. Copies, not references: hot buckets' Counters
  mutate in place and iterating one mid-merge is the round-1 race class.
  Deterministic every-tick stalls (40-150 ms measured by the panel at
  5 posts/s) drop to ~0.03 ms median; remaining sporadic ~20 ms worst-case
  is CPython GC pauses, unrelated to the lock.
- NEW-2 MAJ: restore() coerces counter keys/values (str/int) inside its
  existing try, so a poisoned-but-valid checkpoint (e.g. a string counter
  value) skips that bucket at restore instead of detonating hours later in
  merged()/most_common() as silent misses.
- MIN: _refresh/README now state the honest ceiling — the probe covers
  recompute failure only; a backend write failure after invalidate still
  briefly deletes the key (no atomic set in the decorator API). Dropped the
  checkpoint's double-snapshot probe (pure in-memory recompute; the probe
  just doubled the work). merged() documents the lock contract.
- merged() memoises its result only when no add() landed since the merge
  started (generation counter): add() clears the memo, and re-inserting a
  pre-add view would serve it stale to every same-second caller.
- restore() clamps what coercion alone let through: negative counts (would
  skew ppm/lang_mix) and future minutes (would park a bucket forever) skip
  the bucket, consistent with the untrusted-checkpoint posture.
- test fix: the 'window not wiped' assertion now forces a fresh merge
  (NOW+1) instead of re-reading the memoised Bucket, which would have
  passed even on a wiped store.
@27Bslash6
27Bslash6 force-pushed the lab-744-review-fixes branch from 61f2b29 to b6a5645 Compare July 27, 2026 10:26
@kodus-27b

This comment has been minimized.

Comment thread ingester/src/skyline_ingester/jetstream.py
Comment thread ingester/src/skyline_ingester/publisher.py
Comment thread ingester/src/skyline_ingester/publisher.py Outdated
Comment thread ingester/src/skyline_ingester/windows.py Outdated
Comment thread ingester/src/skyline_ingester/windows.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ingester/tests/test_windows.py`:
- Around line 158-172: Move the try/finally in the test around t.start(),
start.wait(), and the snapshot/merged loop so
sys.setswitchinterval(old_interval) always executes if any setup or test
operation raises; preserve the existing exception collection and thread join
behavior.
- Around line 170-174: Update the thread cleanup in the test around t.join and
the writer thread creation so the thread is marked daemon and completion is
explicitly asserted after the 10-second join. Fail the test when the thread
remains alive, before checking errors, while preserving the existing error
assertion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b32a4bda-d170-491d-bbad-f9768ec1e128

📥 Commits

Reviewing files that changed from the base of the PR and between 67f45a1 and b6a5645.

📒 Files selected for processing (8)
  • ingester/README.md
  • ingester/src/skyline_ingester/jetstream.py
  • ingester/src/skyline_ingester/publisher.py
  • ingester/src/skyline_ingester/windows.py
  • ingester/tests/test_checkpoint.py
  • ingester/tests/test_extract.py
  • ingester/tests/test_publisher.py
  • ingester/tests/test_windows.py
🚧 Files skipped from review as they are similar to previous changes (5)
  • ingester/tests/test_publisher.py
  • ingester/tests/test_checkpoint.py
  • ingester/src/skyline_ingester/jetstream.py
  • ingester/src/skyline_ingester/publisher.py
  • ingester/src/skyline_ingester/windows.py

Comment thread ingester/tests/test_windows.py
Comment thread ingester/tests/test_windows.py
Winston added 2 commits July 27, 2026 22:56
- test_windows race test: move t.start()/start.wait() inside try so the
  1ns switch interval is always restored; daemon writer + is_alive assert
  so a real deadlock fails the test instead of passing silently (CodeRabbit)
- restore(): log each corrupt checkpoint bucket skipped (truncated repr) —
  checkpoint corruption is now visible, with a caplog regression assert (Kody)
- publisher: structured extra={operation, window} on failure logs (Kody)
- Rejected in-thread: base-image rule misfire on Python imports (no
  Dockerfile in repo); _refresh's broad except is the deliberate per-item
  isolation boundary of the publish loop (documented in its docstring)
Round 3 (bug-hunter / security / craftsman / catchphrase) on the PR head.
Closure scorecard: all four round-2 findings CLOSED (NEW-1 skew guard,
NEW-2 coercion, NEW-3 chunked lock — closure re-measured: add() stall
p99 ~0ms vs 40-150ms before — and the doc ceiling). Two MAJ survivors,
both fixed here:

- ZK integrity [MAJ]: restore() read legacy `sent` from the plaintext
  checkpoint, so a backend operator (the ZK adversary, who can poison the
  checkpoint) could choose the plaintext the next @cache.secure publish
  encrypts — proven with injected avg=999/inf. restore() now ignores
  `sent` entirely; sentiment repopulates from live ingestion only.
  Reverses round-2's 'harmless legacy bridge' on new evidence; also
  closes the NaN/inf-via-float(s) hole bug-hunter reproduced.
- Boot loop reopened [CRIT-class]: {'n': inf} in a poisoned checkpoint
  raised OverflowError (not in restore()'s except tuple) and
  store.restore() ran outside restore_checkpoint's try — reproduced
  startup crash that would loop for the checkpoint's 26h TTL. Both
  guards added; regression tests for each.
- Observability: warn on whole-checkpoint rejection (version/shape) and
  on frames without int time_us — same silent-degradation class round 2
  flagged.
- Honesty/cuts: module docstring no longer overclaims the memo (it is
  suppressed by concurrent adds); _fast_counter_copy deleted — measured
  parity with Counter.copy() (2.18 vs 2.19 us/500-key copy), the
  justifying stdlib claim was false; _copy_range sentinel bounds ->
  defaults.

Rejected (reasons on the PR): probe/wrapper dedup re-litigates round-2's
veto on dual callables; restore cardinality caps are the vetoed broad-
validation class — the try-wrap closes the boot-loop consequence.

44 tests green, ruff clean.
@kodus-27b

kodus-27b Bot commented Jul 27, 2026

Copy link
Copy Markdown

Kody Review Complete

Great news! 🎉
No issues were found that match your current review configurations.

Keep up the excellent work! 🚀

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the @kody start-review command at the root of your PR.

  • Validate Business Logic: Ask Kody to validate your code against business rules by adding a comment with the @kody -v business-logic command.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug
Performance
Security
Business Logic

Access your configuration settings here.

@27Bslash6

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@27Bslash6

Copy link
Copy Markdown
Contributor Author

@kody start-review

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@27Bslash6

Copy link
Copy Markdown
Contributor Author

@27Bslash6 — ready for your signoff / merge.

All automated gates are green on head 77da928: CI qa SUCCESS, CodeRabbit APPROVED (re-approved 13:14Z, after its 10:32Z changes-requested round), Kody APPROVED (13:16Z — this was the one item still pending at the last handoff), 0 of 10 review threads unresolved, MERGEABLE / CLEAN, 0 commits behind main. Expert-panel round 3 (high stakes) ran and its surviving findings are applied, so the mandatory crypto/ZK gate is satisfied.

A formal review-request could not be used here: you authored this PR, so GitHub silently drops a request naming you. This mention, plus the awaiting-signoff label on LAB-952, is the signal. The merge is yours — the autopilot does not merge.

@27Bslash6
27Bslash6 merged commit 2b351e9 into main Jul 28, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant