Skip to content

feat: return clear error on storage-node disk full instead of hanging#3497

Open
halfprice wants to merge 2 commits into
mainfrom
zhewu/reject_writes_when_disk_full
Open

feat: return clear error on storage-node disk full instead of hanging#3497
halfprice wants to merge 2 commits into
mainfrom
zhewu/reject_writes_when_disk_full

Conversation

@halfprice

@halfprice halfprice commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Problem

The storage node's PUT sliver and PUT metadata endpoints awaited their RocksDB writes via spawn_blocking with no timeout. Combined with:

  • write options never setting no_slowdown, and
  • the metadata column family deliberately disabling the write stall (database_config.rs),

a full disk can cause RocksDB to block indefinitely, hanging the request forever. There was no disk-space monitoring, and when RocksDB did return an error it collapsed into an opaque HTTP 500 Internal with no signal that the node was out of space.

Change

Bound the metadata and sliver write chokepoints with a configurable timeout, and check free disk space so a full node answers with a clear error:

  • Fast-fail up-front: before dispatching a write, check available disk space (a ~1µs statvfs call). If it is below the configured threshold, reject immediately with HTTP 429 RESOURCE_EXHAUSTED ("the storage node is out of disk space"). This matters because a timed-out spawn_blocking write cannot be cancelled — without the up-front check, a full node would stall every request for the full timeout and leak one blocked thread per request until the blocking pool (512 threads) is exhausted, hanging reads too.
  • Timeout + classify: a dispatched write that fails or times out is re-checked against free disk space: below the threshold → 429 out-of-space; otherwise a timeout returns HTTP 504 DEADLINE_EXCEEDED instead of blocking, and a genuine DB error still maps to HTTP 500 as before.

Details

  • New OutOfSpace (429) and StorageWriteTimeout (504) errors, added as variants on StoreMetadataError/StoreSliverError, plus a StorageWriteFailure classifier; cross-error mappings preserve the disk-full/timeout semantics.
  • Storage::available_disk_space via libc::statvfs (libc already a workspace dep; non-Unix falls back to "never full").
  • StorageWriteConfig { write_timeout (default 60s), min_available_disk_space (default 1 GiB) }, configurable with serde defaults.
  • classify_storage_write (free function, unit-testable) applies pre-check + timeout + classification; StorageNodeInner::run_storage_write wraps it at the three request-path chokepoints: store_sliver_unchecked, persist_verified_metadata, flush_pending_metadata.
  • Regenerated node_config_example.yaml and storage_openapi.yaml golden files (new config section; 429/504 documented on the PUT endpoints).

Testing

  • cargo clippy --all-features --all-targets clean (0 warnings); formatted.
  • Unit tests: up-front rejection (paused clock, never waits out the timeout), timeout classification of a stalled write, success/DB-error passthrough, reclassification when the disk fills while a write is in flight, error-to-status mapping (429/504/500), and available_disk_space.

Notes / follow-ups

  • No background disk poller: the pre-check is inline at the write chokepoints, so coverage is limited to metadata/sliver writes. Event processing and other background writes are unaffected by design.
  • No end-to-end/simtest that induces a real RocksDB stall (hard to do deterministically); covered by unit tests instead.

The PUT sliver and PUT metadata endpoints awaited their RocksDB writes via
`spawn_blocking` with no timeout. Because the write options never set
`no_slowdown` and the metadata column family disables the write stall, a full
disk can cause RocksDB to block indefinitely, hanging the request forever. When
RocksDB did surface an error it collapsed into an opaque HTTP 500 with no signal
that the node was out of space.

Wrap the metadata and sliver write chokepoints in a configurable timeout and
classify failures: when a write fails or times out and free disk space is below
a configurable threshold, return a dedicated out-of-space error (HTTP 429
RESOURCE_EXHAUSTED) so clients get a clear signal that the node is full;
otherwise return a write-timeout error (HTTP 504) instead of blocking.

- add `OutOfSpace` and `StorageWriteTimeout` errors plus variants on
  `StoreMetadataError`/`StoreSliverError`, and a `StorageWriteFailure` classifier
- add `Storage::available_disk_space` (statvfs via libc)
- add `StorageWriteConfig` (write_timeout, min_available_disk_space)
- wrap writes in `StorageNodeInner::run_storage_write` with timeout + disk-full
  classification at the request-path chokepoints
A timed-out `spawn_blocking` write cannot be cancelled: it occupies a
blocking-pool thread until RocksDB returns. Under sustained disk-full, every
request would stall for the full write timeout before returning 429, and each
one would leak a blocked thread until the pool (512 threads) is exhausted —
at which point all `spawn_blocking` work, including reads, queues indefinitely.

Check available disk space before dispatching the write (a ~1µs statvfs call,
negligible next to a RocksDB insert) and reject immediately with the
out-of-space error, so a full node answers with an instant 429 instead of a
60-second stall and a leaked thread per request.

Also extract the timeout-and-classify logic into a free
`classify_storage_write` function so its behavior is unit-testable without a
full storage node, and add tests covering the up-front rejection, timeout
classification, error passthrough, and reclassification of failures that race
with the disk filling up.

Regenerate `node_config_example.yaml` and `storage_openapi.yaml` (golden-file
tests) for the new `storage_write` config section and the 429/504 responses
now documented on the PUT metadata/sliver endpoints.
@halfprice halfprice force-pushed the zhewu/reject_writes_when_disk_full branch from 282b8e6 to 9f832a5 Compare July 13, 2026 05:36
@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Warning: This PR modifies one of the OpenAPI files. Please consider the
following:

  • Make sure the API changes are backwards compatible.
  • Make sure to follow existing conventions for any added parameters, requests, and
    responses.
  • Make sure the generated HTML files do not contain errors.
  • This probably requires release notes and maybe changes to the documentation.

@halfprice

Copy link
Copy Markdown
Collaborator Author

@codex review

@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: 9f832a5db1

ℹ️ 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".

Comment on lines +3681 to +3683
self.run_storage_write(self.storage.put_verified_metadata(&metadata))
.await
.context("unable to persist pending metadata")
.map_err(StoreMetadataError::Internal)?;
.map_err(StoreMetadataError::from)?;

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 Requeue pending metadata when write is refused

When a blob was uploaded with pending=true and later becomes registered while the node is under the new disk-space threshold (or the write times out), this call returns OutOfSpace/WriteTimeout after the metadata was already removed by the surrounding pending_metadata_cache.remove(...). Unlike the sliver flush path, the metadata is not inserted back into the pending cache before returning, so a transient full-disk/timeout condition can permanently drop the buffered metadata and leave the pending upload unable to flush after space is freed.

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.

1 participant