1mod filesystem;
30
31use std::collections::HashMap;
32use std::collections::VecDeque;
33use std::fmt;
34use std::num::NonZeroUsize;
35use std::path::Path;
36use std::path::PathBuf;
37use std::sync::atomic::AtomicUsize;
38use std::sync::atomic::Ordering;
39use std::sync::Arc;
40use std::sync::Weak;
41use std::time::Duration;
42
43use futures::stream::FuturesUnordered;
44use futures::StreamExt;
45use parking_lot::Mutex;
46use tokio::sync::mpsc;
47use tokio::sync::oneshot;
48use tokio::sync::Notify;
49use tokio_util::sync::CancellationToken;
50
51use crate::metadata_target::MetadataTargetIdentity;
52use crate::metadata_target::MetadataTargetRegistration;
53use crate::metadata_target::MetadataTargetRegistrationOutcome;
54use crate::metadata_target::MetadataTargetRegistry;
55use crate::metadata_target::MetadataTargetRetirementOutcome;
56use crate::resource_budget::BudgetCapacity;
57use crate::resource_budget::BudgetLimit;
58use crate::resource_budget::FullPolicy;
59use crate::resource_budget::ResourceBudget;
60use crate::resource_budget::ResourcePermit;
61use crate::shutdown_deadline::ShutdownDeadline;
62use crate::BlockingExecutor;
63use crate::BlockingPoolPolicy;
64use crate::ChildServiceContext;
65use crate::RuntimeContractPolicy;
66use crate::RuntimeContractViolation;
67use crate::RuntimeError;
68use crate::RuntimeResult;
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub struct MetadataDeadline {
73 at: tokio::time::Instant,
74}
75
76impl MetadataDeadline {
77 #[must_use]
79 pub fn after(timeout: Duration) -> Self {
80 let now = tokio::time::Instant::now();
81 Self {
82 at: now.checked_add(timeout).unwrap_or(now),
83 }
84 }
85
86 #[must_use]
88 pub const fn at(at: tokio::time::Instant) -> Self {
89 Self { at }
90 }
91
92 #[must_use]
94 pub const fn instant(self) -> tokio::time::Instant {
95 self.at
96 }
97
98 #[must_use]
100 pub fn remaining(self) -> Duration {
101 self.at.saturating_duration_since(tokio::time::Instant::now())
102 }
103
104 #[must_use]
106 pub fn is_expired(self) -> bool {
107 tokio::time::Instant::now() >= self.at
108 }
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
113pub struct MetadataGeneration(u64);
114
115impl MetadataGeneration {
116 #[must_use]
118 pub const fn new(value: u64) -> Self {
119 Self(value)
120 }
121
122 #[must_use]
124 pub const fn get(self) -> u64 {
125 self.0
126 }
127}
128
129impl From<u64> for MetadataGeneration {
130 fn from(value: u64) -> Self {
131 Self::new(value)
132 }
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137pub enum MetadataDurability {
138 Accepted,
140 Durable(MetadataGeneration),
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub enum MetadataIoOperation {
147 CreateParent,
149 CreateTemporary,
151 WriteTemporary,
153 SyncTemporary,
155 ReplaceTarget,
157 SyncParent,
159 RemoveTemporary,
161}
162
163impl fmt::Display for MetadataIoOperation {
164 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
165 let value = match self {
166 Self::CreateParent => "create parent directory",
167 Self::CreateTemporary => "create temporary file",
168 Self::WriteTemporary => "write temporary file",
169 Self::SyncTemporary => "sync temporary file",
170 Self::ReplaceTarget => "replace target file",
171 Self::SyncParent => "sync parent directory",
172 Self::RemoveTemporary => "remove temporary file",
173 };
174 formatter.write_str(value)
175 }
176}
177
178impl MetadataIoOperation {
179 #[must_use]
181 pub const fn runtime_operation(self) -> crate::RuntimeOperation {
182 match self {
183 Self::CreateParent => crate::RuntimeOperation::MetadataCreateParent,
184 Self::CreateTemporary => crate::RuntimeOperation::MetadataCreateTemporary,
185 Self::WriteTemporary => crate::RuntimeOperation::MetadataWriteTemporary,
186 Self::SyncTemporary => crate::RuntimeOperation::MetadataSyncTemporary,
187 Self::ReplaceTarget => crate::RuntimeOperation::MetadataReplaceTarget,
188 Self::SyncParent => crate::RuntimeOperation::MetadataSyncParent,
189 Self::RemoveTemporary => crate::RuntimeOperation::MetadataRemoveTemporary,
190 }
191 }
192}
193
194fn metadata_io_failure(operation: MetadataIoOperation, _path: &Path, source: std::io::Error) -> RuntimeError {
195 RuntimeError::io(operation.runtime_operation(), source)
196}
197
198#[derive(Debug, Clone)]
200pub struct MetadataWriteRequest {
201 resource: Arc<str>,
202 generation: MetadataGeneration,
203 target: Arc<Path>,
204 bytes: Arc<[u8]>,
205 lane_deadline: Option<MetadataDeadline>,
206}
207
208impl MetadataWriteRequest {
209 #[must_use]
211 pub fn new(
212 resource: impl Into<Arc<str>>,
213 generation: impl Into<MetadataGeneration>,
214 target: impl Into<PathBuf>,
215 bytes: impl Into<Vec<u8>>,
216 ) -> Self {
217 Self {
218 resource: resource.into(),
219 generation: generation.into(),
220 target: Arc::from(target.into()),
221 bytes: Arc::from(bytes.into()),
222 lane_deadline: None,
223 }
224 }
225
226 #[must_use]
235 pub fn with_lane_deadline(mut self, deadline: MetadataDeadline) -> Self {
236 self.lane_deadline = Some(deadline);
237 self
238 }
239
240 #[must_use]
242 pub const fn lane_deadline(&self) -> Option<MetadataDeadline> {
243 self.lane_deadline
244 }
245
246 #[must_use]
248 pub fn resource(&self) -> &str {
249 &self.resource
250 }
251
252 #[must_use]
254 pub const fn generation(&self) -> MetadataGeneration {
255 self.generation
256 }
257
258 #[must_use]
260 pub fn target(&self) -> &Path {
261 &self.target
262 }
263
264 #[must_use]
266 pub fn len(&self) -> usize {
267 self.bytes.len()
268 }
269
270 #[must_use]
272 pub fn is_empty(&self) -> bool {
273 self.bytes.is_empty()
274 }
275}
276
277pub trait MetadataFileSystem: fmt::Debug + Send + Sync + 'static {
284 fn persist_atomic(&self, target: &Path, bytes: &[u8]) -> RuntimeResult<()>;
291}
292
293#[derive(Debug, Default)]
295pub struct LocalMetadataFileSystem;
296
297impl MetadataFileSystem for LocalMetadataFileSystem {
298 fn persist_atomic(&self, target: &Path, bytes: &[u8]) -> RuntimeResult<()> {
299 filesystem::persist_atomic_local(target, bytes)
300 }
301}
302
303#[derive(Debug, Clone)]
312pub struct MetadataIoConfig {
313 pub max_pending_operations: usize,
315 pub max_pending_bytes: usize,
317 pub blocking_queue_timeout: Duration,
323 pub blocking_task_timeout: Duration,
329 pub blocking_warn_after: Duration,
334}
335
336#[derive(Debug, Clone)]
338pub struct MetadataIoPlan {
339 config: MetadataIoConfig,
340 max_concurrent_writes: NonZeroUsize,
341}
342
343impl MetadataIoConfig {
344 pub fn validate(&self) -> Result<(), RuntimeContractViolation> {
355 if self.max_pending_operations == 0 {
356 return Err(RuntimeContractViolation::InvalidMetadataConfiguration {
357 policy: RuntimeContractPolicy::MetadataMaxPendingOperationsPositive,
358 });
359 }
360 if self.max_pending_bytes == 0 {
361 return Err(RuntimeContractViolation::InvalidMetadataConfiguration {
362 policy: RuntimeContractPolicy::MetadataMaxPendingBytesPositive,
363 });
364 }
365 if self.blocking_task_timeout.is_zero() {
366 return Err(RuntimeContractViolation::InvalidMetadataConfiguration {
367 policy: RuntimeContractPolicy::MetadataBlockingTaskTimeoutPositive,
368 });
369 }
370 Ok(())
371 }
372
373 pub fn into_plan(self) -> Result<MetadataIoPlan, RuntimeContractViolation> {
380 self.validate()?;
381 Ok(MetadataIoPlan {
382 config: self,
383 max_concurrent_writes: NonZeroUsize::MIN,
384 })
385 }
386}
387
388impl MetadataIoPlan {
389 #[must_use]
399 pub fn with_max_concurrent_writes(mut self, writes: NonZeroUsize) -> Self {
400 self.max_concurrent_writes = writes;
401 self
402 }
403
404 pub fn start(self, service_context: &ChildServiceContext) -> RuntimeResult<MetadataIoActor> {
411 MetadataIoActor::start_validated(service_context, self, Arc::new(LocalMetadataFileSystem))
412 }
413
414 pub fn start_with_file_system(
421 self,
422 service_context: &ChildServiceContext,
423 file_system: Arc<dyn MetadataFileSystem>,
424 ) -> RuntimeResult<MetadataIoActor> {
425 MetadataIoActor::start_validated(service_context, self, file_system)
426 }
427}
428
429impl Default for MetadataIoConfig {
430 fn default() -> Self {
431 Self {
432 max_pending_operations: 1_024,
433 max_pending_bytes: 64 * 1024 * 1024,
434 blocking_queue_timeout: Duration::from_secs(5),
435 blocking_task_timeout: Duration::from_secs(30),
436 blocking_warn_after: Duration::from_secs(1),
437 }
438 }
439}
440
441#[derive(Debug, Clone, Copy, PartialEq, Eq)]
443pub enum MetadataLimitSource {
444 BlockingLanePolicy,
446 ActorConfiguration,
448}
449
450#[derive(Debug, Clone, PartialEq, Eq)]
452pub struct MetadataIoLegacyBlockingField {
453 pub field: &'static str,
455 pub configured: Duration,
457 pub effective: Duration,
459 pub effective_source: MetadataLimitSource,
461}
462
463#[derive(Debug, Clone, PartialEq, Eq)]
465pub struct MetadataIoBlockingLaneProfile {
466 pub name: String,
468 pub max_concurrency: usize,
470 pub max_queue_depth: usize,
472 pub queue_timeout: Duration,
474 pub task_timeout: Duration,
476 pub warn_after: Duration,
478}
479
480#[derive(Debug, Clone, PartialEq, Eq)]
482pub struct MetadataIoActorLimitsProfile {
483 pub max_pending_operations: usize,
485 pub max_pending_bytes: usize,
487 pub max_waiters: usize,
489 pub max_concurrent_writes: usize,
492}
493
494#[derive(Debug, Clone, PartialEq, Eq)]
506pub struct MetadataIoEffectiveProfile {
507 pub blocking_lane: MetadataIoBlockingLaneProfile,
509 pub actor_limits: MetadataIoActorLimitsProfile,
511 pub legacy_blocking_fields: [MetadataIoLegacyBlockingField; 3],
513}
514
515fn max_metadata_waiters(max_pending_operations: usize) -> usize {
517 max_pending_operations.saturating_mul(4).max(1)
518}
519
520fn effective_concurrent_writes(planned: NonZeroUsize, blocking_policy: &BlockingPoolPolicy) -> usize {
523 planned.get().min(blocking_policy.max_concurrency).max(1)
524}
525
526fn effective_profile(
527 config: &MetadataIoConfig,
528 blocking_policy: &BlockingPoolPolicy,
529 max_concurrent_writes: usize,
530) -> MetadataIoEffectiveProfile {
531 let legacy_blocking_field =
532 |field: &'static str, configured: Duration, effective: Duration| MetadataIoLegacyBlockingField {
533 field,
534 configured,
535 effective,
536 effective_source: MetadataLimitSource::BlockingLanePolicy,
537 };
538 MetadataIoEffectiveProfile {
539 blocking_lane: MetadataIoBlockingLaneProfile {
540 name: blocking_policy.name.clone(),
541 max_concurrency: blocking_policy.max_concurrency,
542 max_queue_depth: blocking_policy.max_queue_depth,
543 queue_timeout: blocking_policy.queue_timeout,
544 task_timeout: blocking_policy.task_timeout,
545 warn_after: blocking_policy.warn_after,
546 },
547 actor_limits: MetadataIoActorLimitsProfile {
548 max_pending_operations: config.max_pending_operations,
549 max_pending_bytes: config.max_pending_bytes,
550 max_waiters: max_metadata_waiters(config.max_pending_operations),
551 max_concurrent_writes,
552 },
553 legacy_blocking_fields: [
554 legacy_blocking_field(
555 "blocking_queue_timeout",
556 config.blocking_queue_timeout,
557 blocking_policy.queue_timeout,
558 ),
559 legacy_blocking_field(
560 "blocking_task_timeout",
561 config.blocking_task_timeout,
562 blocking_policy.task_timeout,
563 ),
564 legacy_blocking_field(
565 "blocking_warn_after",
566 config.blocking_warn_after,
567 blocking_policy.warn_after,
568 ),
569 ],
570 }
571}
572
573fn warn_legacy_blocking_fields(config: &MetadataIoConfig, blocking_policy: &BlockingPoolPolicy) {
580 let defaults = MetadataIoConfig::default();
581 let changed = [
582 (
583 "blocking_queue_timeout",
584 config.blocking_queue_timeout,
585 defaults.blocking_queue_timeout,
586 ),
587 (
588 "blocking_task_timeout",
589 config.blocking_task_timeout,
590 defaults.blocking_task_timeout,
591 ),
592 (
593 "blocking_warn_after",
594 config.blocking_warn_after,
595 defaults.blocking_warn_after,
596 ),
597 ]
598 .into_iter()
599 .filter(|(_field, configured, default)| configured != default)
600 .map(|(field, _configured, _default)| field)
601 .collect::<Vec<_>>();
602 if changed.is_empty() {
603 return;
604 }
605 tracing::warn!(
606 fields = ?changed,
607 lane = %blocking_policy.name,
608 "metadata blocking fields are retained for source compatibility and do not override the shared blocking lane policy"
609 );
610}
611
612#[derive(Debug, Clone, PartialEq, Eq)]
614pub struct MetadataIoResourceSnapshot {
615 pub resource: Arc<str>,
617 pub target: Option<Arc<Path>>,
619 pub durable_generation: Option<MetadataGeneration>,
621 pub in_flight_generation: Option<MetadataGeneration>,
623 pub queued_generation: Option<MetadataGeneration>,
625 pub waiter_count: usize,
627}
628
629#[derive(Debug, Clone, PartialEq, Eq)]
631pub struct MetadataIoSnapshot {
632 pub accepting: bool,
634 pub pending_operations: usize,
636 pub pending_bytes: usize,
638 pub max_pending_operations: usize,
640 pub max_pending_bytes: usize,
642 pub resources: Vec<MetadataIoResourceSnapshot>,
644}
645
646#[derive(Debug, Clone, PartialEq, Eq)]
648pub struct MetadataIoShutdownReport {
649 pub timed_out: bool,
651 pub pending_operations: usize,
653 pub pending_bytes: usize,
655 pub unfinished: Vec<MetadataIoResourceSnapshot>,
657}
658
659#[derive(Debug)]
661pub struct MetadataIoReceipt {
662 generation: MetadataGeneration,
663 identity: MetadataTargetIdentity,
664 durable: oneshot::Receiver<RuntimeResult<MetadataIoCommitOutcome>>,
665}
666
667#[derive(Debug)]
672pub enum MetadataIoAdmissionOutcome {
673 Accepted(MetadataIoReceipt),
675 TargetConflict(MetadataWriteRequest),
677}
678
679#[derive(Debug)]
681pub enum MetadataIoDurabilityOutcome {
682 Durable(MetadataGeneration),
684 TargetConflict(MetadataWriteRequest),
687}
688
689#[derive(Debug, Clone)]
691pub enum MetadataIoCommitOutcome {
692 Durable(MetadataGeneration),
695 FailedBeforeCommit(RuntimeError),
697 CommitOutcomeUnknown(RuntimeError),
700}
701
702#[derive(Debug)]
704pub enum MetadataIoCommitAdmissionOutcome {
705 Completed(MetadataIoCommitOutcome),
707 TargetConflict(MetadataWriteRequest),
710}
711
712#[derive(Debug)]
726pub enum MetadataIoCommitObservation {
727 Settled {
730 generation: MetadataGeneration,
732 outcome: MetadataIoCommitOutcome,
734 },
735 Unobserved {
738 generation: MetadataGeneration,
740 },
741 TargetConflict(MetadataWriteRequest),
744}
745
746impl MetadataIoCommitObservation {
747 #[must_use]
753 pub fn requires_reconciliation(&self) -> bool {
754 match self {
755 MetadataIoCommitObservation::Settled { outcome, .. } => {
756 matches!(outcome, MetadataIoCommitOutcome::CommitOutcomeUnknown(_))
757 }
758 MetadataIoCommitObservation::Unobserved { .. } => true,
759 MetadataIoCommitObservation::TargetConflict(_) => false,
760 }
761 }
762
763 #[must_use]
765 pub fn settled(self) -> Option<MetadataIoCommitOutcome> {
766 match self {
767 MetadataIoCommitObservation::Settled { outcome, .. } => Some(outcome),
768 MetadataIoCommitObservation::Unobserved { .. } | MetadataIoCommitObservation::TargetConflict(_) => None,
769 }
770 }
771
772 #[must_use]
774 pub const fn generation(&self) -> Option<MetadataGeneration> {
775 match self {
776 MetadataIoCommitObservation::Settled { generation, .. }
777 | MetadataIoCommitObservation::Unobserved { generation } => Some(*generation),
778 MetadataIoCommitObservation::TargetConflict(_) => None,
779 }
780 }
781
782 #[must_use]
784 pub const fn unobserved_generation(&self) -> Option<MetadataGeneration> {
785 match self {
786 MetadataIoCommitObservation::Unobserved { generation } => Some(*generation),
787 MetadataIoCommitObservation::Settled { .. } | MetadataIoCommitObservation::TargetConflict(_) => None,
788 }
789 }
790}
791
792enum ReceiptConclusion {
800 Delivered(RuntimeResult<MetadataIoCommitOutcome>),
804 Expired,
806 CoordinatorStopped,
809}
810
811impl MetadataIoReceipt {
812 pub fn target_identity(&self) -> MetadataTargetIdentity {
816 self.identity.clone()
817 }
818 #[must_use]
820 pub const fn generation(&self) -> MetadataGeneration {
821 self.generation
822 }
823
824 pub async fn wait_until_outcome(self, deadline: MetadataDeadline) -> RuntimeResult<MetadataIoCommitOutcome> {
834 if deadline.is_expired() {
835 return Err(RuntimeError::timed_out(crate::RuntimeOperation::WaitForDurableMetadata));
836 }
837 match self.conclude(deadline).await {
838 ReceiptConclusion::Delivered(result) => result,
839 ReceiptConclusion::CoordinatorStopped => {
840 Err(RuntimeError::closed(crate::RuntimeOperation::MetadataWorkerStopped))
841 }
842 ReceiptConclusion::Expired => Err(RuntimeError::timed_out(crate::RuntimeOperation::WaitForDurableMetadata)),
843 }
844 }
845
846 pub async fn observe_until(self, deadline: MetadataDeadline) -> MetadataIoCommitObservation {
859 let generation = self.generation;
860 match self.conclude(deadline).await {
861 ReceiptConclusion::Delivered(Ok(outcome)) => MetadataIoCommitObservation::Settled { generation, outcome },
862 ReceiptConclusion::Delivered(Err(error)) => MetadataIoCommitObservation::Settled {
866 generation,
867 outcome: MetadataIoCommitOutcome::FailedBeforeCommit(error),
868 },
869 ReceiptConclusion::CoordinatorStopped | ReceiptConclusion::Expired => {
873 MetadataIoCommitObservation::Unobserved { generation }
874 }
875 }
876 }
877
878 async fn conclude(self, deadline: MetadataDeadline) -> ReceiptConclusion {
880 match tokio::time::timeout_at(deadline.instant(), self.durable).await {
881 Ok(Ok(result)) => ReceiptConclusion::Delivered(result),
882 Ok(Err(_closed)) => ReceiptConclusion::CoordinatorStopped,
883 Err(_elapsed) => ReceiptConclusion::Expired,
884 }
885 }
886
887 pub async fn wait_until(self, deadline: MetadataDeadline) -> RuntimeResult<MetadataGeneration> {
897 match self.wait_until_outcome(deadline).await? {
898 MetadataIoCommitOutcome::Durable(generation) => Ok(generation),
899 MetadataIoCommitOutcome::FailedBeforeCommit(source)
900 | MetadataIoCommitOutcome::CommitOutcomeUnknown(source) => Err(source),
901 }
902 }
903}
904
905#[derive(Debug, Clone)]
907pub struct MetadataIoActor {
908 inner: Arc<ActorInner>,
909 sender: mpsc::Sender<Arc<str>>,
910}
911
912#[derive(Debug, Clone)]
914pub struct MetadataIoObserver {
915 inner: Weak<ActorInner>,
916}
917
918impl MetadataIoObserver {
919 pub fn snapshot(&self) -> Option<MetadataIoSnapshot> {
923 self.inner.upgrade().map(|inner| snapshot(&inner))
924 }
925}
926
927#[derive(Debug)]
928struct ActorInner {
929 config: MetadataIoConfig,
930 blocking_policy: BlockingPoolPolicy,
931 max_concurrent_writes: usize,
932 retained_bytes: ResourceBudget,
934 targets: MetadataTargetRegistry,
935 waiter_count: Arc<AtomicUsize>,
936 state: Mutex<ActorState>,
937 shutdown: Notify,
938 worker_finished: Notify,
939}
940
941#[derive(Debug)]
942struct ActorState {
943 accepting: bool,
944 worker_finished: bool,
945 pending_operations: usize,
946 pending_bytes: usize,
947 resources: HashMap<Arc<str>, ResourceState>,
948}
949
950#[derive(Debug, Default)]
951struct ResourceState {
952 identity: Option<MetadataTargetIdentity>,
953 target: Option<Arc<Path>>,
954 target_registration: Option<MetadataTargetRegistration>,
955 durable_generation: Option<MetadataGeneration>,
956 in_flight: Option<WorkMeta>,
957 queued: Option<QueuedMetadataWrite>,
958 waiters: Vec<GenerationWaiter>,
959}
960
961#[derive(Debug)]
962struct QueuedMetadataWrite {
963 request: MetadataWriteRequest,
964 registration: MetadataTargetRegistration,
965 charge: ResourcePermit,
968}
969
970#[derive(Debug)]
971struct WorkMeta {
972 generation: MetadataGeneration,
973 bytes: usize,
974}
975
976struct ChargedSnapshot {
979 bytes: Arc<[u8]>,
980 _charge: ResourcePermit,
981}
982
983#[derive(Debug)]
984struct GenerationWaiter {
985 generation: MetadataGeneration,
986 sender: oneshot::Sender<RuntimeResult<MetadataIoCommitOutcome>>,
987 _permit: WaiterPermit,
988}
989
990#[derive(Debug)]
991struct WaiterPermit {
992 waiter_count: Arc<AtomicUsize>,
993}
994
995impl Drop for WaiterPermit {
996 fn drop(&mut self) {
997 self.waiter_count.fetch_sub(1, Ordering::AcqRel);
998 }
999}
1000
1001impl ActorInner {
1002 fn reserve_waiter(&self, resource_waiters: usize) -> RuntimeResult<WaiterPermit> {
1003 let max_waiters = max_metadata_waiters(self.config.max_pending_operations);
1004 if resource_waiters >= max_waiters {
1005 return Err(RuntimeError::capacity(crate::RuntimeOperation::AdmitMetadataOperation));
1006 }
1007
1008 let mut current = self.waiter_count.load(Ordering::Acquire);
1009 loop {
1010 if current >= max_waiters {
1011 return Err(RuntimeError::capacity(crate::RuntimeOperation::AdmitMetadataOperation));
1012 }
1013 match self
1014 .waiter_count
1015 .compare_exchange_weak(current, current + 1, Ordering::AcqRel, Ordering::Acquire)
1016 {
1017 Ok(_) => {
1018 return Ok(WaiterPermit {
1019 waiter_count: Arc::clone(&self.waiter_count),
1020 });
1021 }
1022 Err(observed) => current = observed,
1023 }
1024 }
1025 }
1026}
1027
1028impl MetadataIoActor {
1029 fn start_validated(
1030 service_context: &ChildServiceContext,
1031 plan: MetadataIoPlan,
1032 file_system: Arc<dyn MetadataFileSystem>,
1033 ) -> RuntimeResult<Self> {
1034 let MetadataIoPlan {
1035 config,
1036 max_concurrent_writes,
1037 } = plan;
1038 let task_group = service_context.component("metadata-io").task_group().clone();
1039 let blocking = service_context.metadata_io().clone();
1040 let blocking_policy = blocking.policy().clone();
1041 warn_legacy_blocking_fields(&config, &blocking_policy);
1042 let max_concurrent_writes = effective_concurrent_writes(max_concurrent_writes, &blocking_policy);
1043 let retained_bytes = retained_bytes_budget(&service_context.process_budget())?;
1044 let targets = service_context.resources().metadata_targets();
1045 let (sender, receiver) = mpsc::channel(config.max_pending_operations);
1046 let inner = Arc::new(ActorInner {
1047 config,
1048 blocking_policy,
1049 max_concurrent_writes,
1050 retained_bytes,
1051 targets,
1052 waiter_count: Arc::new(AtomicUsize::new(0)),
1053 state: Mutex::new(ActorState {
1054 accepting: true,
1055 worker_finished: false,
1056 pending_operations: 0,
1057 pending_bytes: 0,
1058 resources: HashMap::new(),
1059 }),
1060 shutdown: Notify::new(),
1061 worker_finished: Notify::new(),
1062 });
1063 let actor = Self {
1064 inner: inner.clone(),
1065 sender,
1066 };
1067 let cancellation = task_group.cancellation_token();
1068 task_group
1069 .spawn_service(
1070 "metadata-io.coordinator",
1071 run_actor(inner, receiver, blocking, file_system, cancellation),
1072 )
1073 .map_err(startup_error)?;
1074 Ok(actor)
1075 }
1076
1077 pub fn submit(
1090 &self,
1091 request: MetadataWriteRequest,
1092 deadline: MetadataDeadline,
1093 ) -> RuntimeResult<MetadataIoAdmissionOutcome> {
1094 if deadline.is_expired() {
1095 return Err(RuntimeError::timed_out(crate::RuntimeOperation::AdmitMetadataSnapshot));
1096 }
1097
1098 let resource = request.resource.clone();
1099 let generation = request.generation;
1100 let (waiter_sender, durable) = oneshot::channel();
1101 let mut state = self.inner.state.lock();
1102 if !state.accepting {
1103 return Err(RuntimeError::closed(crate::RuntimeOperation::MetadataIoClosed));
1104 }
1105
1106 let Some(target_registration) = ensure_target_registration(&mut state, &self.inner.targets, &request)? else {
1107 return Ok(MetadataIoAdmissionOutcome::TargetConflict(request));
1108 };
1109
1110 let existing = state.resources.get(&resource);
1111 if let Some(existing) = existing {
1112 if let Some(durable_generation) = existing.durable_generation {
1113 if generation <= durable_generation {
1114 let _ = waiter_sender.send(Ok(MetadataIoCommitOutcome::Durable(durable_generation)));
1115 return Ok(MetadataIoAdmissionOutcome::Accepted(MetadataIoReceipt {
1116 generation,
1117 identity: target_registration.identity(),
1118 durable,
1119 }));
1120 }
1121 }
1122 if let Some(in_flight) = &existing.in_flight {
1123 if generation <= in_flight.generation {
1124 let waiter_permit = self.inner.reserve_waiter(existing.waiters.len())?;
1125 let Some(resource_state) = state.resources.get_mut(&resource) else {
1126 return Err(RuntimeError::closed(crate::RuntimeOperation::MetadataWorkerStopped));
1127 };
1128 resource_state.waiters.push(GenerationWaiter {
1129 generation,
1130 sender: waiter_sender,
1131 _permit: waiter_permit,
1132 });
1133 return Ok(MetadataIoAdmissionOutcome::Accepted(MetadataIoReceipt {
1134 generation,
1135 identity: target_registration.identity(),
1136 durable,
1137 }));
1138 }
1139 }
1140 if let Some(queued) = &existing.queued {
1141 if generation <= queued.request.generation {
1142 let waiter_permit = self.inner.reserve_waiter(existing.waiters.len())?;
1143 let Some(resource_state) = state.resources.get_mut(&resource) else {
1144 return Err(RuntimeError::closed(crate::RuntimeOperation::MetadataWorkerStopped));
1145 };
1146 resource_state.waiters.push(GenerationWaiter {
1147 generation,
1148 sender: waiter_sender,
1149 _permit: waiter_permit,
1150 });
1151 return Ok(MetadataIoAdmissionOutcome::Accepted(MetadataIoReceipt {
1152 generation,
1153 identity: target_registration.identity(),
1154 durable,
1155 }));
1156 }
1157 }
1158 }
1159
1160 let waiter_permit = self
1161 .inner
1162 .reserve_waiter(existing.map_or(0, |resource_state| resource_state.waiters.len()))?;
1163 let old_queued_bytes = existing
1164 .and_then(|resource_state| resource_state.queued.as_ref())
1165 .map_or(0, |queued| queued.request.len());
1166 let adds_operation = existing.is_none_or(|resource_state| resource_state.queued.is_none());
1167 let needs_queue_token =
1168 existing.is_none_or(|resource_state| resource_state.in_flight.is_none() && resource_state.queued.is_none());
1169 let next_operations = state.pending_operations + usize::from(adds_operation);
1170 if next_operations > self.inner.config.max_pending_operations {
1171 return Err(RuntimeError::capacity(crate::RuntimeOperation::AdmitMetadataOperation));
1172 }
1173 let retained_without_replaced = state.pending_bytes.saturating_sub(old_queued_bytes);
1174 let next_bytes = retained_without_replaced
1175 .checked_add(request.len())
1176 .ok_or_else(|| RuntimeError::capacity(crate::RuntimeOperation::AdmitMetadataBytes))?;
1177 if next_bytes > self.inner.config.max_pending_bytes {
1178 return Err(RuntimeError::capacity(crate::RuntimeOperation::AdmitMetadataBytes));
1179 }
1180 let queue_permit = if needs_queue_token {
1181 Some(
1182 self.sender
1183 .try_reserve()
1184 .map_err(|_error| RuntimeError::capacity(crate::RuntimeOperation::AdmitMetadataOperation))?,
1185 )
1186 } else {
1187 None
1188 };
1189
1190 let resource_state = state.resources.entry(resource.clone()).or_default();
1191 if let Some(queued) = &mut resource_state.queued {
1192 let new_bytes = request.len();
1193 queued
1196 .charge
1197 .try_resize(new_bytes.max(queued.charge.bytes()))
1198 .map_err(|_rejection| RuntimeError::capacity(crate::RuntimeOperation::AdmitMetadataBytes))?;
1199 queued.request = request;
1200 queued.charge.shrink_to(new_bytes);
1201 queued.registration = target_registration.clone();
1202 } else {
1203 let charge = self
1204 .inner
1205 .retained_bytes
1206 .try_acquire_data(request.len())
1207 .map_err(|_rejection| RuntimeError::capacity(crate::RuntimeOperation::AdmitMetadataBytes))?;
1208 resource_state.queued = Some(QueuedMetadataWrite {
1209 request,
1210 registration: target_registration.clone(),
1211 charge,
1212 });
1213 }
1214 let identity = target_registration.identity();
1215 resource_state.target = resource_state
1216 .queued
1217 .as_ref()
1218 .map(|queued| queued.request.target.clone());
1219 resource_state.target_registration = Some(target_registration.clone());
1220 resource_state.waiters.push(GenerationWaiter {
1221 generation,
1222 sender: waiter_sender,
1223 _permit: waiter_permit,
1224 });
1225 state.pending_operations = next_operations;
1226 state.pending_bytes = next_bytes;
1227 drop(state);
1228 if let Some(permit) = queue_permit {
1229 permit.send(resource.clone());
1230 }
1231 Ok(MetadataIoAdmissionOutcome::Accepted(MetadataIoReceipt {
1232 generation,
1233 identity,
1234 durable,
1235 }))
1236 }
1237
1238 pub fn submit_next(
1250 &self,
1251 resource: impl Into<Arc<str>>,
1252 target: impl Into<PathBuf>,
1253 bytes: impl Into<Vec<u8>>,
1254 deadline: MetadataDeadline,
1255 ) -> RuntimeResult<MetadataIoAdmissionOutcome> {
1256 let generation = self.inner.targets.next_generation();
1257 self.submit(MetadataWriteRequest::new(resource, generation, target, bytes), deadline)
1258 }
1259
1260 pub async fn submit_durable(
1270 &self,
1271 request: MetadataWriteRequest,
1272 deadline: MetadataDeadline,
1273 ) -> RuntimeResult<MetadataIoDurabilityOutcome> {
1274 match self.submit(request, deadline)? {
1275 MetadataIoAdmissionOutcome::Accepted(receipt) => receipt
1276 .wait_until(deadline)
1277 .await
1278 .map(MetadataIoDurabilityOutcome::Durable),
1279 MetadataIoAdmissionOutcome::TargetConflict(request) => {
1280 Ok(MetadataIoDurabilityOutcome::TargetConflict(request))
1281 }
1282 }
1283 }
1284
1285 pub async fn submit_commit(
1290 &self,
1291 request: MetadataWriteRequest,
1292 deadline: MetadataDeadline,
1293 ) -> RuntimeResult<MetadataIoCommitAdmissionOutcome> {
1294 match self.submit(request, deadline)? {
1295 MetadataIoAdmissionOutcome::Accepted(receipt) => receipt
1296 .wait_until_outcome(deadline)
1297 .await
1298 .map(MetadataIoCommitAdmissionOutcome::Completed),
1299 MetadataIoAdmissionOutcome::TargetConflict(request) => {
1300 Ok(MetadataIoCommitAdmissionOutcome::TargetConflict(request))
1301 }
1302 }
1303 }
1304
1305 pub async fn submit_next_durable(
1313 &self,
1314 resource: impl Into<Arc<str>>,
1315 target: impl Into<PathBuf>,
1316 bytes: impl Into<Vec<u8>>,
1317 deadline: MetadataDeadline,
1318 ) -> RuntimeResult<MetadataIoDurabilityOutcome> {
1319 match self.submit_next(resource, target, bytes, deadline)? {
1320 MetadataIoAdmissionOutcome::Accepted(receipt) => receipt
1321 .wait_until(deadline)
1322 .await
1323 .map(MetadataIoDurabilityOutcome::Durable),
1324 MetadataIoAdmissionOutcome::TargetConflict(request) => {
1325 Ok(MetadataIoDurabilityOutcome::TargetConflict(request))
1326 }
1327 }
1328 }
1329
1330 pub async fn submit_next_commit(
1333 &self,
1334 resource: impl Into<Arc<str>>,
1335 target: impl Into<PathBuf>,
1336 bytes: impl Into<Vec<u8>>,
1337 deadline: MetadataDeadline,
1338 ) -> RuntimeResult<MetadataIoCommitAdmissionOutcome> {
1339 match self.submit_next(resource, target, bytes, deadline)? {
1340 MetadataIoAdmissionOutcome::Accepted(receipt) => receipt
1341 .wait_until_outcome(deadline)
1342 .await
1343 .map(MetadataIoCommitAdmissionOutcome::Completed),
1344 MetadataIoAdmissionOutcome::TargetConflict(request) => {
1345 Ok(MetadataIoCommitAdmissionOutcome::TargetConflict(request))
1346 }
1347 }
1348 }
1349
1350 pub async fn submit_observed(
1362 &self,
1363 request: MetadataWriteRequest,
1364 deadline: MetadataDeadline,
1365 ) -> RuntimeResult<MetadataIoCommitObservation> {
1366 match self.submit(request, deadline)? {
1367 MetadataIoAdmissionOutcome::Accepted(receipt) => Ok(receipt.observe_until(deadline).await),
1368 MetadataIoAdmissionOutcome::TargetConflict(request) => {
1369 Ok(MetadataIoCommitObservation::TargetConflict(request))
1370 }
1371 }
1372 }
1373
1374 pub async fn submit_next_observed(
1382 &self,
1383 resource: impl Into<Arc<str>>,
1384 target: impl Into<PathBuf>,
1385 bytes: impl Into<Vec<u8>>,
1386 deadline: MetadataDeadline,
1387 ) -> RuntimeResult<MetadataIoCommitObservation> {
1388 match self.submit_next(resource, target, bytes, deadline)? {
1389 MetadataIoAdmissionOutcome::Accepted(receipt) => Ok(receipt.observe_until(deadline).await),
1390 MetadataIoAdmissionOutcome::TargetConflict(request) => {
1391 Ok(MetadataIoCommitObservation::TargetConflict(request))
1392 }
1393 }
1394 }
1395
1396 #[must_use]
1408 pub fn confirmed_durable_generation(&self, resource: &str) -> Option<MetadataGeneration> {
1409 let state = self.inner.state.lock();
1410 state
1411 .resources
1412 .get(resource)
1413 .and_then(|resource_state| resource_state.durable_generation)
1414 }
1415
1416 #[must_use]
1419 pub async fn shutdown_until(&self, deadline: MetadataDeadline) -> MetadataIoShutdownReport {
1420 self.stop_admission();
1421 loop {
1422 let finished_notification = self.inner.worker_finished.notified();
1423 tokio::pin!(finished_notification);
1424 finished_notification.as_mut().enable();
1425 let finished = self.inner.state.lock().worker_finished;
1426 if finished {
1427 return shutdown_report(&self.inner, false);
1428 }
1429 if deadline.is_expired() {
1430 return shutdown_report(&self.inner, true);
1431 }
1432 if tokio::time::timeout_at(deadline.instant(), finished_notification)
1433 .await
1434 .is_err()
1435 {
1436 return shutdown_report(&self.inner, true);
1437 }
1438 }
1439 }
1440
1441 pub fn stop_admission(&self) {
1443 stop_admission(&self.inner);
1444 }
1445
1446 #[must_use]
1448 pub fn snapshot(&self) -> MetadataIoSnapshot {
1449 snapshot(&self.inner)
1450 }
1451
1452 pub fn observer(&self) -> MetadataIoObserver {
1454 MetadataIoObserver {
1455 inner: Arc::downgrade(&self.inner),
1456 }
1457 }
1458
1459 pub fn target_identity(&self, resource: &str) -> Option<MetadataTargetIdentity> {
1461 self.inner
1462 .state
1463 .lock()
1464 .resources
1465 .get(resource)
1466 .and_then(|state| state.identity.clone())
1467 }
1468
1469 pub fn retire_target_for_new_identity(&self, resource: &str) -> RuntimeResult<MetadataTargetRetirementOutcome> {
1484 let mut state = self.inner.state.lock();
1485 if state.accepting {
1486 return Ok(MetadataTargetRetirementOutcome::AdmissionOpen);
1487 }
1488 if !state.worker_finished {
1489 return Ok(MetadataTargetRetirementOutcome::WorkInProgress);
1490 }
1491 let Some(resource_state) = state.resources.get_mut(resource) else {
1492 return Ok(MetadataTargetRetirementOutcome::NotFound);
1493 };
1494 if resource_state.in_flight.is_some() || resource_state.queued.is_some() || !resource_state.waiters.is_empty() {
1495 return Ok(MetadataTargetRetirementOutcome::WorkInProgress);
1496 }
1497 let (Some(target), Some(identity)) = (&resource_state.target, &resource_state.identity) else {
1498 return Ok(MetadataTargetRetirementOutcome::NotFound);
1499 };
1500 resource_state.target_registration.take();
1503 let outcome = self
1504 .inner
1505 .targets
1506 .retire(target, resource, identity, resource_state.durable_generation)?;
1507 if outcome == MetadataTargetRetirementOutcome::Retired {
1508 state.resources.remove(resource);
1509 }
1510 Ok(outcome)
1511 }
1512
1513 #[must_use]
1519 pub fn effective_profile(&self) -> MetadataIoEffectiveProfile {
1520 effective_profile(
1521 &self.inner.config,
1522 &self.inner.blocking_policy,
1523 self.inner.max_concurrent_writes,
1524 )
1525 }
1526}
1527
1528fn retained_bytes_budget(process_budget: &ResourceBudget) -> RuntimeResult<ResourceBudget> {
1536 let parent = process_budget.limit();
1537 let limit = BudgetLimit {
1538 capacity: parent.capacity,
1539 control_reserve: BudgetCapacity::default(),
1540 max_age: parent.max_age,
1541 full_policy: FullPolicy::Reject,
1542 };
1543 process_budget.child("metadata-io", limit).map_err(|violation| {
1544 RuntimeError::configuration_failure(crate::RuntimeOperation::AdmitMetadataBytes, violation)
1545 })
1546}
1547
1548fn ensure_target_registration(
1549 state: &mut ActorState,
1550 registry: &MetadataTargetRegistry,
1551 request: &MetadataWriteRequest,
1552) -> RuntimeResult<Option<MetadataTargetRegistration>> {
1553 let resource = &request.resource;
1554 if !state.resources.contains_key(resource) && state.resources.len() >= registry.capacity() {
1557 return Err(RuntimeError::capacity(crate::RuntimeOperation::AdmitMetadataOperation));
1558 }
1559 let mut target_changed = false;
1560 if let Some(existing) = state.resources.get(resource) {
1561 if let Some(target) = &existing.target {
1562 target_changed = !registry.same_target(target.as_ref(), request.target.as_ref())?;
1563 if target_changed && (existing.in_flight.is_some() || existing.queued.is_some()) {
1564 return Ok(None);
1565 }
1566 if !target_changed {
1567 if let Some(registration) = &existing.target_registration {
1568 if registration.reconciliation_required() {
1574 return Err(RuntimeError::context_unavailable(
1575 crate::RuntimeOperation::MetadataResourceTarget,
1576 ));
1577 }
1578 return Ok(Some(registration.clone()));
1579 }
1580 }
1581 }
1582 }
1583
1584 let expected = state
1585 .resources
1586 .get(resource)
1587 .filter(|_| !target_changed)
1588 .and_then(|state| state.identity.as_ref());
1589 let registration = match registry.register_with_identity(request.target.as_ref(), Arc::clone(resource), expected)? {
1590 MetadataTargetRegistrationOutcome::Registered(registration) => registration,
1591 MetadataTargetRegistrationOutcome::Conflict => return Ok(None),
1592 MetadataTargetRegistrationOutcome::ReconciliationRequired => {
1593 return Err(RuntimeError::context_unavailable(
1594 crate::RuntimeOperation::MetadataResourceTarget,
1595 ));
1596 }
1597 };
1598 let durable_generation = registration.durable_generation();
1599 let resource_state = state.resources.entry(Arc::clone(resource)).or_default();
1600 if target_changed {
1601 resource_state.target_registration = None;
1602 resource_state.durable_generation = None;
1603 }
1604 resource_state.target = Some(request.target.clone());
1605 resource_state.identity = Some(registration.identity());
1606 resource_state.target_registration = Some(registration.clone());
1607 if let Some(durable_generation) = durable_generation {
1608 resource_state.durable_generation = Some(
1609 resource_state
1610 .durable_generation
1611 .map_or(durable_generation, |current| current.max(durable_generation)),
1612 );
1613 }
1614 Ok(Some(registration))
1615}
1616
1617async fn run_actor(
1618 inner: Arc<ActorInner>,
1619 mut receiver: mpsc::Receiver<Arc<str>>,
1620 blocking: BlockingExecutor,
1621 file_system: Arc<dyn MetadataFileSystem>,
1622 cancellation: CancellationToken,
1623) {
1624 let mut cancellation_observed = false;
1625 let mut channel_closed = false;
1626 let mut ready: VecDeque<Arc<str>> = VecDeque::new();
1630 let mut writes = FuturesUnordered::new();
1631 loop {
1632 if !cancellation_observed && cancellation.is_cancelled() {
1633 cancellation_observed = true;
1634 stop_admission(&inner);
1635 }
1636 while writes.len() < inner.max_concurrent_writes {
1637 let Some(resource) = ready.pop_front() else {
1638 break;
1639 };
1640 writes.push(write_resource(&inner, &blocking, &file_system, resource));
1641 }
1642 if writes.is_empty() && should_finish(&inner) {
1643 break;
1644 }
1645 tokio::select! {
1646 biased;
1647 _ = cancellation.cancelled(), if !cancellation_observed => {
1648 cancellation_observed = true;
1649 stop_admission(&inner);
1650 }
1651 Some((resource, has_more)) = writes.next(), if !writes.is_empty() => {
1652 while let Ok(pending) = receiver.try_recv() {
1655 ready.push_back(pending);
1656 }
1657 if has_more {
1658 ready.push_back(resource);
1659 }
1660 }
1661 _ = inner.shutdown.notified() => {}
1662 resource = receiver.recv(), if !channel_closed => {
1663 match resource {
1664 Some(resource) => ready.push_back(resource),
1665 None => {
1666 channel_closed = true;
1667 stop_admission(&inner);
1668 }
1669 }
1670 }
1671 }
1672 }
1673 finish_worker(&inner);
1674}
1675
1676async fn write_resource(
1679 inner: &Arc<ActorInner>,
1680 blocking: &BlockingExecutor,
1681 file_system: &Arc<dyn MetadataFileSystem>,
1682 resource: Arc<str>,
1683) -> (Arc<str>, bool) {
1684 let has_more = process_resource(inner, blocking, file_system, resource.clone()).await;
1685 (resource, has_more)
1686}
1687
1688async fn process_resource(
1689 inner: &Arc<ActorInner>,
1690 blocking: &BlockingExecutor,
1691 file_system: &Arc<dyn MetadataFileSystem>,
1692 resource: Arc<str>,
1693) -> bool {
1694 let Some(QueuedMetadataWrite {
1695 request,
1696 registration,
1697 charge,
1698 }) = take_next_request(inner, &resource)
1699 else {
1700 return false;
1701 };
1702 let MetadataWriteRequest {
1703 target,
1704 bytes,
1705 generation,
1706 lane_deadline,
1707 ..
1708 } = request;
1709 let lane_deadline = lane_deadline.map(|deadline| ShutdownDeadline::at(deadline.instant().into_std()));
1714 let worker_file_system = file_system.clone();
1715 let completion_registration = registration.clone();
1716 let snapshot = ChargedSnapshot { bytes, _charge: charge };
1717 let result = match blocking
1718 .submit_io_until(format!("metadata-io:{resource}"), lane_deadline, move || {
1719 let snapshot = snapshot;
1722 let result = worker_file_system.persist_atomic(&target, &snapshot.bytes);
1723 match &result {
1724 Ok(()) => {
1725 let _ = completion_registration.record_durable(generation);
1726 }
1727 Err(error) if error.operation() == crate::RuntimeOperation::MetadataSyncParent => {
1728 let _ = completion_registration.record_reconciliation_required();
1729 }
1730 Err(_) => {}
1731 }
1732 result
1733 })
1734 .await
1735 {
1736 Ok(mut task) => task.wait().await.and_then(|result| result),
1739 Err(source) => Err(source),
1740 };
1741 drop(registration);
1749 finish_request(inner, &resource, generation, result)
1750}
1751
1752fn take_next_request(inner: &ActorInner, resource: &Arc<str>) -> Option<QueuedMetadataWrite> {
1753 let mut state = inner.state.lock();
1754 let resource_state = state.resources.get_mut(resource)?;
1755 let queued = resource_state.queued.take()?;
1756 resource_state.in_flight = Some(WorkMeta {
1757 generation: queued.request.generation,
1758 bytes: queued.request.len(),
1759 });
1760 Some(queued)
1761}
1762
1763fn finish_request(
1764 inner: &ActorInner,
1765 resource: &Arc<str>,
1766 generation: MetadataGeneration,
1767 result: RuntimeResult<()>,
1768) -> bool {
1769 let mut completed_waiters = Vec::new();
1770 let (has_queued, outcome) = {
1771 let mut state = inner.state.lock();
1772 let Some(mut resource_state) = state.resources.remove(resource) else {
1773 return false;
1774 };
1775 let Some(in_flight) = resource_state.in_flight.take() else {
1776 state.resources.insert(resource.clone(), resource_state);
1777 return false;
1778 };
1779 debug_assert_eq!(in_flight.generation, generation);
1780 state.pending_operations = state.pending_operations.saturating_sub(1);
1781 state.pending_bytes = state.pending_bytes.saturating_sub(in_flight.bytes);
1782 let outcome = commit_outcome(generation, result);
1786 if matches!(outcome, MetadataIoCommitOutcome::Durable(_)) {
1787 resource_state.durable_generation = Some(
1788 resource_state
1789 .durable_generation
1790 .map_or(generation, |durable| durable.max(generation)),
1791 );
1792 }
1793 let mut retained = Vec::with_capacity(resource_state.waiters.len());
1794 for waiter in resource_state.waiters.drain(..) {
1795 if waiter.generation <= generation {
1796 completed_waiters.push(waiter);
1797 } else {
1798 retained.push(waiter);
1799 }
1800 }
1801 resource_state.waiters = retained;
1802 let has_queued = resource_state.queued.is_some();
1803 if !has_queued {
1804 resource_state.target_registration = None;
1805 }
1806 state.resources.insert(resource.clone(), resource_state);
1807 (has_queued, outcome)
1808 };
1809
1810 for waiter in completed_waiters {
1811 let _ = waiter.sender.send(Ok(outcome.clone()));
1812 }
1813 has_queued
1814}
1815
1816fn commit_outcome(generation: MetadataGeneration, result: RuntimeResult<()>) -> MetadataIoCommitOutcome {
1817 match result {
1818 Ok(()) => MetadataIoCommitOutcome::Durable(generation),
1819 Err(error) if error.operation() == crate::RuntimeOperation::MetadataSyncParent => {
1820 MetadataIoCommitOutcome::CommitOutcomeUnknown(error)
1821 }
1822 Err(error) => MetadataIoCommitOutcome::FailedBeforeCommit(error),
1823 }
1824}
1825
1826fn stop_admission(inner: &ActorInner) {
1827 let changed = {
1828 let mut state = inner.state.lock();
1829 let changed = state.accepting;
1830 state.accepting = false;
1831 changed
1832 };
1833 if changed {
1834 inner.shutdown.notify_one();
1835 }
1836}
1837
1838fn should_finish(inner: &ActorInner) -> bool {
1839 let state = inner.state.lock();
1840 !state.accepting && state.pending_operations == 0
1841}
1842
1843fn finish_worker(inner: &ActorInner) {
1844 let mut abandoned = Vec::new();
1845 {
1846 let mut state = inner.state.lock();
1847 state.accepting = false;
1848 state.worker_finished = true;
1849 for (resource, resource_state) in &mut state.resources {
1850 for waiter in resource_state.waiters.drain(..) {
1851 abandoned.push((resource.clone(), waiter));
1852 }
1853 }
1854 }
1855 for (_resource, waiter) in abandoned {
1856 let _ = waiter.sender.send(Err(RuntimeError::closed(
1857 crate::RuntimeOperation::MetadataWorkerStopped,
1858 )));
1859 }
1860 inner.worker_finished.notify_waiters();
1861}
1862
1863fn snapshot(inner: &ActorInner) -> MetadataIoSnapshot {
1864 let state = inner.state.lock();
1865 let mut resources = state
1866 .resources
1867 .iter()
1868 .map(|(resource, resource_state)| resource_snapshot(resource, resource_state))
1869 .collect::<Vec<_>>();
1870 resources.sort_by(|left, right| left.resource.cmp(&right.resource));
1871 MetadataIoSnapshot {
1872 accepting: state.accepting,
1873 pending_operations: state.pending_operations,
1874 pending_bytes: state.pending_bytes,
1875 max_pending_operations: inner.config.max_pending_operations,
1876 max_pending_bytes: inner.config.max_pending_bytes,
1877 resources,
1878 }
1879}
1880
1881fn shutdown_report(inner: &ActorInner, timed_out: bool) -> MetadataIoShutdownReport {
1882 let snapshot = snapshot(inner);
1883 let unfinished = snapshot
1884 .resources
1885 .into_iter()
1886 .filter(|resource| resource.in_flight_generation.is_some() || resource.queued_generation.is_some())
1887 .collect();
1888 MetadataIoShutdownReport {
1889 timed_out,
1890 pending_operations: snapshot.pending_operations,
1891 pending_bytes: snapshot.pending_bytes,
1892 unfinished,
1893 }
1894}
1895
1896fn resource_snapshot(resource: &Arc<str>, state: &ResourceState) -> MetadataIoResourceSnapshot {
1897 MetadataIoResourceSnapshot {
1898 resource: resource.clone(),
1899 target: state.target.clone(),
1900 durable_generation: state.durable_generation,
1901 in_flight_generation: state.in_flight.as_ref().map(|work| work.generation),
1902 queued_generation: state.queued.as_ref().map(|queued| queued.request.generation),
1903 waiter_count: state.waiters.len(),
1904 }
1905}
1906
1907fn startup_error(source: RuntimeError) -> RuntimeError {
1908 source
1909}
1910
1911#[cfg(test)]
1912mod tests {
1913 use std::error::Error;
1914
1915 use super::*;
1916
1917 #[test]
1918 fn metadata_io_config_rejects_unbounded_zero_limits() {
1919 let zero_operations = MetadataIoConfig {
1920 max_pending_operations: 0,
1921 ..MetadataIoConfig::default()
1922 };
1923 assert_eq!(
1924 zero_operations.validate(),
1925 Err(RuntimeContractViolation::InvalidMetadataConfiguration {
1926 policy: RuntimeContractPolicy::MetadataMaxPendingOperationsPositive,
1927 })
1928 );
1929
1930 let zero_bytes = MetadataIoConfig {
1931 max_pending_bytes: 0,
1932 ..MetadataIoConfig::default()
1933 };
1934 assert_eq!(
1935 zero_bytes.validate(),
1936 Err(RuntimeContractViolation::InvalidMetadataConfiguration {
1937 policy: RuntimeContractPolicy::MetadataMaxPendingBytesPositive,
1938 })
1939 );
1940
1941 assert!(matches!(
1942 MetadataIoConfig {
1943 max_pending_operations: 0,
1944 ..MetadataIoConfig::default()
1945 }
1946 .into_plan(),
1947 Err(RuntimeContractViolation::InvalidMetadataConfiguration {
1948 policy: RuntimeContractPolicy::MetadataMaxPendingOperationsPositive,
1949 })
1950 ));
1951 }
1952
1953 #[test]
1954 fn metadata_io_error_retains_the_original_io_source() {
1955 let error = metadata_io_failure(
1956 MetadataIoOperation::WriteTemporary,
1957 Path::new("metadata.json"),
1958 std::io::Error::new(std::io::ErrorKind::WriteZero, "partial write"),
1959 );
1960 let source = error.source().expect("metadata error should retain its source");
1961 let source = source
1962 .downcast_ref::<std::io::Error>()
1963 .expect("metadata error source should remain a std::io::Error");
1964 assert_eq!(source.kind(), std::io::ErrorKind::WriteZero);
1965 }
1966
1967 #[test]
1968 fn commit_observation_requires_reconciliation_only_for_unconfirmed_outcomes() {
1969 let step_error = || {
1970 RuntimeError::io(
1971 crate::RuntimeOperation::MetadataSyncParent,
1972 std::io::Error::other("injected failure"),
1973 )
1974 };
1975
1976 let confirmed = [
1977 MetadataIoCommitObservation::Settled {
1978 generation: MetadataGeneration::new(7),
1979 outcome: MetadataIoCommitOutcome::Durable(MetadataGeneration::new(7)),
1980 },
1981 MetadataIoCommitObservation::Settled {
1982 generation: MetadataGeneration::new(7),
1983 outcome: MetadataIoCommitOutcome::FailedBeforeCommit(step_error()),
1984 },
1985 ];
1986 for observation in confirmed {
1987 assert!(
1988 !observation.requires_reconciliation(),
1989 "a settled conclusion cannot require reconciliation"
1990 );
1991 assert_eq!(observation.unobserved_generation(), None);
1992 assert_eq!(observation.generation(), Some(MetadataGeneration::new(7)));
1993 }
1994
1995 let unconfirmed = MetadataIoCommitObservation::Settled {
1996 generation: MetadataGeneration::new(7),
1997 outcome: MetadataIoCommitOutcome::CommitOutcomeUnknown(step_error()),
1998 };
1999 assert!(unconfirmed.requires_reconciliation());
2000 assert_eq!(unconfirmed.generation(), Some(MetadataGeneration::new(7)));
2001 assert!(unconfirmed.settled().is_some());
2002
2003 let unobserved = MetadataIoCommitObservation::Unobserved {
2004 generation: MetadataGeneration::new(9),
2005 };
2006 assert!(unobserved.requires_reconciliation());
2007 assert_eq!(unobserved.unobserved_generation(), Some(MetadataGeneration::new(9)));
2008 assert_eq!(unobserved.generation(), Some(MetadataGeneration::new(9)));
2009 assert!(unobserved.settled().is_none());
2010 }
2011
2012 #[test]
2013 fn target_conflict_observation_is_not_a_durability_conclusion() {
2014 let conflict = MetadataIoCommitObservation::TargetConflict(MetadataWriteRequest::new(
2015 "resource",
2016 MetadataGeneration::new(1),
2017 Path::new("metadata.json"),
2018 b"snapshot".to_vec(),
2019 ));
2020 assert!(!conflict.requires_reconciliation());
2021 assert_eq!(conflict.unobserved_generation(), None);
2022 assert_eq!(conflict.generation(), None);
2023 assert!(conflict.settled().is_none());
2024 }
2025}