use std::collections::HashMap;
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::{Arc, Mutex as StdMutex, Weak};
use anyhow::{Result, anyhow};
use bytes::Bytes;
use micromegas_tracing::prelude::*;
use tokio::sync::{Notify, OwnedSemaphorePermit, Semaphore, watch};
use crate::metric_tags;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u8)]
pub(super) enum Priority {
Demand = 0,
Prefetch = 1,
}
impl Priority {
fn from_u8(v: u8) -> Self {
if v == Priority::Demand as u8 {
Priority::Demand
} else {
Priority::Prefetch
}
}
pub(super) fn class_label(self) -> &'static str {
match self {
Priority::Demand => metric_tags::CLASS_DEMAND,
Priority::Prefetch => metric_tags::CLASS_PREFETCH,
}
}
}
pub(super) fn effective_priority(entries: &[Arc<InFlight>]) -> Priority {
if entries.iter().any(|e| e.priority() == Priority::Demand) {
Priority::Demand
} else {
Priority::Prefetch
}
}
pub(super) struct BatchState {
entries: StdMutex<Vec<Weak<InFlight>>>,
}
impl BatchState {
pub(super) fn new(capacity: usize) -> Self {
Self {
entries: StdMutex::new(Vec::with_capacity(capacity)),
}
}
}
type FetchResult = Result<Bytes, Arc<anyhow::Error>>;
pub(super) struct InFlight {
priority: AtomicU8,
promote: Notify,
result: watch::Sender<Option<FetchResult>>,
batch: Option<Arc<BatchState>>,
fulfilled: std::sync::atomic::AtomicBool,
}
impl InFlight {
fn new(priority: Priority, batch: Option<Arc<BatchState>>) -> Self {
let (result, _rx) = watch::channel(None);
Self {
priority: AtomicU8::new(priority as u8),
promote: Notify::new(),
result,
batch,
fulfilled: std::sync::atomic::AtomicBool::new(false),
}
}
fn priority(&self) -> Priority {
Priority::from_u8(self.priority.load(Ordering::Acquire))
}
fn promote_to_demand(&self) {
self.priority
.store(Priority::Demand as u8, Ordering::Release);
self.promote.notify_one();
}
pub(super) fn fulfill(&self, result: FetchResult) {
if self.fulfilled.swap(true, Ordering::AcqRel) {
return;
}
self.result.send_replace(Some(result));
}
pub(super) async fn join(&self) -> FetchResult {
let mut rx = self.result.subscribe();
loop {
if let Some(r) = rx.borrow_and_update().clone() {
return r;
}
if rx.changed().await.is_err() {
return Err(Arc::new(anyhow!(
"in-flight fetch entry dropped without a result"
)));
}
}
}
}
pub(super) enum Ownership {
Owner(Arc<InFlight>),
Joiner(Arc<InFlight>),
}
pub(super) struct FetchScheduler {
inflight: StdMutex<HashMap<String, Arc<InFlight>>>,
shared_permits: Arc<Semaphore>,
shared_total: usize,
prefetch_permits: Arc<Semaphore>,
prefetch_total: usize,
promote_whole_batch: bool,
}
impl FetchScheduler {
pub(super) fn new(total: usize, demand_reserved: usize, promote_whole_batch: bool) -> Self {
assert!(total > 0, "fetch concurrency total must be > 0");
assert!(
demand_reserved < total,
"demand_reserved ({demand_reserved}) must be < total ({total})"
);
let prefetch_total = total - demand_reserved;
Self {
inflight: StdMutex::new(HashMap::new()),
shared_permits: Arc::new(Semaphore::new(total)),
shared_total: total,
prefetch_permits: Arc::new(Semaphore::new(prefetch_total)),
prefetch_total,
promote_whole_batch,
}
}
pub(super) fn fetch_budget_stats(&self) -> (usize, usize, usize, usize) {
(
self.shared_permits.available_permits(),
self.shared_total,
self.prefetch_permits.available_permits(),
self.prefetch_total,
)
}
pub(super) fn inflight_len(&self) -> usize {
self.inflight.lock().expect("inflight lock").len()
}
pub(super) fn own_or_join(
&self,
key: String,
prio: Priority,
batch: Option<Arc<BatchState>>,
) -> Ownership {
let mut promote_batch: Option<(Arc<BatchState>, Arc<InFlight>)> = None;
let ownership = {
let mut map = self.inflight.lock().expect("inflight lock");
if let Some(existing) = map.get(&key) {
if prio == Priority::Demand && existing.priority() == Priority::Prefetch {
existing.promote_to_demand();
if self.promote_whole_batch
&& let Some(bs) = existing.batch.clone()
{
promote_batch = Some((bs, existing.clone()));
}
}
if let Some(bs) = &batch {
bs.entries
.lock()
.expect("batch lock")
.push(Arc::downgrade(existing));
}
Ownership::Joiner(existing.clone())
} else {
let entry = Arc::new(InFlight::new(prio, batch.clone()));
if let Some(bs) = &batch {
bs.entries
.lock()
.expect("batch lock")
.push(Arc::downgrade(&entry));
}
map.insert(key.clone(), entry.clone());
Ownership::Owner(entry)
}
};
if let Some((bs, skip)) = promote_batch {
self.promote_batch_siblings(&bs, &skip);
}
ownership
}
fn promote_batch_siblings(&self, batch: &BatchState, skip: &Arc<InFlight>) {
let entries = batch.entries.lock().expect("batch lock");
for weak in entries.iter() {
if let Some(entry) = weak.upgrade()
&& !Arc::ptr_eq(&entry, skip)
{
entry.promote_to_demand();
}
}
}
pub(super) fn remove_entry(&self, key: &str) {
self.inflight.lock().expect("inflight lock").remove(key);
}
}
pub(super) struct FulfillGuard {
scheduler: Arc<FetchScheduler>,
entries: Vec<(String, Arc<InFlight>)>,
armed: bool,
}
impl FulfillGuard {
pub(super) fn new(
scheduler: Arc<FetchScheduler>,
entries: Vec<(String, Arc<InFlight>)>,
) -> Self {
Self {
scheduler,
entries,
armed: true,
}
}
pub(super) fn disarm(mut self) {
self.armed = false;
}
}
impl Drop for FulfillGuard {
fn drop(&mut self) {
if !self.armed {
return;
}
warn!(
"fetch task exited without completing normally (likely a panic); \
fulfilling {} in-flight entries with an error",
self.entries.len()
);
for (key, entry) in &self.entries {
entry.fulfill(Err(Arc::new(anyhow!(
"fetch task exited without producing a result (panic during fetch)"
))));
self.scheduler.remove_entry(key);
}
}
}
pub(super) struct RunPermit {
_shared: OwnedSemaphorePermit,
_prefetch: Option<OwnedSemaphorePermit>,
}
async fn any_entry_promoted(entries: &[Arc<InFlight>]) {
match entries {
[] => std::future::pending::<()>().await,
[only] => only.promote.notified().await,
many => {
let futs: Vec<_> = many
.iter()
.map(|e| Box::pin(e.promote.notified()))
.collect();
futures::future::select_all(futs).await;
}
}
}
pub(super) async fn acquire_run_permit(
scheduler: &FetchScheduler,
entries: &[Arc<InFlight>],
) -> RunPermit {
loop {
let all_prefetch = entries.iter().all(|e| e.priority() == Priority::Prefetch);
if !all_prefetch {
let shared = scheduler
.shared_permits
.clone()
.acquire_owned()
.await
.expect("shared_permits semaphore is never closed");
return RunPermit {
_shared: shared,
_prefetch: None,
};
}
tokio::select! {
prefetch = scheduler.prefetch_permits.clone().acquire_owned() => {
let prefetch = prefetch.expect("prefetch_permits semaphore is never closed");
tokio::select! {
shared = scheduler.shared_permits.clone().acquire_owned() => {
let shared = shared.expect("shared_permits semaphore is never closed");
return RunPermit { _shared: shared, _prefetch: Some(prefetch) };
}
_ = any_entry_promoted(entries) => {
drop(prefetch);
continue;
}
}
}
_ = any_entry_promoted(entries) => {
continue;
}
}
}
}
pub(super) fn reconstruct_shared_error(shared: &Arc<anyhow::Error>) -> anyhow::Error {
if let Some(object_store::Error::NotFound { path, source }) =
shared.downcast_ref::<object_store::Error>()
{
let rebuilt = object_store::Error::NotFound {
path: path.clone(),
source: Box::new(std::io::Error::new(
std::io::ErrorKind::NotFound,
source.to_string(),
)),
};
return anyhow::Error::from(rebuilt);
}
anyhow!("{shared:?}")
}
pub(super) fn decode_size(data: &Bytes) -> Result<u64> {
Ok(u64::from_le_bytes(
data[..8].try_into().expect("8-byte size slice"),
))
}