mod filesystem;
use std::collections::HashMap;
use std::collections::VecDeque;
use std::fmt;
use std::num::NonZeroUsize;
use std::path::Path;
use std::path::PathBuf;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::sync::Weak;
use std::time::Duration;
use futures::stream::FuturesUnordered;
use futures::StreamExt;
use parking_lot::Mutex;
use tokio::sync::mpsc;
use tokio::sync::oneshot;
use tokio::sync::Notify;
use tokio_util::sync::CancellationToken;
use crate::metadata_target::MetadataTargetIdentity;
use crate::metadata_target::MetadataTargetRegistration;
use crate::metadata_target::MetadataTargetRegistrationOutcome;
use crate::metadata_target::MetadataTargetRegistry;
use crate::metadata_target::MetadataTargetRetirementOutcome;
use crate::resource_budget::BudgetCapacity;
use crate::resource_budget::BudgetLimit;
use crate::resource_budget::FullPolicy;
use crate::resource_budget::ResourceBudget;
use crate::resource_budget::ResourcePermit;
use crate::shutdown_deadline::ShutdownDeadline;
use crate::BlockingExecutor;
use crate::BlockingPoolPolicy;
use crate::ChildServiceContext;
use crate::RuntimeContractPolicy;
use crate::RuntimeContractViolation;
use crate::RuntimeError;
use crate::RuntimeResult;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MetadataDeadline {
at: tokio::time::Instant,
}
impl MetadataDeadline {
#[must_use]
pub fn after(timeout: Duration) -> Self {
let now = tokio::time::Instant::now();
Self {
at: now.checked_add(timeout).unwrap_or(now),
}
}
#[must_use]
pub const fn at(at: tokio::time::Instant) -> Self {
Self { at }
}
#[must_use]
pub const fn instant(self) -> tokio::time::Instant {
self.at
}
#[must_use]
pub fn remaining(self) -> Duration {
self.at.saturating_duration_since(tokio::time::Instant::now())
}
#[must_use]
pub fn is_expired(self) -> bool {
tokio::time::Instant::now() >= self.at
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct MetadataGeneration(u64);
impl MetadataGeneration {
#[must_use]
pub const fn new(value: u64) -> Self {
Self(value)
}
#[must_use]
pub const fn get(self) -> u64 {
self.0
}
}
impl From<u64> for MetadataGeneration {
fn from(value: u64) -> Self {
Self::new(value)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MetadataDurability {
Accepted,
Durable(MetadataGeneration),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MetadataIoOperation {
CreateParent,
CreateTemporary,
WriteTemporary,
SyncTemporary,
ReplaceTarget,
SyncParent,
RemoveTemporary,
}
impl fmt::Display for MetadataIoOperation {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let value = match self {
Self::CreateParent => "create parent directory",
Self::CreateTemporary => "create temporary file",
Self::WriteTemporary => "write temporary file",
Self::SyncTemporary => "sync temporary file",
Self::ReplaceTarget => "replace target file",
Self::SyncParent => "sync parent directory",
Self::RemoveTemporary => "remove temporary file",
};
formatter.write_str(value)
}
}
impl MetadataIoOperation {
#[must_use]
pub const fn runtime_operation(self) -> crate::RuntimeOperation {
match self {
Self::CreateParent => crate::RuntimeOperation::MetadataCreateParent,
Self::CreateTemporary => crate::RuntimeOperation::MetadataCreateTemporary,
Self::WriteTemporary => crate::RuntimeOperation::MetadataWriteTemporary,
Self::SyncTemporary => crate::RuntimeOperation::MetadataSyncTemporary,
Self::ReplaceTarget => crate::RuntimeOperation::MetadataReplaceTarget,
Self::SyncParent => crate::RuntimeOperation::MetadataSyncParent,
Self::RemoveTemporary => crate::RuntimeOperation::MetadataRemoveTemporary,
}
}
}
fn metadata_io_failure(operation: MetadataIoOperation, _path: &Path, source: std::io::Error) -> RuntimeError {
RuntimeError::io(operation.runtime_operation(), source)
}
#[derive(Debug, Clone)]
pub struct MetadataWriteRequest {
resource: Arc<str>,
generation: MetadataGeneration,
target: Arc<Path>,
bytes: Arc<[u8]>,
lane_deadline: Option<MetadataDeadline>,
}
impl MetadataWriteRequest {
#[must_use]
pub fn new(
resource: impl Into<Arc<str>>,
generation: impl Into<MetadataGeneration>,
target: impl Into<PathBuf>,
bytes: impl Into<Vec<u8>>,
) -> Self {
Self {
resource: resource.into(),
generation: generation.into(),
target: Arc::from(target.into()),
bytes: Arc::from(bytes.into()),
lane_deadline: None,
}
}
#[must_use]
pub fn with_lane_deadline(mut self, deadline: MetadataDeadline) -> Self {
self.lane_deadline = Some(deadline);
self
}
#[must_use]
pub const fn lane_deadline(&self) -> Option<MetadataDeadline> {
self.lane_deadline
}
#[must_use]
pub fn resource(&self) -> &str {
&self.resource
}
#[must_use]
pub const fn generation(&self) -> MetadataGeneration {
self.generation
}
#[must_use]
pub fn target(&self) -> &Path {
&self.target
}
#[must_use]
pub fn len(&self) -> usize {
self.bytes.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.bytes.is_empty()
}
}
pub trait MetadataFileSystem: fmt::Debug + Send + Sync + 'static {
fn persist_atomic(&self, target: &Path, bytes: &[u8]) -> RuntimeResult<()>;
}
#[derive(Debug, Default)]
pub struct LocalMetadataFileSystem;
impl MetadataFileSystem for LocalMetadataFileSystem {
fn persist_atomic(&self, target: &Path, bytes: &[u8]) -> RuntimeResult<()> {
filesystem::persist_atomic_local(target, bytes)
}
}
#[derive(Debug, Clone)]
pub struct MetadataIoConfig {
pub max_pending_operations: usize,
pub max_pending_bytes: usize,
pub blocking_queue_timeout: Duration,
pub blocking_task_timeout: Duration,
pub blocking_warn_after: Duration,
}
#[derive(Debug, Clone)]
pub struct MetadataIoPlan {
config: MetadataIoConfig,
max_concurrent_writes: NonZeroUsize,
}
impl MetadataIoConfig {
pub fn validate(&self) -> Result<(), RuntimeContractViolation> {
if self.max_pending_operations == 0 {
return Err(RuntimeContractViolation::InvalidMetadataConfiguration {
policy: RuntimeContractPolicy::MetadataMaxPendingOperationsPositive,
});
}
if self.max_pending_bytes == 0 {
return Err(RuntimeContractViolation::InvalidMetadataConfiguration {
policy: RuntimeContractPolicy::MetadataMaxPendingBytesPositive,
});
}
if self.blocking_task_timeout.is_zero() {
return Err(RuntimeContractViolation::InvalidMetadataConfiguration {
policy: RuntimeContractPolicy::MetadataBlockingTaskTimeoutPositive,
});
}
Ok(())
}
pub fn into_plan(self) -> Result<MetadataIoPlan, RuntimeContractViolation> {
self.validate()?;
Ok(MetadataIoPlan {
config: self,
max_concurrent_writes: NonZeroUsize::MIN,
})
}
}
impl MetadataIoPlan {
#[must_use]
pub fn with_max_concurrent_writes(mut self, writes: NonZeroUsize) -> Self {
self.max_concurrent_writes = writes;
self
}
pub fn start(self, service_context: &ChildServiceContext) -> RuntimeResult<MetadataIoActor> {
MetadataIoActor::start_validated(service_context, self, Arc::new(LocalMetadataFileSystem))
}
pub fn start_with_file_system(
self,
service_context: &ChildServiceContext,
file_system: Arc<dyn MetadataFileSystem>,
) -> RuntimeResult<MetadataIoActor> {
MetadataIoActor::start_validated(service_context, self, file_system)
}
}
impl Default for MetadataIoConfig {
fn default() -> Self {
Self {
max_pending_operations: 1_024,
max_pending_bytes: 64 * 1024 * 1024,
blocking_queue_timeout: Duration::from_secs(5),
blocking_task_timeout: Duration::from_secs(30),
blocking_warn_after: Duration::from_secs(1),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MetadataLimitSource {
BlockingLanePolicy,
ActorConfiguration,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MetadataIoLegacyBlockingField {
pub field: &'static str,
pub configured: Duration,
pub effective: Duration,
pub effective_source: MetadataLimitSource,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MetadataIoBlockingLaneProfile {
pub name: String,
pub max_concurrency: usize,
pub max_queue_depth: usize,
pub queue_timeout: Duration,
pub task_timeout: Duration,
pub warn_after: Duration,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MetadataIoActorLimitsProfile {
pub max_pending_operations: usize,
pub max_pending_bytes: usize,
pub max_waiters: usize,
pub max_concurrent_writes: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MetadataIoEffectiveProfile {
pub blocking_lane: MetadataIoBlockingLaneProfile,
pub actor_limits: MetadataIoActorLimitsProfile,
pub legacy_blocking_fields: [MetadataIoLegacyBlockingField; 3],
}
fn max_metadata_waiters(max_pending_operations: usize) -> usize {
max_pending_operations.saturating_mul(4).max(1)
}
fn effective_concurrent_writes(planned: NonZeroUsize, blocking_policy: &BlockingPoolPolicy) -> usize {
planned.get().min(blocking_policy.max_concurrency).max(1)
}
fn effective_profile(
config: &MetadataIoConfig,
blocking_policy: &BlockingPoolPolicy,
max_concurrent_writes: usize,
) -> MetadataIoEffectiveProfile {
let legacy_blocking_field =
|field: &'static str, configured: Duration, effective: Duration| MetadataIoLegacyBlockingField {
field,
configured,
effective,
effective_source: MetadataLimitSource::BlockingLanePolicy,
};
MetadataIoEffectiveProfile {
blocking_lane: MetadataIoBlockingLaneProfile {
name: blocking_policy.name.clone(),
max_concurrency: blocking_policy.max_concurrency,
max_queue_depth: blocking_policy.max_queue_depth,
queue_timeout: blocking_policy.queue_timeout,
task_timeout: blocking_policy.task_timeout,
warn_after: blocking_policy.warn_after,
},
actor_limits: MetadataIoActorLimitsProfile {
max_pending_operations: config.max_pending_operations,
max_pending_bytes: config.max_pending_bytes,
max_waiters: max_metadata_waiters(config.max_pending_operations),
max_concurrent_writes,
},
legacy_blocking_fields: [
legacy_blocking_field(
"blocking_queue_timeout",
config.blocking_queue_timeout,
blocking_policy.queue_timeout,
),
legacy_blocking_field(
"blocking_task_timeout",
config.blocking_task_timeout,
blocking_policy.task_timeout,
),
legacy_blocking_field(
"blocking_warn_after",
config.blocking_warn_after,
blocking_policy.warn_after,
),
],
}
}
fn warn_legacy_blocking_fields(config: &MetadataIoConfig, blocking_policy: &BlockingPoolPolicy) {
let defaults = MetadataIoConfig::default();
let changed = [
(
"blocking_queue_timeout",
config.blocking_queue_timeout,
defaults.blocking_queue_timeout,
),
(
"blocking_task_timeout",
config.blocking_task_timeout,
defaults.blocking_task_timeout,
),
(
"blocking_warn_after",
config.blocking_warn_after,
defaults.blocking_warn_after,
),
]
.into_iter()
.filter(|(_field, configured, default)| configured != default)
.map(|(field, _configured, _default)| field)
.collect::<Vec<_>>();
if changed.is_empty() {
return;
}
tracing::warn!(
fields = ?changed,
lane = %blocking_policy.name,
"metadata blocking fields are retained for source compatibility and do not override the shared blocking lane policy"
);
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MetadataIoResourceSnapshot {
pub resource: Arc<str>,
pub target: Option<Arc<Path>>,
pub durable_generation: Option<MetadataGeneration>,
pub in_flight_generation: Option<MetadataGeneration>,
pub queued_generation: Option<MetadataGeneration>,
pub waiter_count: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MetadataIoSnapshot {
pub accepting: bool,
pub pending_operations: usize,
pub pending_bytes: usize,
pub max_pending_operations: usize,
pub max_pending_bytes: usize,
pub resources: Vec<MetadataIoResourceSnapshot>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MetadataIoShutdownReport {
pub timed_out: bool,
pub pending_operations: usize,
pub pending_bytes: usize,
pub unfinished: Vec<MetadataIoResourceSnapshot>,
}
#[derive(Debug)]
pub struct MetadataIoReceipt {
generation: MetadataGeneration,
identity: MetadataTargetIdentity,
durable: oneshot::Receiver<RuntimeResult<MetadataIoCommitOutcome>>,
}
#[derive(Debug)]
pub enum MetadataIoAdmissionOutcome {
Accepted(MetadataIoReceipt),
TargetConflict(MetadataWriteRequest),
}
#[derive(Debug)]
pub enum MetadataIoDurabilityOutcome {
Durable(MetadataGeneration),
TargetConflict(MetadataWriteRequest),
}
#[derive(Debug, Clone)]
pub enum MetadataIoCommitOutcome {
Durable(MetadataGeneration),
FailedBeforeCommit(RuntimeError),
CommitOutcomeUnknown(RuntimeError),
}
#[derive(Debug)]
pub enum MetadataIoCommitAdmissionOutcome {
Completed(MetadataIoCommitOutcome),
TargetConflict(MetadataWriteRequest),
}
#[derive(Debug)]
pub enum MetadataIoCommitObservation {
Settled {
generation: MetadataGeneration,
outcome: MetadataIoCommitOutcome,
},
Unobserved {
generation: MetadataGeneration,
},
TargetConflict(MetadataWriteRequest),
}
impl MetadataIoCommitObservation {
#[must_use]
pub fn requires_reconciliation(&self) -> bool {
match self {
MetadataIoCommitObservation::Settled { outcome, .. } => {
matches!(outcome, MetadataIoCommitOutcome::CommitOutcomeUnknown(_))
}
MetadataIoCommitObservation::Unobserved { .. } => true,
MetadataIoCommitObservation::TargetConflict(_) => false,
}
}
#[must_use]
pub fn settled(self) -> Option<MetadataIoCommitOutcome> {
match self {
MetadataIoCommitObservation::Settled { outcome, .. } => Some(outcome),
MetadataIoCommitObservation::Unobserved { .. } | MetadataIoCommitObservation::TargetConflict(_) => None,
}
}
#[must_use]
pub const fn generation(&self) -> Option<MetadataGeneration> {
match self {
MetadataIoCommitObservation::Settled { generation, .. }
| MetadataIoCommitObservation::Unobserved { generation } => Some(*generation),
MetadataIoCommitObservation::TargetConflict(_) => None,
}
}
#[must_use]
pub const fn unobserved_generation(&self) -> Option<MetadataGeneration> {
match self {
MetadataIoCommitObservation::Unobserved { generation } => Some(*generation),
MetadataIoCommitObservation::Settled { .. } | MetadataIoCommitObservation::TargetConflict(_) => None,
}
}
}
enum ReceiptConclusion {
Delivered(RuntimeResult<MetadataIoCommitOutcome>),
Expired,
CoordinatorStopped,
}
impl MetadataIoReceipt {
pub fn target_identity(&self) -> MetadataTargetIdentity {
self.identity.clone()
}
#[must_use]
pub const fn generation(&self) -> MetadataGeneration {
self.generation
}
pub async fn wait_until_outcome(self, deadline: MetadataDeadline) -> RuntimeResult<MetadataIoCommitOutcome> {
if deadline.is_expired() {
return Err(RuntimeError::timed_out(crate::RuntimeOperation::WaitForDurableMetadata));
}
match self.conclude(deadline).await {
ReceiptConclusion::Delivered(result) => result,
ReceiptConclusion::CoordinatorStopped => {
Err(RuntimeError::closed(crate::RuntimeOperation::MetadataWorkerStopped))
}
ReceiptConclusion::Expired => Err(RuntimeError::timed_out(crate::RuntimeOperation::WaitForDurableMetadata)),
}
}
pub async fn observe_until(self, deadline: MetadataDeadline) -> MetadataIoCommitObservation {
let generation = self.generation;
match self.conclude(deadline).await {
ReceiptConclusion::Delivered(Ok(outcome)) => MetadataIoCommitObservation::Settled { generation, outcome },
ReceiptConclusion::Delivered(Err(error)) => MetadataIoCommitObservation::Settled {
generation,
outcome: MetadataIoCommitOutcome::FailedBeforeCommit(error),
},
ReceiptConclusion::CoordinatorStopped | ReceiptConclusion::Expired => {
MetadataIoCommitObservation::Unobserved { generation }
}
}
}
async fn conclude(self, deadline: MetadataDeadline) -> ReceiptConclusion {
match tokio::time::timeout_at(deadline.instant(), self.durable).await {
Ok(Ok(result)) => ReceiptConclusion::Delivered(result),
Ok(Err(_closed)) => ReceiptConclusion::CoordinatorStopped,
Err(_elapsed) => ReceiptConclusion::Expired,
}
}
pub async fn wait_until(self, deadline: MetadataDeadline) -> RuntimeResult<MetadataGeneration> {
match self.wait_until_outcome(deadline).await? {
MetadataIoCommitOutcome::Durable(generation) => Ok(generation),
MetadataIoCommitOutcome::FailedBeforeCommit(source)
| MetadataIoCommitOutcome::CommitOutcomeUnknown(source) => Err(source),
}
}
}
#[derive(Debug, Clone)]
pub struct MetadataIoActor {
inner: Arc<ActorInner>,
sender: mpsc::Sender<Arc<str>>,
}
#[derive(Debug, Clone)]
pub struct MetadataIoObserver {
inner: Weak<ActorInner>,
}
impl MetadataIoObserver {
pub fn snapshot(&self) -> Option<MetadataIoSnapshot> {
self.inner.upgrade().map(|inner| snapshot(&inner))
}
}
#[derive(Debug)]
struct ActorInner {
config: MetadataIoConfig,
blocking_policy: BlockingPoolPolicy,
max_concurrent_writes: usize,
retained_bytes: ResourceBudget,
targets: MetadataTargetRegistry,
waiter_count: Arc<AtomicUsize>,
state: Mutex<ActorState>,
shutdown: Notify,
worker_finished: Notify,
}
#[derive(Debug)]
struct ActorState {
accepting: bool,
worker_finished: bool,
pending_operations: usize,
pending_bytes: usize,
resources: HashMap<Arc<str>, ResourceState>,
}
#[derive(Debug, Default)]
struct ResourceState {
identity: Option<MetadataTargetIdentity>,
target: Option<Arc<Path>>,
target_registration: Option<MetadataTargetRegistration>,
durable_generation: Option<MetadataGeneration>,
in_flight: Option<WorkMeta>,
queued: Option<QueuedMetadataWrite>,
waiters: Vec<GenerationWaiter>,
}
#[derive(Debug)]
struct QueuedMetadataWrite {
request: MetadataWriteRequest,
registration: MetadataTargetRegistration,
charge: ResourcePermit,
}
#[derive(Debug)]
struct WorkMeta {
generation: MetadataGeneration,
bytes: usize,
}
struct ChargedSnapshot {
bytes: Arc<[u8]>,
_charge: ResourcePermit,
}
#[derive(Debug)]
struct GenerationWaiter {
generation: MetadataGeneration,
sender: oneshot::Sender<RuntimeResult<MetadataIoCommitOutcome>>,
_permit: WaiterPermit,
}
#[derive(Debug)]
struct WaiterPermit {
waiter_count: Arc<AtomicUsize>,
}
impl Drop for WaiterPermit {
fn drop(&mut self) {
self.waiter_count.fetch_sub(1, Ordering::AcqRel);
}
}
impl ActorInner {
fn reserve_waiter(&self, resource_waiters: usize) -> RuntimeResult<WaiterPermit> {
let max_waiters = max_metadata_waiters(self.config.max_pending_operations);
if resource_waiters >= max_waiters {
return Err(RuntimeError::capacity(crate::RuntimeOperation::AdmitMetadataOperation));
}
let mut current = self.waiter_count.load(Ordering::Acquire);
loop {
if current >= max_waiters {
return Err(RuntimeError::capacity(crate::RuntimeOperation::AdmitMetadataOperation));
}
match self
.waiter_count
.compare_exchange_weak(current, current + 1, Ordering::AcqRel, Ordering::Acquire)
{
Ok(_) => {
return Ok(WaiterPermit {
waiter_count: Arc::clone(&self.waiter_count),
});
}
Err(observed) => current = observed,
}
}
}
}
impl MetadataIoActor {
fn start_validated(
service_context: &ChildServiceContext,
plan: MetadataIoPlan,
file_system: Arc<dyn MetadataFileSystem>,
) -> RuntimeResult<Self> {
let MetadataIoPlan {
config,
max_concurrent_writes,
} = plan;
let task_group = service_context.component("metadata-io").task_group().clone();
let blocking = service_context.metadata_io().clone();
let blocking_policy = blocking.policy().clone();
warn_legacy_blocking_fields(&config, &blocking_policy);
let max_concurrent_writes = effective_concurrent_writes(max_concurrent_writes, &blocking_policy);
let retained_bytes = retained_bytes_budget(&service_context.process_budget())?;
let targets = service_context.resources().metadata_targets();
let (sender, receiver) = mpsc::channel(config.max_pending_operations);
let inner = Arc::new(ActorInner {
config,
blocking_policy,
max_concurrent_writes,
retained_bytes,
targets,
waiter_count: Arc::new(AtomicUsize::new(0)),
state: Mutex::new(ActorState {
accepting: true,
worker_finished: false,
pending_operations: 0,
pending_bytes: 0,
resources: HashMap::new(),
}),
shutdown: Notify::new(),
worker_finished: Notify::new(),
});
let actor = Self {
inner: inner.clone(),
sender,
};
let cancellation = task_group.cancellation_token();
task_group
.spawn_service(
"metadata-io.coordinator",
run_actor(inner, receiver, blocking, file_system, cancellation),
)
.map_err(startup_error)?;
Ok(actor)
}
pub fn submit(
&self,
request: MetadataWriteRequest,
deadline: MetadataDeadline,
) -> RuntimeResult<MetadataIoAdmissionOutcome> {
if deadline.is_expired() {
return Err(RuntimeError::timed_out(crate::RuntimeOperation::AdmitMetadataSnapshot));
}
let resource = request.resource.clone();
let generation = request.generation;
let (waiter_sender, durable) = oneshot::channel();
let mut state = self.inner.state.lock();
if !state.accepting {
return Err(RuntimeError::closed(crate::RuntimeOperation::MetadataIoClosed));
}
let Some(target_registration) = ensure_target_registration(&mut state, &self.inner.targets, &request)? else {
return Ok(MetadataIoAdmissionOutcome::TargetConflict(request));
};
let existing = state.resources.get(&resource);
if let Some(existing) = existing {
if let Some(durable_generation) = existing.durable_generation {
if generation <= durable_generation {
let _ = waiter_sender.send(Ok(MetadataIoCommitOutcome::Durable(durable_generation)));
return Ok(MetadataIoAdmissionOutcome::Accepted(MetadataIoReceipt {
generation,
identity: target_registration.identity(),
durable,
}));
}
}
if let Some(in_flight) = &existing.in_flight {
if generation <= in_flight.generation {
let waiter_permit = self.inner.reserve_waiter(existing.waiters.len())?;
let Some(resource_state) = state.resources.get_mut(&resource) else {
return Err(RuntimeError::closed(crate::RuntimeOperation::MetadataWorkerStopped));
};
resource_state.waiters.push(GenerationWaiter {
generation,
sender: waiter_sender,
_permit: waiter_permit,
});
return Ok(MetadataIoAdmissionOutcome::Accepted(MetadataIoReceipt {
generation,
identity: target_registration.identity(),
durable,
}));
}
}
if let Some(queued) = &existing.queued {
if generation <= queued.request.generation {
let waiter_permit = self.inner.reserve_waiter(existing.waiters.len())?;
let Some(resource_state) = state.resources.get_mut(&resource) else {
return Err(RuntimeError::closed(crate::RuntimeOperation::MetadataWorkerStopped));
};
resource_state.waiters.push(GenerationWaiter {
generation,
sender: waiter_sender,
_permit: waiter_permit,
});
return Ok(MetadataIoAdmissionOutcome::Accepted(MetadataIoReceipt {
generation,
identity: target_registration.identity(),
durable,
}));
}
}
}
let waiter_permit = self
.inner
.reserve_waiter(existing.map_or(0, |resource_state| resource_state.waiters.len()))?;
let old_queued_bytes = existing
.and_then(|resource_state| resource_state.queued.as_ref())
.map_or(0, |queued| queued.request.len());
let adds_operation = existing.is_none_or(|resource_state| resource_state.queued.is_none());
let needs_queue_token =
existing.is_none_or(|resource_state| resource_state.in_flight.is_none() && resource_state.queued.is_none());
let next_operations = state.pending_operations + usize::from(adds_operation);
if next_operations > self.inner.config.max_pending_operations {
return Err(RuntimeError::capacity(crate::RuntimeOperation::AdmitMetadataOperation));
}
let retained_without_replaced = state.pending_bytes.saturating_sub(old_queued_bytes);
let next_bytes = retained_without_replaced
.checked_add(request.len())
.ok_or_else(|| RuntimeError::capacity(crate::RuntimeOperation::AdmitMetadataBytes))?;
if next_bytes > self.inner.config.max_pending_bytes {
return Err(RuntimeError::capacity(crate::RuntimeOperation::AdmitMetadataBytes));
}
let queue_permit = if needs_queue_token {
Some(
self.sender
.try_reserve()
.map_err(|_error| RuntimeError::capacity(crate::RuntimeOperation::AdmitMetadataOperation))?,
)
} else {
None
};
let resource_state = state.resources.entry(resource.clone()).or_default();
if let Some(queued) = &mut resource_state.queued {
let new_bytes = request.len();
queued
.charge
.try_resize(new_bytes.max(queued.charge.bytes()))
.map_err(|_rejection| RuntimeError::capacity(crate::RuntimeOperation::AdmitMetadataBytes))?;
queued.request = request;
queued.charge.shrink_to(new_bytes);
queued.registration = target_registration.clone();
} else {
let charge = self
.inner
.retained_bytes
.try_acquire_data(request.len())
.map_err(|_rejection| RuntimeError::capacity(crate::RuntimeOperation::AdmitMetadataBytes))?;
resource_state.queued = Some(QueuedMetadataWrite {
request,
registration: target_registration.clone(),
charge,
});
}
let identity = target_registration.identity();
resource_state.target = resource_state
.queued
.as_ref()
.map(|queued| queued.request.target.clone());
resource_state.target_registration = Some(target_registration.clone());
resource_state.waiters.push(GenerationWaiter {
generation,
sender: waiter_sender,
_permit: waiter_permit,
});
state.pending_operations = next_operations;
state.pending_bytes = next_bytes;
drop(state);
if let Some(permit) = queue_permit {
permit.send(resource.clone());
}
Ok(MetadataIoAdmissionOutcome::Accepted(MetadataIoReceipt {
generation,
identity,
durable,
}))
}
pub fn submit_next(
&self,
resource: impl Into<Arc<str>>,
target: impl Into<PathBuf>,
bytes: impl Into<Vec<u8>>,
deadline: MetadataDeadline,
) -> RuntimeResult<MetadataIoAdmissionOutcome> {
let generation = self.inner.targets.next_generation();
self.submit(MetadataWriteRequest::new(resource, generation, target, bytes), deadline)
}
pub async fn submit_durable(
&self,
request: MetadataWriteRequest,
deadline: MetadataDeadline,
) -> RuntimeResult<MetadataIoDurabilityOutcome> {
match self.submit(request, deadline)? {
MetadataIoAdmissionOutcome::Accepted(receipt) => receipt
.wait_until(deadline)
.await
.map(MetadataIoDurabilityOutcome::Durable),
MetadataIoAdmissionOutcome::TargetConflict(request) => {
Ok(MetadataIoDurabilityOutcome::TargetConflict(request))
}
}
}
pub async fn submit_commit(
&self,
request: MetadataWriteRequest,
deadline: MetadataDeadline,
) -> RuntimeResult<MetadataIoCommitAdmissionOutcome> {
match self.submit(request, deadline)? {
MetadataIoAdmissionOutcome::Accepted(receipt) => receipt
.wait_until_outcome(deadline)
.await
.map(MetadataIoCommitAdmissionOutcome::Completed),
MetadataIoAdmissionOutcome::TargetConflict(request) => {
Ok(MetadataIoCommitAdmissionOutcome::TargetConflict(request))
}
}
}
pub async fn submit_next_durable(
&self,
resource: impl Into<Arc<str>>,
target: impl Into<PathBuf>,
bytes: impl Into<Vec<u8>>,
deadline: MetadataDeadline,
) -> RuntimeResult<MetadataIoDurabilityOutcome> {
match self.submit_next(resource, target, bytes, deadline)? {
MetadataIoAdmissionOutcome::Accepted(receipt) => receipt
.wait_until(deadline)
.await
.map(MetadataIoDurabilityOutcome::Durable),
MetadataIoAdmissionOutcome::TargetConflict(request) => {
Ok(MetadataIoDurabilityOutcome::TargetConflict(request))
}
}
}
pub async fn submit_next_commit(
&self,
resource: impl Into<Arc<str>>,
target: impl Into<PathBuf>,
bytes: impl Into<Vec<u8>>,
deadline: MetadataDeadline,
) -> RuntimeResult<MetadataIoCommitAdmissionOutcome> {
match self.submit_next(resource, target, bytes, deadline)? {
MetadataIoAdmissionOutcome::Accepted(receipt) => receipt
.wait_until_outcome(deadline)
.await
.map(MetadataIoCommitAdmissionOutcome::Completed),
MetadataIoAdmissionOutcome::TargetConflict(request) => {
Ok(MetadataIoCommitAdmissionOutcome::TargetConflict(request))
}
}
}
pub async fn submit_observed(
&self,
request: MetadataWriteRequest,
deadline: MetadataDeadline,
) -> RuntimeResult<MetadataIoCommitObservation> {
match self.submit(request, deadline)? {
MetadataIoAdmissionOutcome::Accepted(receipt) => Ok(receipt.observe_until(deadline).await),
MetadataIoAdmissionOutcome::TargetConflict(request) => {
Ok(MetadataIoCommitObservation::TargetConflict(request))
}
}
}
pub async fn submit_next_observed(
&self,
resource: impl Into<Arc<str>>,
target: impl Into<PathBuf>,
bytes: impl Into<Vec<u8>>,
deadline: MetadataDeadline,
) -> RuntimeResult<MetadataIoCommitObservation> {
match self.submit_next(resource, target, bytes, deadline)? {
MetadataIoAdmissionOutcome::Accepted(receipt) => Ok(receipt.observe_until(deadline).await),
MetadataIoAdmissionOutcome::TargetConflict(request) => {
Ok(MetadataIoCommitObservation::TargetConflict(request))
}
}
}
#[must_use]
pub fn confirmed_durable_generation(&self, resource: &str) -> Option<MetadataGeneration> {
let state = self.inner.state.lock();
state
.resources
.get(resource)
.and_then(|resource_state| resource_state.durable_generation)
}
#[must_use]
pub async fn shutdown_until(&self, deadline: MetadataDeadline) -> MetadataIoShutdownReport {
self.stop_admission();
loop {
let finished_notification = self.inner.worker_finished.notified();
tokio::pin!(finished_notification);
finished_notification.as_mut().enable();
let finished = self.inner.state.lock().worker_finished;
if finished {
return shutdown_report(&self.inner, false);
}
if deadline.is_expired() {
return shutdown_report(&self.inner, true);
}
if tokio::time::timeout_at(deadline.instant(), finished_notification)
.await
.is_err()
{
return shutdown_report(&self.inner, true);
}
}
}
pub fn stop_admission(&self) {
stop_admission(&self.inner);
}
#[must_use]
pub fn snapshot(&self) -> MetadataIoSnapshot {
snapshot(&self.inner)
}
pub fn observer(&self) -> MetadataIoObserver {
MetadataIoObserver {
inner: Arc::downgrade(&self.inner),
}
}
pub fn target_identity(&self, resource: &str) -> Option<MetadataTargetIdentity> {
self.inner
.state
.lock()
.resources
.get(resource)
.and_then(|state| state.identity.clone())
}
pub fn retire_target_for_new_identity(&self, resource: &str) -> RuntimeResult<MetadataTargetRetirementOutcome> {
let mut state = self.inner.state.lock();
if state.accepting {
return Ok(MetadataTargetRetirementOutcome::AdmissionOpen);
}
if !state.worker_finished {
return Ok(MetadataTargetRetirementOutcome::WorkInProgress);
}
let Some(resource_state) = state.resources.get_mut(resource) else {
return Ok(MetadataTargetRetirementOutcome::NotFound);
};
if resource_state.in_flight.is_some() || resource_state.queued.is_some() || !resource_state.waiters.is_empty() {
return Ok(MetadataTargetRetirementOutcome::WorkInProgress);
}
let (Some(target), Some(identity)) = (&resource_state.target, &resource_state.identity) else {
return Ok(MetadataTargetRetirementOutcome::NotFound);
};
resource_state.target_registration.take();
let outcome = self
.inner
.targets
.retire(target, resource, identity, resource_state.durable_generation)?;
if outcome == MetadataTargetRetirementOutcome::Retired {
state.resources.remove(resource);
}
Ok(outcome)
}
#[must_use]
pub fn effective_profile(&self) -> MetadataIoEffectiveProfile {
effective_profile(
&self.inner.config,
&self.inner.blocking_policy,
self.inner.max_concurrent_writes,
)
}
}
fn retained_bytes_budget(process_budget: &ResourceBudget) -> RuntimeResult<ResourceBudget> {
let parent = process_budget.limit();
let limit = BudgetLimit {
capacity: parent.capacity,
control_reserve: BudgetCapacity::default(),
max_age: parent.max_age,
full_policy: FullPolicy::Reject,
};
process_budget.child("metadata-io", limit).map_err(|violation| {
RuntimeError::configuration_failure(crate::RuntimeOperation::AdmitMetadataBytes, violation)
})
}
fn ensure_target_registration(
state: &mut ActorState,
registry: &MetadataTargetRegistry,
request: &MetadataWriteRequest,
) -> RuntimeResult<Option<MetadataTargetRegistration>> {
let resource = &request.resource;
if !state.resources.contains_key(resource) && state.resources.len() >= registry.capacity() {
return Err(RuntimeError::capacity(crate::RuntimeOperation::AdmitMetadataOperation));
}
let mut target_changed = false;
if let Some(existing) = state.resources.get(resource) {
if let Some(target) = &existing.target {
target_changed = !registry.same_target(target.as_ref(), request.target.as_ref())?;
if target_changed && (existing.in_flight.is_some() || existing.queued.is_some()) {
return Ok(None);
}
if !target_changed {
if let Some(registration) = &existing.target_registration {
if registration.reconciliation_required() {
return Err(RuntimeError::context_unavailable(
crate::RuntimeOperation::MetadataResourceTarget,
));
}
return Ok(Some(registration.clone()));
}
}
}
}
let expected = state
.resources
.get(resource)
.filter(|_| !target_changed)
.and_then(|state| state.identity.as_ref());
let registration = match registry.register_with_identity(request.target.as_ref(), Arc::clone(resource), expected)? {
MetadataTargetRegistrationOutcome::Registered(registration) => registration,
MetadataTargetRegistrationOutcome::Conflict => return Ok(None),
MetadataTargetRegistrationOutcome::ReconciliationRequired => {
return Err(RuntimeError::context_unavailable(
crate::RuntimeOperation::MetadataResourceTarget,
));
}
};
let durable_generation = registration.durable_generation();
let resource_state = state.resources.entry(Arc::clone(resource)).or_default();
if target_changed {
resource_state.target_registration = None;
resource_state.durable_generation = None;
}
resource_state.target = Some(request.target.clone());
resource_state.identity = Some(registration.identity());
resource_state.target_registration = Some(registration.clone());
if let Some(durable_generation) = durable_generation {
resource_state.durable_generation = Some(
resource_state
.durable_generation
.map_or(durable_generation, |current| current.max(durable_generation)),
);
}
Ok(Some(registration))
}
async fn run_actor(
inner: Arc<ActorInner>,
mut receiver: mpsc::Receiver<Arc<str>>,
blocking: BlockingExecutor,
file_system: Arc<dyn MetadataFileSystem>,
cancellation: CancellationToken,
) {
let mut cancellation_observed = false;
let mut channel_closed = false;
let mut ready: VecDeque<Arc<str>> = VecDeque::new();
let mut writes = FuturesUnordered::new();
loop {
if !cancellation_observed && cancellation.is_cancelled() {
cancellation_observed = true;
stop_admission(&inner);
}
while writes.len() < inner.max_concurrent_writes {
let Some(resource) = ready.pop_front() else {
break;
};
writes.push(write_resource(&inner, &blocking, &file_system, resource));
}
if writes.is_empty() && should_finish(&inner) {
break;
}
tokio::select! {
biased;
_ = cancellation.cancelled(), if !cancellation_observed => {
cancellation_observed = true;
stop_admission(&inner);
}
Some((resource, has_more)) = writes.next(), if !writes.is_empty() => {
while let Ok(pending) = receiver.try_recv() {
ready.push_back(pending);
}
if has_more {
ready.push_back(resource);
}
}
_ = inner.shutdown.notified() => {}
resource = receiver.recv(), if !channel_closed => {
match resource {
Some(resource) => ready.push_back(resource),
None => {
channel_closed = true;
stop_admission(&inner);
}
}
}
}
}
finish_worker(&inner);
}
async fn write_resource(
inner: &Arc<ActorInner>,
blocking: &BlockingExecutor,
file_system: &Arc<dyn MetadataFileSystem>,
resource: Arc<str>,
) -> (Arc<str>, bool) {
let has_more = process_resource(inner, blocking, file_system, resource.clone()).await;
(resource, has_more)
}
async fn process_resource(
inner: &Arc<ActorInner>,
blocking: &BlockingExecutor,
file_system: &Arc<dyn MetadataFileSystem>,
resource: Arc<str>,
) -> bool {
let Some(QueuedMetadataWrite {
request,
registration,
charge,
}) = take_next_request(inner, &resource)
else {
return false;
};
let MetadataWriteRequest {
target,
bytes,
generation,
lane_deadline,
..
} = request;
let lane_deadline = lane_deadline.map(|deadline| ShutdownDeadline::at(deadline.instant().into_std()));
let worker_file_system = file_system.clone();
let completion_registration = registration.clone();
let snapshot = ChargedSnapshot { bytes, _charge: charge };
let result = match blocking
.submit_io_until(format!("metadata-io:{resource}"), lane_deadline, move || {
let snapshot = snapshot;
let result = worker_file_system.persist_atomic(&target, &snapshot.bytes);
match &result {
Ok(()) => {
let _ = completion_registration.record_durable(generation);
}
Err(error) if error.operation() == crate::RuntimeOperation::MetadataSyncParent => {
let _ = completion_registration.record_reconciliation_required();
}
Err(_) => {}
}
result
})
.await
{
Ok(mut task) => task.wait().await.and_then(|result| result),
Err(source) => Err(source),
};
drop(registration);
finish_request(inner, &resource, generation, result)
}
fn take_next_request(inner: &ActorInner, resource: &Arc<str>) -> Option<QueuedMetadataWrite> {
let mut state = inner.state.lock();
let resource_state = state.resources.get_mut(resource)?;
let queued = resource_state.queued.take()?;
resource_state.in_flight = Some(WorkMeta {
generation: queued.request.generation,
bytes: queued.request.len(),
});
Some(queued)
}
fn finish_request(
inner: &ActorInner,
resource: &Arc<str>,
generation: MetadataGeneration,
result: RuntimeResult<()>,
) -> bool {
let mut completed_waiters = Vec::new();
let (has_queued, outcome) = {
let mut state = inner.state.lock();
let Some(mut resource_state) = state.resources.remove(resource) else {
return false;
};
let Some(in_flight) = resource_state.in_flight.take() else {
state.resources.insert(resource.clone(), resource_state);
return false;
};
debug_assert_eq!(in_flight.generation, generation);
state.pending_operations = state.pending_operations.saturating_sub(1);
state.pending_bytes = state.pending_bytes.saturating_sub(in_flight.bytes);
let outcome = commit_outcome(generation, result);
if matches!(outcome, MetadataIoCommitOutcome::Durable(_)) {
resource_state.durable_generation = Some(
resource_state
.durable_generation
.map_or(generation, |durable| durable.max(generation)),
);
}
let mut retained = Vec::with_capacity(resource_state.waiters.len());
for waiter in resource_state.waiters.drain(..) {
if waiter.generation <= generation {
completed_waiters.push(waiter);
} else {
retained.push(waiter);
}
}
resource_state.waiters = retained;
let has_queued = resource_state.queued.is_some();
if !has_queued {
resource_state.target_registration = None;
}
state.resources.insert(resource.clone(), resource_state);
(has_queued, outcome)
};
for waiter in completed_waiters {
let _ = waiter.sender.send(Ok(outcome.clone()));
}
has_queued
}
fn commit_outcome(generation: MetadataGeneration, result: RuntimeResult<()>) -> MetadataIoCommitOutcome {
match result {
Ok(()) => MetadataIoCommitOutcome::Durable(generation),
Err(error) if error.operation() == crate::RuntimeOperation::MetadataSyncParent => {
MetadataIoCommitOutcome::CommitOutcomeUnknown(error)
}
Err(error) => MetadataIoCommitOutcome::FailedBeforeCommit(error),
}
}
fn stop_admission(inner: &ActorInner) {
let changed = {
let mut state = inner.state.lock();
let changed = state.accepting;
state.accepting = false;
changed
};
if changed {
inner.shutdown.notify_one();
}
}
fn should_finish(inner: &ActorInner) -> bool {
let state = inner.state.lock();
!state.accepting && state.pending_operations == 0
}
fn finish_worker(inner: &ActorInner) {
let mut abandoned = Vec::new();
{
let mut state = inner.state.lock();
state.accepting = false;
state.worker_finished = true;
for (resource, resource_state) in &mut state.resources {
for waiter in resource_state.waiters.drain(..) {
abandoned.push((resource.clone(), waiter));
}
}
}
for (_resource, waiter) in abandoned {
let _ = waiter.sender.send(Err(RuntimeError::closed(
crate::RuntimeOperation::MetadataWorkerStopped,
)));
}
inner.worker_finished.notify_waiters();
}
fn snapshot(inner: &ActorInner) -> MetadataIoSnapshot {
let state = inner.state.lock();
let mut resources = state
.resources
.iter()
.map(|(resource, resource_state)| resource_snapshot(resource, resource_state))
.collect::<Vec<_>>();
resources.sort_by(|left, right| left.resource.cmp(&right.resource));
MetadataIoSnapshot {
accepting: state.accepting,
pending_operations: state.pending_operations,
pending_bytes: state.pending_bytes,
max_pending_operations: inner.config.max_pending_operations,
max_pending_bytes: inner.config.max_pending_bytes,
resources,
}
}
fn shutdown_report(inner: &ActorInner, timed_out: bool) -> MetadataIoShutdownReport {
let snapshot = snapshot(inner);
let unfinished = snapshot
.resources
.into_iter()
.filter(|resource| resource.in_flight_generation.is_some() || resource.queued_generation.is_some())
.collect();
MetadataIoShutdownReport {
timed_out,
pending_operations: snapshot.pending_operations,
pending_bytes: snapshot.pending_bytes,
unfinished,
}
}
fn resource_snapshot(resource: &Arc<str>, state: &ResourceState) -> MetadataIoResourceSnapshot {
MetadataIoResourceSnapshot {
resource: resource.clone(),
target: state.target.clone(),
durable_generation: state.durable_generation,
in_flight_generation: state.in_flight.as_ref().map(|work| work.generation),
queued_generation: state.queued.as_ref().map(|queued| queued.request.generation),
waiter_count: state.waiters.len(),
}
}
fn startup_error(source: RuntimeError) -> RuntimeError {
source
}
#[cfg(test)]
mod tests {
use std::error::Error;
use super::*;
#[test]
fn metadata_io_config_rejects_unbounded_zero_limits() {
let zero_operations = MetadataIoConfig {
max_pending_operations: 0,
..MetadataIoConfig::default()
};
assert_eq!(
zero_operations.validate(),
Err(RuntimeContractViolation::InvalidMetadataConfiguration {
policy: RuntimeContractPolicy::MetadataMaxPendingOperationsPositive,
})
);
let zero_bytes = MetadataIoConfig {
max_pending_bytes: 0,
..MetadataIoConfig::default()
};
assert_eq!(
zero_bytes.validate(),
Err(RuntimeContractViolation::InvalidMetadataConfiguration {
policy: RuntimeContractPolicy::MetadataMaxPendingBytesPositive,
})
);
assert!(matches!(
MetadataIoConfig {
max_pending_operations: 0,
..MetadataIoConfig::default()
}
.into_plan(),
Err(RuntimeContractViolation::InvalidMetadataConfiguration {
policy: RuntimeContractPolicy::MetadataMaxPendingOperationsPositive,
})
));
}
#[test]
fn metadata_io_error_retains_the_original_io_source() {
let error = metadata_io_failure(
MetadataIoOperation::WriteTemporary,
Path::new("metadata.json"),
std::io::Error::new(std::io::ErrorKind::WriteZero, "partial write"),
);
let source = error.source().expect("metadata error should retain its source");
let source = source
.downcast_ref::<std::io::Error>()
.expect("metadata error source should remain a std::io::Error");
assert_eq!(source.kind(), std::io::ErrorKind::WriteZero);
}
#[test]
fn commit_observation_requires_reconciliation_only_for_unconfirmed_outcomes() {
let step_error = || {
RuntimeError::io(
crate::RuntimeOperation::MetadataSyncParent,
std::io::Error::other("injected failure"),
)
};
let confirmed = [
MetadataIoCommitObservation::Settled {
generation: MetadataGeneration::new(7),
outcome: MetadataIoCommitOutcome::Durable(MetadataGeneration::new(7)),
},
MetadataIoCommitObservation::Settled {
generation: MetadataGeneration::new(7),
outcome: MetadataIoCommitOutcome::FailedBeforeCommit(step_error()),
},
];
for observation in confirmed {
assert!(
!observation.requires_reconciliation(),
"a settled conclusion cannot require reconciliation"
);
assert_eq!(observation.unobserved_generation(), None);
assert_eq!(observation.generation(), Some(MetadataGeneration::new(7)));
}
let unconfirmed = MetadataIoCommitObservation::Settled {
generation: MetadataGeneration::new(7),
outcome: MetadataIoCommitOutcome::CommitOutcomeUnknown(step_error()),
};
assert!(unconfirmed.requires_reconciliation());
assert_eq!(unconfirmed.generation(), Some(MetadataGeneration::new(7)));
assert!(unconfirmed.settled().is_some());
let unobserved = MetadataIoCommitObservation::Unobserved {
generation: MetadataGeneration::new(9),
};
assert!(unobserved.requires_reconciliation());
assert_eq!(unobserved.unobserved_generation(), Some(MetadataGeneration::new(9)));
assert_eq!(unobserved.generation(), Some(MetadataGeneration::new(9)));
assert!(unobserved.settled().is_none());
}
#[test]
fn target_conflict_observation_is_not_a_durability_conclusion() {
let conflict = MetadataIoCommitObservation::TargetConflict(MetadataWriteRequest::new(
"resource",
MetadataGeneration::new(1),
Path::new("metadata.json"),
b"snapshot".to_vec(),
));
assert!(!conflict.requires_reconciliation());
assert_eq!(conflict.unobserved_generation(), None);
assert_eq!(conflict.generation(), None);
assert!(conflict.settled().is_none());
}
}