Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
174aa67
unbounded-storage: remove unused crypto dependencies
jveski Jul 9, 2026
1b31cfa
unbounded-storage: avoid boxing eviction checks
jveski Jul 9, 2026
57fed29
unbounded-storage: remove unused admission sketch
jveski Jul 9, 2026
3c31427
unbounded-storage: simplify write singleflight
jveski Jul 9, 2026
374c49a
unbounded-storage: avoid allocating for btree checksums
jveski Jul 9, 2026
d863eff
unbounded-storage: update btree mirror in place
jveski Jul 9, 2026
0705a99
unbounded-storage: simplify checksum implementation
jveski Jul 9, 2026
3c6c2ef
unbounded-storage: remove unused engine settings
jveski Jul 9, 2026
9deea9d
unbounded-storage: remove unused write depth hint
jveski Jul 9, 2026
0fdfe22
unbounded-storage: fold eviction metadata into SIEVE
jveski Jul 10, 2026
8dade42
unbounded-storage: remove worker-local origin rings
jveski Jul 9, 2026
fd5fbe0
unbounded-storage: remove legacy routing API
jveski Jul 9, 2026
e4aef69
unbounded-storage: enforce shard-local backends
jveski Jul 9, 2026
7f582c4
unbounded-storage: unify disk registry entries
jveski Jul 9, 2026
a44f94b
unbounded-storage: remove redundant peer map
jveski Jul 9, 2026
9f16c96
unbounded-storage: make backend registry shard-local
jveski Jul 9, 2026
df110d3
unbounded-storage: flatten disk channel snapshots
jveski Jul 9, 2026
a482360
unbounded-storage: share routing across caches
jveski Jul 9, 2026
001a71f
unbounded-storage: make fetch limiter shard-local
jveski Jul 9, 2026
9d1531f
unbounded-storage: deduplicate backing registration
jveski Jul 9, 2026
7fa2271
unbounded-storage: remove unused pool group
jveski Jul 9, 2026
aa0d4a2
unbounded-storage: remove unused io facade
jveski Jul 9, 2026
bb8dbe5
unbounded-storage: simplify origin connection pool
jveski Jul 9, 2026
9efdb4c
unbounded-storage: remove multi-registry disk layer
jveski Jul 9, 2026
175cd8f
unbounded-storage: remove legacy p2p request types
jveski Jul 10, 2026
bcb618e
unbounded-storage: remove unused shard context
jveski Jul 9, 2026
c2d739b
unbounded-storage: remove borrowed origin dispatch
jveski Jul 10, 2026
f933a0c
unbounded-storage: release completion capacity before handlers
jveski Jul 9, 2026
7e685e0
unbounded-storage: reuse fabric receive buffers
jveski Jul 10, 2026
f813dc9
unbounded-storage: distribute fabric worker threads
jveski Jul 10, 2026
4f6a9f8
unbounded-storage: make rpc admission panic-safe
jveski Jul 10, 2026
44b0128
unbounded-storage: simplify network ring ownership
jveski Jul 10, 2026
1ebe72d
unbounded-storage: simplify fabric ack delivery
jveski Jul 10, 2026
ac0db6e
unbounded-storage: scope storage ring installation
jveski Jul 10, 2026
5186a10
unbounded-storage: simplify completion state
jveski Jul 10, 2026
9a6840b
unbounded-storage: simplify flag wakers
jveski Jul 10, 2026
04d5e83
unbounded-storage: repost receives before dispatch
jveski Jul 10, 2026
5425450
unbounded-storage: share storage completion state
jveski Jul 10, 2026
8ff4336
unbounded-storage: enforce shard memory budget
jveski Jul 10, 2026
4d944dd
storage-supervisor: cap default serving cores
jveski Jul 10, 2026
726b428
unbounded-storage: publish disks before serving
jveski Jul 10, 2026
7039325
unbounded-storage: bound config apply fan-in
jveski Jul 10, 2026
526335f
unbounded-storage: supervise shard panics
jveski Jul 10, 2026
48c3203
unbounded-storage: preserve resources on reload failure
jveski Jul 10, 2026
6edc7c8
unbounded-storage: share finalized config snapshots
jveski Jul 10, 2026
29f45ae
unbounded-storage: align disk lifecycle simulation
jveski Jul 10, 2026
52b85c9
unbounded-storage: centralize route publication
jveski Jul 10, 2026
7f6e00b
unbounded-storage: surface disk reconcile failures
jveski Jul 10, 2026
513f428
unbounded-storage: surface hard peer failures
jveski Jul 10, 2026
15c6a31
unbounded-storage: separate frontend bind and listen
jveski Jul 10, 2026
d3adbe5
unbounded-storage: prepare frontends before activation
jveski Jul 10, 2026
2e97c28
unbounded-storage: transact shard-local frontend replacement
jveski Jul 10, 2026
eb3e25e
unbounded-storage: guard stale evictions
jveski Jul 10, 2026
8fe1d02
unbounded-storage: retry failed frontend rebuilds
jveski Jul 10, 2026
256c139
unbounded-storage: transact frontend activation
jveski Jul 10, 2026
7b24e75
Merge remote-tracking branch 'origin/storage-stack/01-engine-integrit…
jveski Jul 10, 2026
db8b509
Merge remote-tracking branch 'origin/storage-stack/02-disk-channels' …
jveski Jul 10, 2026
3330a6d
Merge remote-tracking branch 'origin/storage-stack/03-routing' into s…
jveski Jul 10, 2026
2dfadd6
Merge remote-tracking branch 'origin/storage-stack/04-runtime-cleanup…
jveski Jul 10, 2026
4fba02d
Merge remote-tracking branch 'origin/storage-stack/05-shard-local-ori…
jveski Jul 10, 2026
9620f36
Merge remote-tracking branch 'origin/storage-stack/06-fabric-resilien…
jveski Jul 10, 2026
3ee87bf
Merge remote-tracking branch 'origin/storage-stack/07-ring-ownership'…
jveski Jul 10, 2026
525339f
Merge remote-tracking branch 'origin/storage-stack/08-memory-budget' …
jveski Jul 10, 2026
f4100a3
Merge remote-tracking branch 'origin/storage-stack/09-config-supervis…
jveski Jul 10, 2026
5c18a8d
Merge remote-tracking branch 'origin/storage-stack/09-config-supervis…
jveski Jul 10, 2026
d4b177b
Merge remote-tracking branch 'origin/storage-stack/10-frontend-lifecy…
jveski Jul 10, 2026
fca9897
WIP: begin resolving merge conflicts with main
Copilot Jul 16, 2026
e33a626
Resolve merge conflicts with main in unbounded-storage frontend lifec…
Copilot Jul 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions cmd/unbounded-storage/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,10 +178,16 @@ shutdown; teardown drains terminal reports and returns a failing process status
instead of continuing with a missing shard.

Backend and frontend reloads construct replacements before swapping them into
their shard-local registries. A failed replacement therefore leaves the prior
resource, binding, and stripe geometry live and retryable. This is a
per-resource guarantee; route, peer, shard, and disk publication across the
whole process is not yet one transaction.
their shard-local registries. Network frontends first validate and bind a
dormant `SO_REUSEPORT` socket, then enter the listening state only during
activation. A failed preparation therefore leaves the prior resource, binding,
and stripe geometry live and retryable. Active frontend entries record the
binding and stripe geometry they realized, so a failed rebuild is detected and
retried on the next apply. Replacement uses a staged transaction: the complete
frontend change set is prepared before activation and swaps into the live
registry only after every listener activates successfully. This is not yet
a whole-process guarantee; route, peer, shard, and disk publication across
the whole process is not yet one transaction.

### Shutdown

Expand Down Expand Up @@ -220,10 +226,11 @@ per-shard `!Send` object graph:
`LiveShardLocalStore` because a `PageRef` resolves through exactly one
backing's geometry.
6. **Frontends.** A shard hosts a `FrontendRegistry` of any number of
frontends keyed by component name. Each spec binds its listener with `SO_REUSEPORT` and
builds an `HttpDriver`/`S3Driver`; the registry can add and remove frontends
in place on a live config apply (a removed driver's `Drop` closes its
listener fd).
frontends keyed by component name. Each network frontend is prepared as a
bound but non-listening `SO_REUSEPORT` socket, then activated into an
`HttpDriver`/`S3Driver` that owns the listening socket. The registry can add
and remove frontends in place on a live config apply; RAII closes removed
listeners after their pending accept future is dropped.
7. **Tick loop.** Register tick hooks (socket-ring `progress()`, the
control-drain hook that reconciles backends/frontends/routing from applied
configs, and the frontend registry's `progress()`), report `Up`, and run
Expand Down
66 changes: 26 additions & 40 deletions cmd/unbounded-storage/src/frontend/http_serve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
use std::collections::VecDeque;
use std::future::{Future, poll_fn};
use std::net::SocketAddr;
use std::os::fd::RawFd;
use std::os::fd::{AsRawFd, RawFd};
use std::pin::Pin;
use std::rc::Rc;
use std::task::{Context, Poll, Waker};
Expand All @@ -51,9 +51,9 @@ use crate::frontend::range::{
};
use crate::frontend::serve_metrics::{ConnGuard, ReqOutcome};
use crate::http::{
FdGuard, HttpRequest, MAX_HEADER_BYTES, Method, ParseError, RECV_CHUNK, bind_listener,
connection_header_value, request_allows_keep_alive, send_all, serialize_response_head,
split_query,
BoundListener, FdGuard, HttpRequest, ListeningSocket, MAX_HEADER_BYTES, Method, ParseError,
RECV_CHUNK, bind_socket, connection_header_value, request_allows_keep_alive, send_all,
serialize_response_head, split_query,
};
use crate::ring::NetHandle;
use crate::runtime::noop_waker;
Expand Down Expand Up @@ -118,14 +118,16 @@ impl HttpFrontend {
self.max_requests_per_connection
}

/// Create, bind, and listen the per-shard accept socket with
/// `SO_REUSEPORT` (and `SO_REUSEADDR`) so every shard accepts on the
/// same port, returning the listening [`RawFd`]. Linux-only.
///
/// The caller owns the returned fd and is responsible for closing
/// it; [`HttpDriver`] only reads it for `accept`.
pub fn bind_listener(&self) -> Result<RawFd, FrontendError> {
bind_listener(self.addr).map_err(|_| FrontendError::BadBind(self.addr.to_string()))
/// Bind a dormant per-shard `SO_REUSEPORT` socket.
pub fn bind_socket(&self) -> Result<BoundListener, FrontendError> {
bind_socket(self.addr).map_err(|_| FrontendError::BadBind(self.addr.to_string()))
}

/// Bind and activate the per-shard listener.
pub fn bind_listener(&self) -> Result<ListeningSocket, FrontendError> {
self.bind_socket()?
.listen()
.map_err(|_| FrontendError::BadBind(self.addr.to_string()))
}
}

Expand All @@ -143,7 +145,6 @@ impl HttpFrontend {
pub struct HttpDriver<P: BufferPool<Req = StripeReq> + 'static> {
pool: Rc<P>,
handle: NetHandle,
listen_fd: RawFd,
frontend_id: Rc<str>,
backend_id: String,
cache_id: Option<String>,
Expand All @@ -153,12 +154,14 @@ pub struct HttpDriver<P: BufferPool<Req = StripeReq> + 'static> {
bypass: bool,
max_requests_per_connection: usize,
accept_fut: Pin<Box<dyn Future<Output = std::io::Result<RawFd>>>>,
// Declared after the future so the pending accept is cancelled first.
listener: ListeningSocket,
conns: Vec<Pin<Box<dyn Future<Output = ()>>>>,
waker: Waker,
}

impl<P: BufferPool<Req = StripeReq> + 'static> HttpDriver<P> {
/// Build a serving engine over a bound `listen_fd`.
/// Build a serving engine that owns an active listener.
///
/// `stripe_size` and `page_size` come from the shard's pool
/// geometry. `fanout` is this shard's view of the stripe-ownership
Expand All @@ -168,7 +171,7 @@ impl<P: BufferPool<Req = StripeReq> + 'static> HttpDriver<P> {
pub fn new(
pool: Rc<P>,
handle: NetHandle,
listen_fd: RawFd,
listener: ListeningSocket,
frontend_id: Rc<str>,
backend_id: String,
cache_id: Option<String>,
Expand All @@ -178,11 +181,10 @@ impl<P: BufferPool<Req = StripeReq> + 'static> HttpDriver<P> {
bypass: bool,
max_requests_per_connection: usize,
) -> Self {
let accept_fut = Box::pin(handle.accept(listen_fd));
let accept_fut = Box::pin(handle.accept(listener.as_raw_fd()));
Self {
pool,
handle,
listen_fd,
frontend_id,
backend_id,
cache_id,
Expand All @@ -192,6 +194,7 @@ impl<P: BufferPool<Req = StripeReq> + 'static> HttpDriver<P> {
bypass,
max_requests_per_connection,
accept_fut,
listener,
conns: Vec::new(),
waker: noop_waker(),
}
Expand Down Expand Up @@ -231,7 +234,8 @@ impl<P: BufferPool<Req = StripeReq> + 'static> HttpDriver<P> {
unsafe {
libc::close(fd);
}
self.accept_fut = Box::pin(self.handle.accept(self.listen_fd));
self.accept_fut =
Box::pin(self.handle.accept(self.listener.as_raw_fd()));
continue;
}
let serve = serve_connection(
Expand All @@ -251,7 +255,7 @@ impl<P: BufferPool<Req = StripeReq> + 'static> HttpDriver<P> {
}
// Rearm regardless of accept success so a transient
// accept error does not stop the listener.
self.accept_fut = Box::pin(self.handle.accept(self.listen_fd));
self.accept_fut = Box::pin(self.handle.accept(self.listener.as_raw_fd()));
}
Poll::Pending => break,
}
Expand All @@ -271,24 +275,6 @@ impl<P: BufferPool<Req = StripeReq> + 'static> HttpDriver<P> {
}
}

impl<P: BufferPool<Req = StripeReq> + 'static> Drop for HttpDriver<P> {
/// Close the listen fd the driver owns. The driver is the sole owner
/// of this `SO_REUSEPORT` listener (the embedder hands it the fd from
/// `bind_listener` and never touches it again), so dropping the
/// driver (on shard shutdown or a live frontend removal) must close
/// it or the fd leaks. The accept future borrows only the fd number,
/// not ownership, so closing here is sound once the driver is gone.
fn drop(&mut self) {
if self.listen_fd >= 0 {
// SAFETY: `listen_fd` was returned by `bind_listener` and is
// owned exclusively by this driver; it is closed exactly once.
unsafe {
libc::close(self.listen_fd);
}
}
}
}

///
/// Owns everything it needs so the future is `'static` and can live in
/// the driver's future set across ticks. All paths close `conn_fd` via
Expand Down Expand Up @@ -1362,8 +1348,8 @@ mod tests {
}
};
let handle = NetHandle::new(ring);
let listen_fd = match bind_listener("127.0.0.1:0".parse().unwrap()) {
Ok(fd) => fd,
let listener = match bind_socket("127.0.0.1:0".parse().unwrap()).and_then(|b| b.listen()) {
Ok(listener) => listener,
Err(e) => {
eprintln!("driver_idle_progress: bind failed: {e}; skipping");
return;
Expand All @@ -1374,7 +1360,7 @@ mod tests {
err: Error::from("mock pool has no data"),
}),
handle,
listen_fd,
listener,
Rc::from("primary"),
"primary".to_string(),
None,
Expand Down
61 changes: 23 additions & 38 deletions cmd/unbounded-storage/src/frontend/s3_serve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

use std::future::Future;
use std::net::SocketAddr;
use std::os::fd::RawFd;
use std::os::fd::{AsRawFd, RawFd};
use std::pin::Pin;
use std::rc::Rc;
use std::sync::atomic::{AtomicU64, Ordering};
Expand All @@ -40,8 +40,8 @@ use crate::frontend::range::{ByteRange, RangeError, ResolvedRange, full_object,
use crate::frontend::s3_xml::{S3ErrorCode, error_xml};
use crate::frontend::serve_metrics::{ConnGuard, ReqOutcome};
use crate::http::{
FdGuard, HttpRequest, MAX_HEADER_BYTES, Method, ParseError, RECV_CHUNK, bind_listener,
send_all, serialize_response_head, split_query,
BoundListener, FdGuard, HttpRequest, ListeningSocket, MAX_HEADER_BYTES, Method, ParseError,
RECV_CHUNK, bind_socket, send_all, serialize_response_head, split_query,
};
use crate::ring::NetHandle;
use crate::runtime::noop_waker;
Expand Down Expand Up @@ -102,14 +102,16 @@ impl S3Frontend {
self.addr
}

/// Create, bind, and listen the per-shard accept socket with
/// `SO_REUSEPORT` (and `SO_REUSEADDR`) so every shard accepts on the
/// same port, returning the listening [`RawFd`]. Linux-only.
///
/// The caller owns the returned fd and is responsible for closing
/// it; [`S3Driver`] only reads it for `accept`.
pub fn bind_listener(&self) -> Result<RawFd, FrontendError> {
bind_listener(self.addr).map_err(|_| FrontendError::BadBind(self.addr.to_string()))
/// Bind a dormant per-shard `SO_REUSEPORT` socket.
pub fn bind_socket(&self) -> Result<BoundListener, FrontendError> {
bind_socket(self.addr).map_err(|_| FrontendError::BadBind(self.addr.to_string()))
}

/// Bind and activate the per-shard listener.
pub fn bind_listener(&self) -> Result<ListeningSocket, FrontendError> {
self.bind_socket()?
.listen()
.map_err(|_| FrontendError::BadBind(self.addr.to_string()))
}
}

Expand All @@ -125,47 +127,48 @@ impl S3Frontend {
pub struct S3Driver<P: BufferPool<Req = StripeReq> + 'static> {
pool: Rc<P>,
handle: NetHandle,
listen_fd: RawFd,
frontend_id: Rc<str>,
backend_id: String,
cache_id: Option<String>,
stripe_size: u64,
page_size: usize,
bypass: bool,
accept_fut: Pin<Box<dyn Future<Output = std::io::Result<RawFd>>>>,
// Declared after the future so the pending accept is cancelled first.
listener: ListeningSocket,
conns: Vec<Pin<Box<dyn Future<Output = ()>>>>,
waker: Waker,
}

impl<P: BufferPool<Req = StripeReq> + 'static> S3Driver<P> {
/// Build a serving engine over a bound `listen_fd`.
/// Build a serving engine that owns an active listener.
///
/// `stripe_size` and `page_size` come from the shard's pool
/// geometry. When `bypass` is set, the frontend bridges straight to
/// its backend, skipping the disk cache and peer routing.
pub fn new(
pool: Rc<P>,
handle: NetHandle,
listen_fd: RawFd,
listener: ListeningSocket,
frontend_id: Rc<str>,
backend_id: String,
cache_id: Option<String>,
stripe_size: u64,
page_size: usize,
bypass: bool,
) -> Self {
let accept_fut = Box::pin(handle.accept(listen_fd));
let accept_fut = Box::pin(handle.accept(listener.as_raw_fd()));
Self {
pool,
handle,
listen_fd,
frontend_id,
backend_id,
cache_id,
stripe_size,
page_size,
bypass,
accept_fut,
listener,
conns: Vec::new(),
waker: noop_waker(),
}
Expand Down Expand Up @@ -215,7 +218,7 @@ impl<P: BufferPool<Req = StripeReq> + 'static> S3Driver<P> {
}
// Rearm regardless of accept success so a transient
// accept error does not stop the listener.
self.accept_fut = Box::pin(self.handle.accept(self.listen_fd));
self.accept_fut = Box::pin(self.handle.accept(self.listener.as_raw_fd()));
}
Poll::Pending => break,
}
Expand All @@ -235,24 +238,6 @@ impl<P: BufferPool<Req = StripeReq> + 'static> S3Driver<P> {
}
}

impl<P: BufferPool<Req = StripeReq> + 'static> Drop for S3Driver<P> {
/// Close the listen fd the driver owns. The driver is the sole owner
/// of this `SO_REUSEPORT` listener (the embedder hands it the fd from
/// `bind_listener` and never touches it again), so dropping the
/// driver (on shard shutdown or a live frontend removal) must close
/// it or the fd leaks. The accept future borrows only the fd number,
/// not ownership, so closing here is sound once the driver is gone.
fn drop(&mut self) {
if self.listen_fd >= 0 {
// SAFETY: `listen_fd` was returned by `bind_listener` and is
// owned exclusively by this driver; it is closed exactly once.
unsafe {
libc::close(self.listen_fd);
}
}
}
}

/// Serve one accepted connection end-to-end, then close its fd.
///
/// Owns everything it needs so the future is `'static` and can live in
Expand Down Expand Up @@ -864,8 +849,8 @@ mod tests {
}
};
let handle = NetHandle::new(ring);
let listen_fd = match bind_listener("127.0.0.1:0".parse().unwrap()) {
Ok(fd) => fd,
let listener = match bind_socket("127.0.0.1:0".parse().unwrap()).and_then(|b| b.listen()) {
Ok(listener) => listener,
Err(e) => {
eprintln!("driver_idle_progress: bind failed: {e}; skipping");
return;
Expand All @@ -874,7 +859,7 @@ mod tests {
let mut driver = S3Driver::new(
Rc::new(MockPool),
handle,
listen_fd,
listener,
Rc::from("primary"),
"primary".to_string(),
None,
Expand Down
4 changes: 3 additions & 1 deletion cmd/unbounded-storage/src/http/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ pub use connection::{
pub use headers::{Header, ParseError};
pub use request::{HttpRequest, serialize_request};
pub use response::{ResponseHead, serialize_response_head};
#[cfg(target_os = "linux")]
pub use server::{BoundListener, ListeningSocket};

/// Cross-platform, storage-policy-free server plumbing reused by the
/// HTTP serving frontends.
Expand All @@ -45,7 +47,7 @@ pub(crate) use server::{MAX_HEADER_BYTES, RECV_CHUNK, split_query};
/// Linux-only server plumbing that depends on `libc` and the io_uring
/// [`NetHandle`](crate::ring::NetHandle).
#[cfg(target_os = "linux")]
pub(crate) use server::{FdGuard, bind_listener, send_all};
pub(crate) use server::{FdGuard, bind_socket, send_all};

/// Canonical typed `Method`/`StatusCode`, re-exported from the [`http`]
/// crate so the rest of the crate has a single source for them.
Expand Down
Loading
Loading