Skip to content

examples/flight: add OomGuard allocator-level OOM circuit breaker to sql_server - #22834

Closed
avantgardnerio wants to merge 8 commits into
apache:mainfrom
coralogix:brent/oom-guard
Closed

avantgardnerio wants to merge 8 commits into
apache:mainfrom
coralogix:brent/oom-guard

Conversation

@avantgardnerio

@avantgardnerio avantgardnerio commented Jun 9, 2026 •

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Not closing an issue directly. This is a follow-up to the
MemoryPool accounting discussion (#22723, #22758) — demonstrates an alternative approach.
Instead of catching MemoryPool divergences from allocations, this uses the flight_sql_server example
to demonstrate how downstream applications can install a custom allocator to abort queries rather
than OOMing when MemoryTracker under-reports.

Rationale for this change

MemoryPool is voluntary. Operators that don't call try_grow —
in-flight batches, arrow-row decode buffers, Arrow kernel scratch
space, allocator slab overhead — allocate freely past the declared
budget. On a process with a hard memory ceiling (k8s pod cgroup,
ulimit), the result is an OS-level kill that takes down every
concurrent query, not just the offender.

This PR adds OomGuard, a GlobalAlloc wrapper. Implementation details:

  1. Every Rust allocation is debited from a global BALANCE atomic.
  2. A dedicated OS thread polls jemalloc's stats.resident every 10ms
    and syncs BALANCE = threshold - resident, catching slab pages
    that the kernel sees but GlobalAlloc doesn't.
  3. When BALANCE goes negative on a stamped query worker, the
    wrapper returns NULL from alloc.
  4. An alloc_error_hook originates panic_any(OomGuardPanic) from
    a safe-Rust frame outside the allocator.
  5. The panic unwinds through df.collect(), gets caught in the
    example, and is converted to Status::resource_exhausted. The
    query dies; the server survives.

A/B demo on the same 1GiB cgroup:

  • OOM_GUARD=off: client sees broken pipe, server SIGKILLed.
  • OOM_GUARD on (default): client sees ResourceExhausted,
    follow-up SELECT 1 against the same server returns immediately.

What changes are included in this PR?

Everything is scoped to datafusion-examples/examples/flight/:

  • oom_guard.rs (new) — the GlobalAlloc wrapper, per-thread
    drift settling, jemalloc-stats-based poll,
    cgroup-v2/v1 memory-limit discovery.
  • sql_server.rs — wraps the global allocator with
    OomGuard<Jemalloc>, arms via OOM_GUARD env var with
    configurable headroom (OOM_GUARD_HEADROOM, default 5%), wraps
    df.collect() in catch_unwind to convert OomGuardPanic to
    a Status::resource_exhausted response.
  • main.rs — installs the alloc_error_hook, installs a global
    panic hook that logs OomGuardPanic distinctly
  • .cargo/config.toml (new) — sets RUSTC_BOOTSTRAP=1 so
    #![feature(alloc_error_hook)] (tracking Tracking issue for the OOM hook (alloc_error_hook) rust-lang/rust#51245)
    builds on stable.
  • README.md (new) — A/B walkthrough.

Are these changes tested?

Manually via the A/B in the README. No automated tests
added: the example's behavior depends on a cgroup limit being in
effect, which doesn't fit the existing test harness.

Are there any user-facing changes?

Only to the flight example. No core / library API surface changed.

Caveats

  • alloc_error_hook is currently unstable. The workspace
    .cargo/config.toml sets RUSTC_BOOTSTRAP=1 to compile it on a
    stable toolchain.
  • The poll currently reads jemalloc-specific stats. Making it
    allocator-agnostic by reading cgroup's memory.current instead is
    a straightforward follow-up if there's interest.

avantgardnerio and others added 3 commits June 8, 2026 14:55
Wraps the global allocator with `OomGuard`, a `GlobalAlloc` wrapper that
converts a process-wide OOM into a per-query panic *before* the kernel
SIGKILLs the process. Demonstrates the pattern against the existing
flight sql_server example so a Houzz-shape groupby that previously
OOM-killed the server now surfaces as a per-query `ResourceExhausted`.

- `oom_guard.rs`: thread-stamped balance bank with amortized atomic
  settle. Resident probe via `/proc/self/statm` (no allocator-specific
  dependency). Cgroup v2 / v1 limit discovery.
- `sql_server.rs`: wraps MiMalloc with OomGuard, spawns the balance
  poll, and `catch_unwind`s overdraft panics around `df.collect()`.
- `main.rs`: manual tokio runtime with `on_thread_start` to stamp
  worker threads; installs `alloc_error_hook` as the safe seam to
  originate `panic_any` from outside the allocator.
- `.cargo/config.toml`: `RUSTC_BOOTSTRAP=1` for unstable
  `alloc_error_hook` (tracking rust-lang/rust#51245).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@alamb alamb 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.

Thanks @avantgardnerio -- is there anything DataFusion specific about this example? It looks like it doesn't use a memory pool at all. I thought your solution hooked the allocator into the memory pool infrastructure -- this seems like it is even deeper and hooks the allocator itself

If that is the case, perhaps it would be better as a standalone crate?

//! - `sql_server`
//! (file: sql_server.rs, desc: Standalone SQL server for JDBC clients)
//!
//! ## OomGuard

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.

Rather than extending an existing example, what would you think about making this its own example ? That might keep the basic arrow-flight example slimpler

@avantgardnerio

avantgardnerio commented Jun 9, 2026 •

Copy link
Copy Markdown
Contributor Author

it doesn't use a memory pool at all

It does not. And at this point I have an honest question: what is the MemoryPool for?

We've established it doesn't prevent OOMs, and that it's not easy to keep accurate.

So cost=high, benefit=low - Is there a reason for tracking in-flight batches I'm not seeing?

@avantgardnerio

Copy link
Copy Markdown
Contributor Author

would be better as a standalone crate?

acknowledged

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