Skip to content

feat(options): byte-rate IO limiter for compaction#2306

Open
shaunpatterson wants to merge 1 commit into
dgraph-io:mainfrom
shaunpatterson:feat/compaction-ratelimit
Open

feat(options): byte-rate IO limiter for compaction#2306
shaunpatterson wants to merge 1 commit into
dgraph-io:mainfrom
shaunpatterson:feat/compaction-ratelimit

Conversation

@shaunpatterson

Copy link
Copy Markdown
Contributor

Motivation

Badger's only compaction throttle is a concurrency semaphore (y.Throttle) — there is no byte-rate control. A compaction (or value-log GC) storm can saturate disk IO and starve foreground reads, hurting tail latency.

What changed

New opt-in WithCompactionBytesPerSec(n) (default 0 = unlimited = current behavior). When n > 0, a token-bucket y.RateLimiter paces compaction output: Request(len(key)+len(value)) is charged after each builder.Add in subcompact. L0 flush is deliberately not throttled, so throttling cannot induce a write stall.

Compatibility & correctness

A nil limiter (the default, n == 0) is a no-op with zero overhead — behavior unchanged. The limiter only paces IO; it never gates it, so compaction output/correctness is unaffected (verified). Request loop-drains the bucket so any request larger than the burst is still satisfiable (no deadlock); the refill goroutine is stopped cleanly on Close.

A drain-ratio auto-tuning controller is documented inline as a future v2; this PR ships the fixed-rate MVP.

Testing

go build, go vet, full go test . -count=1 green; y.RateLimiter unit tests and a compaction integration test pass under -race (rate is enforced; large requests don't deadlock; no goroutine leak on close; data intact under throttling).

🤖 Generated with Claude Code

https://claude.ai/code/session_01NtGkC4K2J2XYwcAKwjhHbM

Add y.RateLimiter, a token-bucket byte-rate limiter, and wire it into
compaction via Options.CompactionBytesPerSec (default 0 = unlimited =
unchanged behavior, no limiter constructed). The limiter paces compaction
(and value-log GC rewrite) IO so a compaction/GC storm cannot starve
foreground reads.

- y.RateLimiter: refill goroutine on a z.Closer adds ratePerSec*period
  tokens every 100ms (capped at burst = max(ratePerSec/10, oneBlock)),
  with fractional carry. Request(n) loop-drains the bucket so any n is
  satisfiable regardless of burst (no deadlock for n > burst). nil
  limiter is a no-op (disabled path, zero overhead).
- Hook Request(len(key)+len(value)) after each builder.Add in subcompact.
- L0 flush is NOT throttled (cannot induce a write stall).
- v2 auto-tune (drain-ratio controller) documented in ratelimit.go, not
  implemented.
- TDD: y/ratelimit_test.go (rate, burst-deadlock, close, concurrent) +
  ratelimit_integration_test.go (correctness under throttle, default-off).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NtGkC4K2J2XYwcAKwjhHbM
@shaunpatterson shaunpatterson requested a review from a team as a code owner June 22, 2026 18:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant