Skip to content

Fix Windows CI after #2: metrics test, single-writer commit path - #3

Closed
ChromaBeast wants to merge 21 commits into
mainfrom
fix/windows-ci-commit-handoff
Closed

ChromaBeast wants to merge 21 commits into
mainfrom
fix/windows-ci-commit-handoff

Conversation

@ChromaBeast

Copy link
Copy Markdown
Owner

What changed

Follow-up to #2, whose Windows CI job failed after merge. Two commits:

  1. test(metrics): require the disk-free gauge only where it is reported. metrics.DiskFreeBytes is unix-only (the gauge is omitted elsewhere, as documented), but TestMetricsAuthAndFormat required beastdb_disk_free_bytes unconditionally, so it failed on Windows CI with missing "beastdb_disk_free_bytes ". The test now checks the gauge only when DiskFreeBytes reports support. This one is a certain fix.
  2. perf(engine): let the first writer lead its own commit group. Instead of waking a dedicated committer goroutine for every write, the writer that finds no commit in progress runs the queued group on its own goroutine; if more writers queued meanwhile, a drain goroutine serves them. Group-commit semantics are unchanged (one fsync per group, engine lock held across it, observer notified in LSN order, one group at a time so queue order is LSN order). Close now waits for the in-flight group and rejects later writes.

Why

TestEngineSystemWorkloadBenchmark also failed on Windows CI in #2: 398 ops/s against its fixed 1000 ops/s floor, with p95 latency 16.5 ms. Earlier Windows runs on main measured 1,351 and 1,923 ops/s with p95 about 3.5 ms. My hypothesis is that waking a separate goroutine for every uncontended write costs up to a timer tick (about 15.6 ms) on Windows, which matches the p95.

Things a reviewer should know

  • The commit-path change is unproven. I cannot run Windows locally, and on macOS it did not change the local numbers. This PR's Windows CI run is the experiment. If Windows throughput does not recover, drop the second commit; it is separate for that reason. The throughput floor in the test is not changed.
  • What I could and could not establish locally: on the original code (d8c1280) the benchmark gets about 1,560 to 1,710 ops/s in a full parallel go test ./... -race run; on the merged branch about 830 to 930 (fix branch included). Run alone, the two are within about 4 to 7% of each other. I counted fsyncs (one per write, same as before) and ruled out the beastbench tests and synthetic fsync load as the cause. The remaining difference under a full parallel run is unexplained.
  • Tests pass with the race detector, including the group-commit crash-recovery, conditional-atomicity and write-after-close tests. The benchmark test still fails in a full parallel run on this laptop, as described above.
  • main has moved since Group commit, gRPC roles, /metrics, beastbench and security policy #2 (UI overhaul); this branch is based on the Group commit, gRPC roles, /metrics, beastbench and security policy #2 merge commit.

🤖 Generated with Claude Code

ChromaBeast and others added 21 commits October 28, 2026 12:20
…eckpoint, harden WAL/replication

- free old tuple on overwrite/delete; compact and reuse pages before growing file
- idempotent per-key WAL replay over already-flushed pages
- halt writes after a post-WAL apply failure until restart
- auto-checkpoint when WAL exceeds -wal-checkpoint-mb (default 64)
- sync data file and WAL dir around checkpoint rotation
- leader replays archived WAL generation for follower catch-up
- constant-time gRPC token compare; 0600 data/WAL files
- README claims, CI macOS

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… plan

Add ABSENT/PRESENT/VALUE_EQUALS preconditions to Put, Delete and BatchWrite,
evaluated atomically under the engine lock (gRPC ABORTED on failure). Add
ScanRequest.limit for paged scans. Only effects are logged to the WAL, so
recovery and replication are unchanged. Document semantics and add the
roadmap plan.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Values were capped at 4,000 bytes, one slotted-page tuple. Larger values (up
to MaxValueSize, 2 MiB) are now stored as a CRC-checked chain of chunk tuples
headed by a manifest; the B+ tree flags the manifest RID with bit 15 of the
slot ID. Small values take the existing path unchanged, existing files need no
migration, and the WAL, recovery and replication still log whole values.
Batches may carry up to 8 MiB of values. See docs/adr/0001-large-values.md.

- Reads verify chunk magic, chain length and CRC (ErrCorruptValue); overwrite
  and delete free a chain only after it validates.
- gRPC: MessageSizeServerOptions / MessageSizeDialOption raise the 4 MiB
  default; the server, followers and beastctl use them. Write-error status
  mapping is shared in grpc_status.go.
- WAL: frames over 32 MiB are refused on write, and a torn header claiming a
  huge length now reads as a torn tail instead of allocating up to 4 GiB.
- Storage: InsertTuple reuses tombstoned slots. Previously a recycled page's
  slot directory grew until it could not host a full tuple, and pages too full
  for one request were dropped from the reuse list; 300 overwrites of a 512 KB
  value grew the file to 2,120 pages, now 266, and 600 small overwrites added
  264 pages, now 3.

Tests: boundary sizes, overwrite/delete space reuse, crash and clean restart,
CAS and batches on large values, scans, corruption detection, a mixed-size
oracle across restarts, gRPC 6 MiB batches, and catch-up plus live replication
of 2 MiB values. Full suite passes with -race.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017mWMfRGkKdRgKc5mtSsWEG
…g in batch limit

- freeValue checked chunk magic and chain termination but not total length
  or CRC, so a mis-linked chain that still ended cleanly after the right
  number of chunks could free tuples owned by another key. It now verifies
  length and CRC first and leaves a failing chain in place.
- MaxBatchBytes counted only value bytes. Each WAL batch operation adds 15
  bytes of framing, so many tiny puts could commit a WAL frame larger than
  the replication message limit, and followers could never catch up on it.
  The limit now covers the encoded frame.

Both reported by Codex review on #1; regression tests reproduce each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017mWMfRGkKdRgKc5mtSsWEG
Values up to 2 MiB, page-slot reuse, and WAL frame cap
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…g guide

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Hand-written text exposition (no new deps). Authenticated by admin session or
bearer token (--api-token / BEASTDB_METRICS_TOKEN); no loopback bypass.
Adds buffer-pool hit/miss counters and checkpoint timestamps, gRPC metrics
interceptors, runbook docs with scrape config and alert rules.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
WAL gains Append (assign LSN, no fsync), WaitDurable/Sync with a shared
syncedLSN so one fsync covers all appended records, and a sticky fsync error.
Write keeps its signature and durability but now group-commits too.

Engine writes (Put/PutIfAbsent/PutIf/Delete/DeleteIf/BatchWrite[If] and
replicated apply) go through a commit queue: a committer goroutine runs a
group of writes under the engine lock (append + apply), issues one fsync,
notifies the observer in LSN order, then releases the lock and acks writers.
Reads and replication never see a write before it is durable. A failed
fsync halts the engine and fails every write in the group.

Concurrent Put benchmark: 7.82 ms/op -> 0.51 ms/op (~15x).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Group commit applies writes to pages before the group's fsync, so the buffer
pool could flush a page ahead of its WAL records. Add a WAL barrier
(WAL.EnsureDurable) called before eviction, FlushPage and FlushAll; a failing
barrier blocks the page write. Adds regression tests and the upgrade spec.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Resolved conflicts in engine.go and buffer_pool.go by keeping both the
group-commit/WAL-barrier fields and the metrics counters.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ench and Studio design

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… the 200-line limit

No behaviour change: functions moved verbatim into wal_sync.go,
wal_lifecycle.go, buffer_pool_wal.go, buffer_pool_stats.go,
runner_load.go and runner_run.go.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…guideline

About 400 lines is a prompt to ask whether a file does more than one thing,
not a gate. Generated code, tests and data are exempt. Updates GEMINI.md
(Antigravity), CONTRIBUTING.md and demo.ps1, and adds AGENTS.md (Codex) and
CLAUDE.md (imports AGENTS.md).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…erformance-3d83f3

Group commit, gRPC roles, /metrics, beastbench and security policy
…s it

DiskFreeBytes is unix-only, so TestMetricsAuthAndFormat failed on Windows CI
with a missing beastdb_disk_free_bytes line. Require the gauge only when
DiskFreeBytes reports support.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Instead of waking a dedicated committer goroutine for every write, the writer
that finds no commit in progress runs the queued group on its own goroutine;
if more writers queued meanwhile, a drain goroutine serves them. Group commit
semantics are unchanged (one fsync per group, lock held across it, observer
in LSN order). Aims to remove a cross-goroutine wake-up per uncontended
write, which can cost up to a timer tick on Windows. Not yet verified on
Windows.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-12T11:59:34.114828Z d466719 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@ChromaBeast

Copy link
Copy Markdown
Owner Author

Superseded by #4: same two commits rebased onto the current main, because main was rewritten after #2 and this branch conflicted.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d4667194d0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if e.wal.FileSize() < maxWALBytes {
continue
}
if err := e.Checkpoint(); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Persist data pages before publishing the checkpoint LSN

When the WAL exceeds the threshold, this new background path invokes Checkpoint, which writes the current LSN to the metadata page before FlushAll has persisted every dirty page. If a crash or page-write error occurs after the metadata page is written but before the remaining pages are durable, recovery skips WAL records up to that LSN and can lose acknowledged writes. Persist and sync the data pages before publishing the checkpoint LSN and rotating the WAL.

Useful? React with 👍 / 👎.

Comment on lines +14 to +15
if op.Type != BatchOpPut {
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Count delete payloads toward the batch limit

When a batch contains a DELETE operation with a non-empty Value, this skips its bytes even though wal.EncodeBatchPayload serializes Value for every operation. The batch can therefore exceed MaxBatchBytes, bypassing the encoded-frame limit and the gRPC message-size headroom it is meant to protect. Reject values on deletes or include their lengths in the total.

Useful? React with 👍 / 👎.

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