Skip to content

feat(server): wire the RFC 0014 data write path live (rfc0013 green .6) - #245

Merged
jensholdgaard merged 6 commits into
mainfrom
rfc0014-pt2-sink-wiring
Jun 17, 2026
Merged

feat(server): wire the RFC 0014 data write path live (rfc0013 green .6)#245
jensholdgaard merged 6 commits into
mainfrom
rfc0014-pt2-sink-wiring

Conversation

@jensholdgaard

@jensholdgaard jensholdgaard commented Jun 17, 2026

Copy link
Copy Markdown
Owner

What

Wires the production data write path live in the server: OTLP → WAL → miner → ParquetRecordSink → object store. This greens RFC0013.6 (WAL stays local) → RFC 0013 is now green (8/8), and lays the recovery groundwork the RFC0014.5 crash test will exercise next.

  • SharedParquetSink (ourios-ingester): a cloneable Arc<Mutex<ParquetRecordSink>> handle. The miner emits through it; the pipeline drives the flush triggers the sink can't observe itself.
  • Server wiring (ourios-server): builds a LocalFileSystem-backed Store at bucket_root, injects the sink into the miner, force-flushes on WAL rotation (RFC0014.3), runs an age-sweep tick (RFC0014.2), and drains on graceful shutdown. S3 selection (RFC 0004) is the RFC 0014 §7 follow-on.

Invariants addressed

WAL-before-ack / no-loss (CLAUDE.md §3.4, hazard #3). The sink buffer is an in-memory accelerator, never the durability of record — records are WAL-fsync'd before they reach it. No-loss across a crash is preserved by one ordering rule at every miner-snapshot cadence point (post-recovery, rotation, shutdown): flush the sink before writing the snapshot. That keeps the miner's snapshot horizon ≤ the sink's flushed horizon, so startup recovery's miner-gated replay re-emits every un-flushed acknowledged record into a fresh sink. Semantics are at-least-once: records flushed just before a crash may be re-flushed on restart; none are lost. (Exactly-once dedup is a separate downstream concern, not in RFC 0014's no-loss scope.)

Multi-tenancy (§3.7). Unchanged — buffers are keyed by PartitionKey, which carries tenant_id; no buffer or flush crosses tenants.

Tests

  • RFC0013.6 — new end-to-end test through the served binary (ourios-server/tests/rfc0013_6_wal_stays_local.rs): ingest over HTTP, SIGTERM, assert only Parquet/manifest objects land under bucket_root and the WAL *.wal segments stay on the disjoint local wal_root; the flushed Parquet rounds back out (separation isn't vacuous). The ourios-parquet stub is redirected here (that crate has no WAL/server to observe).
  • SharedParquetSink unit test (shared buffer across clones; flush via the handle drains it).
  • The existing server integration tests (RFC0003.16 served binary, RFC0008.10 recovery driver) still pass unchanged — the sink writes Parquet during recovery/shutdown but never touches miner state, so the snapshot assertions hold.

RFC0014.5 (crash no-loss) stays #[ignore]d pending its dedicated SIGKILL crash fixture (next PR), which exercises this same recovery path.

Verification

cargo fmt --all --check, cargo clippy -p ourios-ingester -p ourios-parquet -p ourios-server --all-targets --all-features -D warnings, the three crates' test suites, and mdbook build — all green locally.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Automatic age-based flushing of Parquet records during operation
    • Improved graceful shutdown with data flush guarantees before exit
  • Documentation

    • RFC 0013 object storage now marked complete and verified end-to-end with Write-Ahead Log separation

The production data path now flows OTLP → WAL → miner → ParquetRecordSink
→ object store. The miner emits through a SharedParquetSink (a cloneable
Arc<Mutex<ParquetRecordSink>> handle); the pipeline drives the flush
triggers the sink can't observe itself — flush_all on WAL segment
rotation (RFC0014.3) and flush_aged on a batch-window age sweep
(RFC0014.2). The store is a LocalFileSystem-backed Store rooted at
bucket_root; S3 selection (RFC 0004) is the RFC 0014 §7 follow-on.

WAL-durability invariant (CLAUDE.md §3.4, hazard #3). The sink buffer is
an in-memory accelerator, never the durability of record — records are
WAL-fsync'd before they reach it. No-loss across a crash is preserved by
a single ordering rule applied at every miner-snapshot cadence point
(post-recovery, rotation, graceful shutdown): flush the sink *before*
writing the snapshot. That keeps the miner's snapshot horizon at or below
the sink's flushed horizon, so startup recovery's miner-gated replay
re-emits every un-flushed acknowledged record into a fresh sink (the
crash discards only the volatile buffer). At-least-once: records flushed
just before a crash may be re-flushed on restart; no record is lost.
Multi-tenancy (§3.7) is unchanged — buffers are keyed by PartitionKey,
which carries tenant_id.

RFC0013.6 (WAL stays local) is greened end to end through the served
binary in ourios-server/tests/rfc0013_6_wal_stays_local.rs: it ingests
over HTTP, SIGTERMs (the shutdown drain flushes), and asserts only
Parquet/manifest objects land under bucket_root while the WAL *.wal
segments stay on the disjoint local wal_root. The stub in
ourios-parquet/tests is redirected there (that crate has no WAL or server
to observe). All eight RFC 0013 §5 scenarios now pass → RFC 0013 green.

RFC0014.5 (crash no-loss) stays #[ignore]d pending its dedicated crash
fixture (the next PR), which exercises this same recovery path under a
real SIGKILL.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@jensholdgaard, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 37 minutes and 47 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a2532182-88ce-47b8-a174-81a869472cca

📥 Commits

Reviewing files that changed from the base of the PR and between 0885899 and 00e34e4.

📒 Files selected for processing (2)
  • crates/ourios-server/src/receiver.rs
  • crates/ourios-server/tests/rfc0013_6_wal_stays_local.rs
📝 Walkthrough

Walkthrough

Adds a cloneable SharedParquetSink wrapping ParquetRecordSink in Arc<Mutex<_>>, then integrates it into the server receiver lifecycle: a background age-sweep task periodically flushes aged partitions, and a new flush_then_snapshot helper gates WAL-rotation and shutdown snapshot writes on full sink drainage. A new end-to-end integration test verifies the WAL-stays-local invariant, and RFC 0013 is marked green.

Changes

RFC 0013 .6: WAL-local Parquet flush integration

Layer / File(s) Summary
SharedParquetSink: Arc/Mutex wrapper and unit tests
crates/ourios-ingester/src/record_sink.rs
SharedParquetSink wraps ParquetRecordSink in Arc<Mutex<_>> with poison-recovering accessors, flush/observability methods, and a RecordSink impl. Unit tests confirm cloned handles share one buffer and flush_all drains and counts correctly.
ReceiverConfig/ReceiverHandle fields and serve() startup wiring
crates/ourios-server/src/main.rs, crates/ourios-server/src/receiver.rs
Adds bucket_root: PathBuf to ReceiverConfig (wired from main.rs), adds sink and flush_tick fields to ReceiverHandle, and updates serve() to open a local store at bucket_root, construct a SharedParquetSink, and inject it into MinerCluster before WAL recovery.
flush_config, spawn_age_sweep, flush_then_snapshot, and cadence-point integration
crates/ourios-server/src/receiver.rs
Introduces flush tuning constants, a spawn_age_sweep background task, and flush_then_snapshot that gates snapshot writes on full sink drain. Replaces unconditional snapshot calls at post-recovery, WAL rotation hook, and shutdown. Shutdown now aborts the age-sweep task first. Unit tests cover drain+snapshot success and store-write-failure skip.
rfc0013_6_wal_stays_local end-to-end integration test
crates/ourios-server/tests/rfc0013_6_wal_stays_local.rs
New Unix-only test spawns ourios-server with disjoint bucket/WAL roots, POSTs an OTLP batch via raw TCP, sends SIGTERM, and after graceful exit asserts Parquet files exist under the bucket root, .wal segments exist only under the WAL root, and exactly 2 rows were flushed.
RFC 0013 status promotion and parquet crate test cleanup
docs/rfcs/0013-object-storage.md, crates/ourios-parquet/tests/rfc0013_object_store.rs
RFC 0013 status advances from red to green with an expanded progress note. The ignored rfc0013_6_wal_stays_local todo!() stub is removed from ourios-parquet tests and replaced with a comment pointing to the server-level integration test.

Sequence Diagram(s)

sequenceDiagram
  participant Client as OTLP HTTP Client
  participant Receiver as ourios-server receiver
  participant MinerCluster
  participant SharedParquetSink
  participant AgeSweep as age-sweep task
  participant ObjectStore as Object Store bucket_root
  participant WAL as WAL wal_root

  Client->>Receiver: POST /v1/logs (OTLP protobuf)
  Receiver->>WAL: append to WAL segment
  Receiver->>MinerCluster: mine record
  MinerCluster->>SharedParquetSink: emit(MinedRecord)
  AgeSweep->>SharedParquetSink: periodic flush_aged()
  SharedParquetSink->>ObjectStore: write Parquet partitions

  Note over Receiver: SIGTERM received
  Receiver->>AgeSweep: abort + await flush_tick
  Receiver->>SharedParquetSink: flush_all() via flush_then_snapshot
  alt buffered_records == 0
    SharedParquetSink->>ObjectStore: final Parquet flush
    Receiver->>WAL: write_snapshots(offset)
  else flush incomplete
    Note over Receiver: skip snapshot, records remain in WAL for recovery
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • jensholdgaard/ourios#188: Adds/wires the WAL rotation snapshot hook in receiver.rs that this PR adjusts to flush the shared Parquet sink before snapshotting.
  • jensholdgaard/ourios#230: Introduces the RFC 0013 .6 todo!() test stub in ourios-parquet that this PR removes and migrates to ourios-server.
  • jensholdgaard/ourios#243: Introduces ParquetRecordSink in record_sink.rs that this PR wraps with SharedParquetSink and wires into the receiver lifecycle.

Poem

🐇 Hoppity-hop, the WAL stays put,
While Parquet flies to the bucket's root.
A shared sink flushes, a sweep task sweeps,
No records lost while the server sleeps.
Green! says the RFC, the rabbit cheers —
Two rows confirmed, no .wal appears! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: wiring the RFC 0014 data write path live and completing RFC0013 scenario 6 (WAL stays local). It is specific, concise, and reflects the primary objective.
Description check ✅ Passed The description comprehensively covers the PR's scope with a detailed What section, clear invariant explanations, extensive test documentation, and verification steps. All required template sections are present and thoroughly populated.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch rfc0014-pt2-sink-wiring

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Wires the production data write path into ourios-server so mined records are buffered and flushed to Parquet via the RFC 0013 Store, while ensuring WAL segments remain on local disk (greening RFC0013.6 and marking RFC 0013 as green).

Changes:

  • Add a shared, cloneable SharedParquetSink so the miner can emit while the pipeline/server drives flush triggers (rotation, age sweep, shutdown).
  • Wire Store::local(bucket_root) + ParquetRecordSink into the receiver role, with flush-on-rotation, periodic age sweep, and drain-on-shutdown.
  • Add an end-to-end served-binary test for RFC0013.6 and update RFC 0013 documentation/status accordingly.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
docs/rfcs/0013-object-storage.md Marks RFC 0013 as green and updates status note to reflect completed scenarios, including RFC0013.6.
crates/ourios-server/tests/rfc0013_6_wal_stays_local.rs New end-to-end served-binary test asserting WAL stays under wal_root while Parquet/manifest land under bucket_root.
crates/ourios-server/src/receiver.rs Wires Store + SharedParquetSink into receiver startup/recovery/rotation/shutdown and adds periodic age-sweep flushing.
crates/ourios-server/src/main.rs Passes bucket_root into the receiver config for the write path.
crates/ourios-parquet/tests/rfc0013_object_store.rs Removes the RFC0013.6 stub here and documents that it’s now covered end-to-end in ourios-server.
crates/ourios-ingester/src/record_sink.rs Introduces SharedParquetSink (Arc+Mutex wrapper) and adds a unit test for shared buffering + flushing across clones.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/ourios-server/src/receiver.rs
Comment thread crates/ourios-server/src/receiver.rs
… root

Addresses Copilot review on #245:
- `spawn_age_sweep` ran `flush_aged` (Parquet encode + blocking store I/O)
  directly on a runtime worker; move it to `spawn_blocking` so periodic
  sweeps never stall the receiver (esp. against S3).
- `Store::local` errors if `bucket_root` is missing, which regressed the
  server's ability to start on a not-yet-created `OURIOS_BUCKET_ROOT`;
  `create_dir_all` the root before opening the store. The RFC0013.6 test
  now omits its own `create_dir_all`, so it covers this path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Comment thread crates/ourios-server/src/receiver.rs Outdated
Comment thread crates/ourios-server/src/receiver.rs Outdated
Comment thread crates/ourios-server/src/receiver.rs Outdated
Closes a no-loss hole Copilot flagged on #245. `flush_all` retains a
partition whose store write fails (the WAL is the durability of record),
but the cadence points wrote the miner snapshot unconditionally
afterward. With the store unavailable that advances the snapshot horizon
past records that never reached object storage — and recovery suppresses
frames at or below the horizon, so on the next start they are never
re-emitted into a fresh sink. That is data loss precisely when the store
is down.

Fix: a `flush_then_snapshot` helper flushes, then writes the snapshot
only if `buffered_records() == 0`. If the sink didn't drain it skips the
snapshot (logged), so the horizon can't outrun the flushed horizon and
recovery re-mines the un-flushed records. Applied at all three cadence
points (post-recovery, rotation, shutdown). The rotation hook — which
runs on the request path — wraps the blocking flush + snapshot in
`block_in_place` so it doesn't stall a runtime worker.

Tested: a fault-injection unit test replaces the store root with a file
so `put_blocking` fails, then asserts the snapshot is skipped and the
records stay buffered (not lost); plus the drained happy path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.

Comment thread crates/ourios-server/src/receiver.rs
Comment thread crates/ourios-server/src/receiver.rs Outdated
Comment thread crates/ourios-server/src/receiver.rs Outdated
Comment thread crates/ourios-server/src/receiver.rs Outdated
Comment thread crates/ourios-ingester/src/record_sink.rs Outdated
Comment thread crates/ourios-ingester/src/record_sink.rs
…shes

Addresses Copilot review round 3 on #245:
- Age sweep: set the interval's missed-tick behavior to `Delay` so a slow
  sweep (S3) can't trigger catch-up bursts of back-to-back flushes.
- `flush_then_snapshot` doc: the bool is "did the sink drain" — clarified
  that a `write_snapshots` failure still returns `true` (the data reached
  the store; the snapshot is a rebuildable-cache miss), resolving the
  contradiction with the old "snapshot written" wording.
- Wrap the post-recovery and shutdown cadence flushes in `block_in_place`
  too (not just the rotation hook), so their blocking Parquet/store I/O
  doesn't stall a runtime worker.
- `SharedParquetSink` doc: correct the "short critical sections" claim —
  `flush_all`/`flush_aged` hold the mutex across encode + `put_blocking`,
  so they are blocking sections (benign sub-ms on the local backend).
  Moving the encode+put outside the lock is tracked for the S3 backend
  (RFC 0014 §7 / RFC 0013), where PUTs are slow enough to matter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@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 `@crates/ourios-server/tests/rfc0013_6_wal_stays_local.rs`:
- Around line 66-83: The http_post_logs function uses .ok() to silently discard
the result of stream.flush(), which makes debugging harder if the flush fails.
Replace the .ok() call on the stream.flush().await line with .expect() and
provide a descriptive error message like "flush stream" to ensure failures are
explicit and visible during test execution.
- Around line 85-103: The files_under function silently ignores filesystem
errors using the `else { continue; }` pattern and `.flatten()` which skips
errored entries. Instead of continuing silently on errors, make the function
fail fast by using expect() or unwrap() on the read_dir result to panic with a
clear message when directory reading fails, and similarly handle any errors from
entry enumeration rather than silently skipping them with flatten(). This
ensures that filesystem problems in the test environment (setup failures,
permissions, races) are caught immediately with clear error messages rather than
causing confusing failures later.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: adfe3809-37bf-4cad-810f-575686493c52

📥 Commits

Reviewing files that changed from the base of the PR and between 39c51c2 and 0885899.

📒 Files selected for processing (6)
  • crates/ourios-ingester/src/record_sink.rs
  • crates/ourios-parquet/tests/rfc0013_object_store.rs
  • crates/ourios-server/src/main.rs
  • crates/ourios-server/src/receiver.rs
  • crates/ourios-server/tests/rfc0013_6_wal_stays_local.rs
  • docs/rfcs/0013-object-storage.md

Comment thread crates/ourios-server/tests/rfc0013_6_wal_stays_local.rs
Comment thread crates/ourios-server/tests/rfc0013_6_wal_stays_local.rs

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Comment thread crates/ourios-server/src/receiver.rs Outdated
Addresses review round 4 on #245:
- Shutdown awaits the age-sweep task instead of aborting it. Aborting the
  async task would orphan an in-flight `spawn_blocking` flush still
  holding the sink mutex (which the shutdown drain would then wait on
  anyway); the task already observes the `shutdown` watch signal, so
  awaiting lets the in-flight flush finish and the task exit cleanly
  (Copilot).
- RFC0013.6 test: `http_post_logs` `.expect()`s the flush, and
  `files_under` fails fast on `read_dir`/entry errors rather than
  silently skipping — in a controlled temp tree those are real problems
  (CodeRabbit).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Comment thread crates/ourios-server/src/receiver.rs
Round 5 (Copilot): a failed snapshot write doesn't force a *full* replay
— prior snapshots may survive and partial per-tenant writes may have
landed. Reword the cadence log from "next start full-replays" to "next
start may replay more from the WAL" so it doesn't mislead operators.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

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.

2 participants