#![forbid(unsafe_code)]
use std::collections::{HashMap, VecDeque};
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::{Arc, Condvar, Mutex, Weak};
use std::time::Instant;
static CAP: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
fn cap() -> usize {
*CAP.get_or_init(|| {
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(4)
.max(1)
})
}
struct BudgetState {
cap: usize,
in_flight: usize,
waiters: usize,
max_waiters: usize,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct WorkerOracleSnapshot {
pub requested: u64,
pub granted: u64,
pub blocked: u64,
pub batches: u64,
pub max_queue_depth: u64,
}
pub struct WorkerBudget {
state: Mutex<BudgetState>,
cv: Condvar,
requested: AtomicUsize,
granted: AtomicUsize,
blocked: AtomicUsize,
batches: AtomicUsize,
}
impl WorkerBudget {
const fn new() -> Self {
Self {
state: Mutex::new(BudgetState {
cap: 0, in_flight: 0,
waiters: 0,
max_waiters: 0,
}),
cv: Condvar::new(),
requested: AtomicUsize::new(0),
granted: AtomicUsize::new(0),
blocked: AtomicUsize::new(0),
batches: AtomicUsize::new(0),
}
}
fn acquire(&self, want: usize) -> WorkerGrant {
let want = want.min(cap()).max(1);
let mut st = self.state.lock().expect("worker budget poisoned");
if st.cap == 0 {
st.cap = cap();
}
self.requested
.fetch_add(want, std::sync::atomic::Ordering::Relaxed);
if st.in_flight + want > st.cap {
st.waiters += 1;
st.max_waiters = st.max_waiters.max(st.waiters);
self.blocked
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
while st.in_flight + want > st.cap {
st = self.cv.wait(st).expect("worker budget poisoned");
}
st.waiters -= 1;
}
st.in_flight += want;
self.granted
.fetch_add(want, std::sync::atomic::Ordering::Relaxed);
self.batches
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
drop(st);
WorkerGrant { n: want }
}
fn release(&self, n: usize) {
if n == 0 {
return;
}
let mut st = self.state.lock().expect("worker budget poisoned");
st.in_flight -= n;
drop(st);
self.cv.notify_all();
}
pub fn snapshot(&self) -> WorkerOracleSnapshot {
let st = self.state.lock().expect("worker budget poisoned");
WorkerOracleSnapshot {
requested: self.requested.load(std::sync::atomic::Ordering::Relaxed) as u64,
granted: self.granted.load(std::sync::atomic::Ordering::Relaxed) as u64,
blocked: self.blocked.load(std::sync::atomic::Ordering::Relaxed) as u64,
batches: self.batches.load(std::sync::atomic::Ordering::Relaxed) as u64,
max_queue_depth: st.max_waiters as u64,
}
}
}
pub static WORKERS: WorkerBudget = WorkerBudget::new();
pub struct WorkerClock {
cpu: Option<u64>,
wall: std::time::Instant,
}
impl WorkerClock {
pub fn start() -> Self {
Self {
cpu: thread_cpu_ns(),
wall: std::time::Instant::now(),
}
}
pub fn elapsed_ns(&self) -> u64 {
match self.cpu {
Some(c0) => thread_cpu_ns().unwrap_or(c0).saturating_sub(c0),
None => self.wall.elapsed().as_nanos() as u64,
}
}
}
fn thread_cpu_ns() -> Option<u64> {
#[cfg(not(target_os = "linux"))]
{
let _ = rustix::time::ClockId::ThreadCPUTime;
None
}
#[cfg(target_os = "linux")]
{
let t = rustix::time::clock_gettime(rustix::time::ClockId::ThreadCPUTime);
Some(t.tv_sec as u64 * 1_000_000_000 + t.tv_nsec as u64)
}
}
#[must_use]
pub struct WorkerGrant {
n: usize,
}
impl WorkerGrant {
pub fn n(&self) -> usize {
self.n
}
}
impl Drop for WorkerGrant {
fn drop(&mut self) {
WORKERS.release(self.n);
}
}
pub fn grant(want: usize) -> WorkerGrant {
WORKERS.acquire(want)
}
#[allow(clippy::large_enum_variant)]
pub(crate) enum WorkerTask {
EncodeChunk {
ordinal: usize,
store: Arc<crate::store::Store>,
ino: u64,
composed: super::Composed,
limits: crate::core::limits::Limits,
options: crate::optimizer::policy::OptimizeOptions,
fg: crate::optimizer::foreground::ForegroundPolicy,
},
DecodeExtent {
ordinal: usize,
store: Arc<crate::store::Store>,
start: u64,
desc: crate::core::representation::Representation,
objects: Arc<HashMap<crate::core::extent::ChunkId, Vec<u8>>>,
descriptors: Arc<HashMap<crate::core::extent::ChunkId, Vec<u8>>>,
limits: crate::core::limits::Limits,
},
}
impl WorkerTask {
fn execute(self) -> (usize, u64, Result<WorkerOutcome, crate::store::StoreError>) {
match self {
WorkerTask::EncodeChunk {
ordinal,
store,
ino,
composed,
limits,
options,
fg,
} => {
let t0 = WorkerClock::start();
let r = super::encode_prepared_chunk(&store, &composed, ino, limits, options, fg);
store.perf().record("worker_tasks", 0);
let cpu = t0.elapsed_ns();
(ordinal, cpu, Ok(WorkerOutcome::Encode(r)))
}
WorkerTask::DecodeExtent {
ordinal,
store,
start,
desc,
objects,
descriptors,
limits,
} => {
let t0 = WorkerClock::start();
let ctx = crate::store::epoch::PrefetchContext::new(
&store,
&objects,
Some(&descriptors),
None,
);
let mut chunk = vec![0u8; desc.len() as usize];
let mut budget = limits.max_decode_work;
let r = crate::core::materialize::materialize(
&desc,
&ctx,
&limits,
0,
&mut budget,
&mut chunk,
)
.map_err(|e| crate::store::StoreError::Descriptor(e.to_string()))
.map(|()| WorkerOutcome::Decode((start, chunk)));
store.perf().record("worker_tasks", 0);
let cpu = t0.elapsed_ns();
(ordinal, cpu, r)
}
}
}
}
#[derive(Debug, Clone)]
pub struct WorkerResult {
pub request_id: u64,
pub ordinal: usize,
pub result: Result<WorkerOutcome, crate::store::StoreError>,
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone)]
pub enum WorkerOutcome {
Encode(super::ChunkResult),
Decode((u64, Vec<u8>)),
}
#[derive(Debug, Clone, Copy)]
pub struct RequestMetrics {
pub queue_wait_ns: u64,
pub span_ns: u64,
pub cpu_ns: u64,
pub tasks: usize,
}
#[must_use = "the request's results are the reassembly; join must be called"]
pub struct PoolSubmit {
state: Arc<RequestState>,
}
impl PoolSubmit {
pub fn join(self) -> (Vec<WorkerResult>, RequestMetrics) {
let state = &self.state;
let mut g = state.results.lock().expect("pool request results poisoned");
while state.done.load(Ordering::Acquire) < state.total {
g = state.cv.wait(g).expect("pool request results poisoned");
}
let out: Vec<WorkerResult> = g
.iter()
.map(|s| s.as_ref().expect("completed slot must be present").clone())
.collect();
let finished = *state
.finished_at
.lock()
.expect("pool request finished_at poisoned");
let first = *state
.first_service
.lock()
.expect("pool request first_service poisoned");
let metrics = RequestMetrics {
queue_wait_ns: first
.map(|t| t.duration_since(state.submitted_at).as_nanos() as u64)
.unwrap_or(0),
span_ns: finished
.map(|t| t.duration_since(state.submitted_at).as_nanos() as u64)
.unwrap_or(0),
cpu_ns: state.cpu_ns.load(Ordering::Relaxed),
tasks: state.total,
};
drop(g);
(out, metrics)
}
}
#[derive(Debug, Clone, Copy)]
pub struct PoolDiagnostics {
pub peak_in_flight: usize,
pub capacity: usize,
pub workers: usize,
pub max_consecutive_same_request: usize,
}
struct RequestState {
id: u64,
total: usize,
results: Mutex<Vec<Option<WorkerResult>>>,
done: AtomicUsize,
cv: Condvar,
submitted_at: Instant,
first_service: Mutex<Option<Instant>>,
finished_at: Mutex<Option<Instant>>,
cpu_ns: AtomicU64,
}
impl RequestState {
fn new(id: u64, total: usize) -> Self {
Self {
id,
total,
results: Mutex::new((0..total).map(|_| None).collect()),
done: AtomicUsize::new(0),
cv: Condvar::new(),
submitted_at: Instant::now(),
first_service: Mutex::new(None),
finished_at: Mutex::new(None),
cpu_ns: AtomicU64::new(0),
}
}
fn mark_first_service(&self) {
let mut f = self
.first_service
.lock()
.expect("pool request first_service poisoned");
if f.is_none() {
*f = Some(Instant::now());
}
}
fn deliver(
&self,
ordinal: usize,
result: Result<WorkerOutcome, crate::store::StoreError>,
cpu_ns: u64,
) {
let mut g = self.results.lock().expect("pool request results poisoned");
let slot = g
.get_mut(ordinal)
.expect("ordinal within the request's result slots");
debug_assert!(slot.is_none(), "task executed twice: ordinal {ordinal}");
*slot = Some(WorkerResult {
request_id: self.id,
ordinal,
result,
});
self.cpu_ns.fetch_add(cpu_ns, Ordering::Relaxed);
if self.done.fetch_add(1, Ordering::AcqRel) + 1 == self.total {
*self
.finished_at
.lock()
.expect("pool request finished_at poisoned") = Some(Instant::now());
self.cv.notify_all();
}
drop(g);
}
}
struct PoolQueues {
active: Vec<u64>,
requests: HashMap<u64, RequestEntry>,
}
impl PoolQueues {
fn new() -> Self {
Self {
active: Vec::new(),
requests: HashMap::new(),
}
}
}
struct RequestEntry {
pending: VecDeque<WorkerTask>,
state: Arc<RequestState>,
}
struct PoolShared {
store: Mutex<Option<Weak<crate::store::Store>>>,
queues: Mutex<PoolQueues>,
in_flight: AtomicUsize,
peak_in_flight: AtomicUsize,
capacity: usize,
shutdown: std::sync::atomic::AtomicBool,
wake: Condvar,
backpressure: Mutex<()>,
max_consecutive: AtomicUsize,
next_request_id: AtomicU64,
}
impl PoolShared {
fn new(workers: usize, queue_factor: usize) -> Self {
Self {
store: Mutex::new(None),
queues: Mutex::new(PoolQueues::new()),
in_flight: AtomicUsize::new(0),
peak_in_flight: AtomicUsize::new(0),
capacity: workers.saturating_mul(queue_factor).max(1),
shutdown: std::sync::atomic::AtomicBool::new(false),
wake: Condvar::new(),
backpressure: Mutex::new(()),
max_consecutive: AtomicUsize::new(0),
next_request_id: AtomicU64::new(1),
}
}
fn submit(&self, request_id: u64, tasks: Vec<WorkerTask>) -> PoolSubmit {
let total = tasks.len();
assert!(total > 0, "pool submit with no tasks");
let mut b = self
.backpressure
.lock()
.expect("pool backpressure poisoned");
while self.in_flight.load(Ordering::Acquire) != 0
&& self.in_flight.load(Ordering::Acquire).saturating_add(total) > self.capacity
{
b = self.wake.wait(b).expect("pool backpressure poisoned");
}
drop(b);
let state = Arc::new(RequestState::new(request_id, total));
let entry = RequestEntry {
pending: tasks.into(),
state: Arc::clone(&state),
};
let mut q = self.queues.lock().expect("pool queues poisoned");
assert!(
!q.requests.contains_key(&request_id),
"duplicate pool request id {request_id}"
);
q.requests.insert(request_id, entry);
q.active.push(request_id);
self.in_flight.fetch_add(total, Ordering::AcqRel);
self.peak_in_flight
.fetch_max(self.in_flight.load(Ordering::Relaxed), Ordering::Relaxed);
drop(q);
self.wake.notify_all();
PoolSubmit { state }
}
}
pub struct SearchPool {
runtime: Mutex<Option<PoolRuntime>>,
}
struct PoolRuntime {
shared: Arc<PoolShared>,
workers: Vec<std::thread::JoinHandle<()>>,
}
impl PoolRuntime {
fn shutdown_and_join(self) {
self.shared.shutdown.store(true, Ordering::Release);
self.shared.wake.notify_all();
for h in self.workers {
let _ = h.join();
}
}
}
impl SearchPool {
const fn new() -> Self {
Self {
runtime: Mutex::new(None),
}
}
pub fn enabled(&self) -> bool {
self.runtime
.lock()
.expect("pool runtime poisoned")
.is_some()
}
pub fn enable(&self, workers: usize, queue_factor: usize) {
assert!(workers >= 1, "pool needs at least one worker");
let mut rt = self.runtime.lock().expect("pool runtime poisoned");
if let Some(old) = rt.take() {
old.shutdown_and_join();
}
let shared = Arc::new(PoolShared::new(workers, queue_factor));
let mut handles = Vec::with_capacity(workers);
for w in 0..workers {
let s = Arc::clone(&shared);
handles.push(
std::thread::Builder::new()
.name("entropyfs-pool".into())
.spawn(move || pool_worker_main(s, w))
.expect("spawn pool worker"),
);
}
*rt = Some(PoolRuntime {
shared,
workers: handles,
});
}
pub fn disable(&self) {
let mut rt = self.runtime.lock().expect("pool runtime poisoned");
if let Some(old) = rt.take() {
old.shutdown_and_join();
}
}
pub fn bind(&self, store: &Arc<crate::store::Store>) {
let rt = self.runtime.lock().expect("pool runtime poisoned");
let rt = rt.as_ref().expect("pool must be enabled before bind");
*rt.shared.store.lock().expect("pool store poisoned") = Some(Arc::downgrade(store));
}
pub fn store_arc(&self) -> Option<Arc<crate::store::Store>> {
let rt = self.runtime.lock().expect("pool runtime poisoned");
let rt = rt.as_ref()?;
let g = rt.shared.store.lock().ok()?;
g.as_ref()?.upgrade()
}
pub fn alloc_request_id(&self) -> u64 {
let rt = self.runtime.lock().expect("pool runtime poisoned");
rt.as_ref()
.expect("pool must be enabled to allocate request ids")
.shared
.next_request_id
.fetch_add(1, Ordering::Relaxed)
}
pub(crate) fn submit(&self, request_id: u64, tasks: Vec<WorkerTask>) -> PoolSubmit {
let rt = self.runtime.lock().expect("pool runtime poisoned");
let rt = rt.as_ref().expect("pool must be enabled before submit");
rt.shared.submit(request_id, tasks)
}
pub fn diagnostics(&self) -> PoolDiagnostics {
let rt = self.runtime.lock().expect("pool runtime poisoned");
match rt.as_ref() {
Some(rt) => PoolDiagnostics {
peak_in_flight: rt.shared.peak_in_flight.load(Ordering::Relaxed),
capacity: rt.shared.capacity,
workers: rt.workers.len(),
max_consecutive_same_request: rt.shared.max_consecutive.load(Ordering::Relaxed),
},
None => PoolDiagnostics {
peak_in_flight: 0,
capacity: 0,
workers: 0,
max_consecutive_same_request: 0,
},
}
}
}
pub static POOL: SearchPool = SearchPool::new();
fn pool_worker_main(shared: Arc<PoolShared>, worker_index: usize) {
let mut cursor = worker_index;
let mut last_request: Option<u64> = None;
let mut consecutive = 0usize;
loop {
let picked: Option<(WorkerTask, Arc<RequestState>)> = {
let mut q = shared.queues.lock().expect("pool queues poisoned");
loop {
if shared.shutdown.load(Ordering::Acquire) && q.active.is_empty() {
return;
}
if let Some((task, state)) = pick_one(
&mut q,
&mut cursor,
&shared,
&mut last_request,
&mut consecutive,
) {
break Some((task, state));
}
q = shared.wake.wait(q).expect("pool queues poisoned");
}
};
let (task, state) = picked.expect("picked a task");
let (ordinal, cpu, result) = task.execute();
state.deliver(ordinal, result, cpu);
if state.done.load(Ordering::Acquire) == state.total {
let mut q = shared.queues.lock().expect("pool queues poisoned");
q.requests.remove(&state.id);
drop(q);
}
if shared.in_flight.fetch_sub(1, Ordering::AcqRel) == 1 {
shared.wake.notify_all();
}
}
}
fn pick_one(
q: &mut PoolQueues,
cursor: &mut usize,
shared: &PoolShared,
last_request: &mut Option<u64>,
consecutive: &mut usize,
) -> Option<(WorkerTask, Arc<RequestState>)> {
if q.active.is_empty() {
return None;
}
let idx = *cursor % q.active.len();
let rid = q.active[idx];
*cursor = cursor.wrapping_add(1);
let entry = q
.requests
.get_mut(&rid)
.expect("active request id must be in the request map");
match entry.pending.pop_front() {
Some(task) => {
if *last_request == Some(rid) {
*consecutive += 1;
} else {
*consecutive = 1;
}
*last_request = Some(rid);
shared
.max_consecutive
.fetch_max(*consecutive, Ordering::Relaxed);
entry.state.mark_first_service();
if entry.pending.is_empty() {
q.active.remove(idx);
if *cursor > idx {
*cursor -= 1;
}
}
Some((task, Arc::clone(&entry.state)))
}
None => {
q.active.remove(idx);
if *cursor > idx {
*cursor -= 1;
}
None
}
}
}
#[cfg(test)]
pub mod tests {
use super::*;
pub static POOL_LOCK: Mutex<()> = Mutex::new(());
#[test]
fn budget_serializes_oversized_requests() {
let a = grant(usize::MAX);
assert!(a.n() >= 1);
let held = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let held2 = std::sync::Arc::clone(&held);
let probe = std::thread::spawn(move || {
let b = grant(usize::MAX);
held2.store(b.n() > 0, std::sync::atomic::Ordering::Relaxed);
});
std::thread::sleep(std::time::Duration::from_millis(50));
assert!(!held.load(std::sync::atomic::Ordering::Relaxed));
drop(a);
probe.join().unwrap();
assert!(held.load(std::sync::atomic::Ordering::Relaxed));
}
#[test]
fn pool_reassembles_ordinals_and_honors_backpressure() {
let _guard = POOL_LOCK.lock().expect("pool test lock poisoned");
let dir = tempfile::TempDir::new().unwrap();
let store = Arc::new(
crate::store::Store::create(dir.path(), &Default::default(), [0x11; 16]).unwrap(),
);
POOL.enable(4, 8);
POOL.bind(&store);
let objects = Arc::new(HashMap::new());
let descriptors = Arc::new(HashMap::new());
let limits = *store.limits();
let mut submits = Vec::new();
for _ in 0..3u64 {
let rid = POOL.alloc_request_id();
let tasks = (0..5usize)
.map(|i| WorkerTask::DecodeExtent {
ordinal: i,
store: Arc::clone(&store),
start: (i as u64) * 64,
desc: crate::core::representation::Representation::Zero { len: 64 },
objects: Arc::clone(&objects),
descriptors: Arc::clone(&descriptors),
limits,
})
.collect();
submits.push(POOL.submit(rid, tasks));
}
for (r, s) in submits.into_iter().enumerate() {
let (results, m) = s.join();
assert_eq!(results.len(), 5, "request {r}: all ordinals present");
for (i, wr) in results.iter().enumerate() {
assert_eq!(wr.ordinal, i, "request {r}: ordinal order preserved");
match &wr.result {
Ok(WorkerOutcome::Decode((start, chunk))) => {
assert_eq!(*start, (i as u64) * 64);
assert_eq!(chunk.len(), 64);
assert!(chunk.iter().all(|&b| b == 0));
}
other => panic!("request {r}: unexpected outcome {other:?}"),
}
}
assert_eq!(m.tasks, 5);
assert!(m.span_ns > 0);
}
let d = POOL.diagnostics();
assert!(
d.peak_in_flight <= d.capacity,
"backpressure bound held (peak {} <= capacity {})",
d.peak_in_flight,
d.capacity
);
assert_eq!(d.workers, 4);
let rid = POOL.alloc_request_id();
let tasks = (0..40usize)
.map(|i| WorkerTask::DecodeExtent {
ordinal: i,
store: Arc::clone(&store),
start: 0,
desc: crate::core::representation::Representation::Zero { len: 16 },
objects: Arc::clone(&objects),
descriptors: Arc::clone(&descriptors),
limits,
})
.collect();
let (results, _) = POOL.submit(rid, tasks).join();
assert_eq!(results.len(), 40);
for (i, wr) in results.iter().enumerate() {
assert_eq!(wr.ordinal, i);
}
POOL.disable();
assert!(!POOL.enabled());
}
}