Skip to main content

tenferro_runtime/runtime/
snapshot.rs

1use std::collections::BTreeMap;
2use std::fmt;
3use std::num::NonZeroU64;
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::sync::{Arc, RwLock};
6
7use tenferro_tensor::{Tensor, TensorValue};
8
9use crate::graph::CompiledGraph;
10use crate::program::FrozenProgram;
11
12use super::cache::{PreparedPlanCacheLimits, RuntimeCacheSet};
13use super::cache_owner::{FrozenCacheOwner, FrozenCacheOwnerKind};
14use super::engine_registration::{CandidateRegistrationToken, EngineRegistrationState};
15use super::execution;
16#[cfg(test)]
17use super::extension::ExtensionSlotFullForTest;
18use super::extension::{
19    bind_candidate_module, configure_module, freeze_extension_slots, BoundCandidateModuleRecord,
20    CandidateModuleRecord, CandidateRegistrationIdentity, ExtensionEngineSnapshotView,
21    ExtensionFamilyId, FrozenExtensionSlots,
22};
23use super::preparation::{PreparedEntryKey, PreparedProgram, PreparedProgramResult};
24use super::schedule::EventDomainId;
25use super::{
26    CacheOwnerId, CoreCapabilityBundle, EngineId, EngineRegistration, ExecutionContextIdentity,
27    ExecutionPolicy, ExtensionModule, ExtensionModuleError, ExtensionModuleId,
28    FrozenTransferRegistry, HardwareClassId, InputSignature, PrepareOptions,
29    ProviderDeviceIdentity, RegistrationIdentity, RegistrationKey, ResolvedTransferEndpoint,
30    ResolvedTransferRoute, RuntimeCacheError, RuntimeCacheStats, RuntimeConfigError, RuntimeEpoch,
31    RuntimeId, RuntimeReconfigureError, RuntimeStateError, StorageClass, TransferEndpoint,
32    TransferProvider, TransferRoute,
33};
34
35static NEXT_RUNTIME_ID: AtomicU64 = AtomicU64::new(1);
36static NEXT_REGISTRATION_ISSUER: AtomicU64 = AtomicU64::new(1);
37const INITIAL_REGISTRATION_ORDINAL: NonZeroU64 = NonZeroU64::MIN;
38
39#[derive(Clone, Debug)]
40struct CandidateEngineRecord {
41    registration: EngineRegistration,
42    identity: CandidateRegistrationIdentity,
43}
44
45#[derive(Clone, Debug)]
46struct BoundCandidateEngineRecord {
47    registration: EngineRegistration,
48    identity: RegistrationIdentity,
49}
50
51#[derive(Clone, Debug)]
52enum CandidateTransferBinding {
53    /// A route registered before its complete candidate can be validated.
54    New,
55    /// A route carried forward from a frozen snapshot.
56    Preserved {
57        source: ProviderDeviceIdentity,
58        destination: ProviderDeviceIdentity,
59    },
60}
61
62#[derive(Clone, Debug)]
63struct CandidateTransferRecord {
64    provider: Arc<dyn TransferProvider>,
65    binding: CandidateTransferBinding,
66}
67
68struct BoundCandidateTransferRecord {
69    provider: Arc<dyn TransferProvider>,
70    source: ProviderDeviceIdentity,
71    destination: ProviderDeviceIdentity,
72}
73
74#[derive(Clone, Debug)]
75struct CandidateConfig {
76    policy: ExecutionPolicy,
77    engines: BTreeMap<EngineId, CandidateEngineRecord>,
78    modules: BTreeMap<ExtensionModuleId, CandidateModuleRecord>,
79    transfers: BTreeMap<TransferRoute, CandidateTransferRecord>,
80}
81
82struct BoundCandidateConfig {
83    policy: ExecutionPolicy,
84    engines: BTreeMap<EngineId, BoundCandidateEngineRecord>,
85    modules: BTreeMap<ExtensionModuleId, BoundCandidateModuleRecord>,
86    transfers: BTreeMap<TransferRoute, BoundCandidateTransferRecord>,
87}
88
89impl CandidateConfig {
90    fn empty() -> Self {
91        Self {
92            policy: default_execution_policy(),
93            engines: BTreeMap::new(),
94            modules: BTreeMap::new(),
95            transfers: BTreeMap::new(),
96        }
97    }
98
99    fn from_snapshot(snapshot: &RuntimeConfigSnapshot) -> Result<Self, RuntimeConfigError> {
100        let engines = snapshot
101            .engines
102            .iter()
103            .map(|slot| {
104                let registration = slot.to_registration()?;
105                Ok((
106                    registration.engine_id().clone(),
107                    CandidateEngineRecord {
108                        registration,
109                        identity: CandidateRegistrationIdentity::Preserved(
110                            slot.metadata().identity,
111                        ),
112                    },
113                ))
114            })
115            .collect::<Result<BTreeMap<_, _>, RuntimeConfigError>>()?;
116        Ok(Self {
117            policy: snapshot.policy.clone(),
118            engines,
119            modules: snapshot.extensions.to_candidate_modules(),
120            transfers: snapshot
121                .transfers
122                .iter()
123                .map(|(resolved_route, provider)| {
124                    (
125                        TransferRoute::new(
126                            resolved_route.source().logical().clone(),
127                            resolved_route.destination().logical().clone(),
128                        ),
129                        CandidateTransferRecord {
130                            provider: Arc::clone(provider),
131                            binding: CandidateTransferBinding::Preserved {
132                                source: resolved_route.source().provider_device_identity().clone(),
133                                destination: resolved_route
134                                    .destination()
135                                    .provider_device_identity()
136                                    .clone(),
137                            },
138                        },
139                    )
140                })
141                .collect(),
142        })
143    }
144}
145
146#[derive(Clone, Debug)]
147struct FrozenEngineMetadata {
148    candidate_token: Arc<CandidateRegistrationToken>,
149    identity: RegistrationIdentity,
150    event_domain_id: EventDomainId,
151}
152
153#[derive(Clone)]
154struct PreparationOnlyEngineSnapshot {
155    metadata: FrozenEngineMetadata,
156    binding: super::ProviderPreparationBinding,
157}
158
159#[derive(Clone, Debug)]
160pub(super) struct ExecutableEngineSnapshot {
161    metadata: FrozenEngineMetadata,
162    binding: super::ProviderExecutableBinding,
163}
164
165#[derive(Clone)]
166enum FrozenEngineSlot {
167    PreparationOnly(Arc<PreparationOnlyEngineSnapshot>),
168    Executable(Arc<ExecutableEngineSnapshot>),
169}
170
171impl FrozenEngineSlot {
172    fn metadata(&self) -> &FrozenEngineMetadata {
173        match self {
174            Self::PreparationOnly(snapshot) => &snapshot.metadata,
175            Self::Executable(snapshot) => &snapshot.metadata,
176        }
177    }
178
179    fn provider_device_identity(&self) -> &ProviderDeviceIdentity {
180        match self {
181            Self::PreparationOnly(snapshot) => snapshot.binding.provider_device_identity(),
182            Self::Executable(snapshot) => snapshot.binding.contract().provider_device_identity(),
183        }
184    }
185
186    fn engine_id(&self) -> &EngineId {
187        match self {
188            Self::PreparationOnly(snapshot) => snapshot.binding.engine_id(),
189            Self::Executable(snapshot) => snapshot.binding.engine_id(),
190        }
191    }
192
193    fn hardware_class(&self) -> &HardwareClassId {
194        match self {
195            Self::PreparationOnly(snapshot) => snapshot.binding.hardware_class(),
196            Self::Executable(snapshot) => snapshot.binding.hardware_class(),
197        }
198    }
199
200    fn storage_classes(&self) -> &[StorageClass] {
201        match self {
202            Self::PreparationOnly(snapshot) => snapshot.binding.storage_classes(),
203            Self::Executable(snapshot) => snapshot.binding.storage_classes(),
204        }
205    }
206
207    fn default_storage_class(&self) -> &StorageClass {
208        match self {
209            Self::PreparationOnly(snapshot) => snapshot.binding.default_storage_class(),
210            Self::Executable(snapshot) => snapshot.binding.default_storage_class(),
211        }
212    }
213
214    fn context_identity(&self) -> ExecutionContextIdentity {
215        match self {
216            Self::PreparationOnly(snapshot) => snapshot.binding.context_identity(),
217            Self::Executable(snapshot) => snapshot.binding.contract().context_identity(),
218        }
219    }
220
221    fn capabilities(&self) -> &CoreCapabilityBundle {
222        match self {
223            Self::PreparationOnly(snapshot) => snapshot.binding.capabilities(),
224            Self::Executable(snapshot) => snapshot.binding.contract().capabilities(),
225        }
226    }
227
228    fn executable(&self) -> Option<&Arc<ExecutableEngineSnapshot>> {
229        match self {
230            Self::PreparationOnly(_) => None,
231            Self::Executable(snapshot) => Some(snapshot),
232        }
233    }
234}
235
236impl ExecutableEngineSnapshot {
237    pub(super) fn engine_id(&self) -> &EngineId {
238        self.binding.engine_id()
239    }
240
241    pub(super) fn event_domain_id(&self) -> EventDomainId {
242        self.metadata.event_domain_id
243    }
244
245    pub(super) fn provider_device_identity(&self) -> &ProviderDeviceIdentity {
246        self.binding.contract().provider_device_identity()
247    }
248
249    #[cfg(test)]
250    pub(super) fn context_identity(&self) -> ExecutionContextIdentity {
251        self.binding.contract().context_identity()
252    }
253
254    pub(super) fn executor(&self) -> &Arc<dyn super::execution::ErasedTensorBackendExecutor> {
255        self.binding.contract().executor()
256    }
257
258    pub(super) fn event_domain_driver(&self) -> &Arc<dyn super::EventDomainDriver> {
259        self.binding.contract().event_domain_driver()
260    }
261
262    #[cfg(test)]
263    pub(super) fn has_executor(&self) -> bool {
264        true
265    }
266
267    #[cfg(test)]
268    pub(super) fn has_event_domain_driver(&self) -> bool {
269        true
270    }
271
272    pub(super) fn accepts_input_placement(
273        &self,
274        placement: &tenferro_tensor::Placement,
275        storage_class: &StorageClass,
276    ) -> bool {
277        self.binding.storage_classes().contains(storage_class)
278            && self
279                .binding
280                .contract()
281                .accepts_input_placement(placement, storage_class)
282    }
283
284    pub(super) fn accepts_input_signature(
285        &self,
286        input: &super::InputSignatureEntry,
287        storage_class: &StorageClass,
288    ) -> bool {
289        self.binding.storage_classes().contains(storage_class)
290            && self
291                .binding
292                .contract()
293                .accepts_input_signature(input, storage_class)
294    }
295
296    pub(super) fn accepts_runtime_input(
297        &self,
298        input: &tenferro_tensor::TensorRead<'_>,
299        storage_class: &StorageClass,
300    ) -> bool {
301        self.binding.storage_classes().contains(storage_class)
302            && self
303                .binding
304                .contract()
305                .accepts_runtime_input(input, storage_class)
306    }
307
308    pub(super) fn owns_resident_tensor(
309        &self,
310        input: &tenferro_tensor::TensorRead<'_>,
311        storage_class: &StorageClass,
312    ) -> bool {
313        self.binding.storage_classes().contains(storage_class)
314            && self
315                .binding
316                .contract()
317                .owns_resident_tensor(input, storage_class)
318    }
319
320    #[cfg(test)]
321    pub(super) fn for_test(
322        engine_id: EngineId,
323        provider_device_identity: ProviderDeviceIdentity,
324        event_domain_id: EventDomainId,
325        storage_class: StorageClass,
326    ) -> Arc<Self> {
327        Self::for_test_with_driver(
328            engine_id,
329            provider_device_identity,
330            event_domain_id,
331            storage_class,
332            Arc::new(super::ImmediateEventDomainDriver::new()),
333        )
334    }
335
336    #[cfg(test)]
337    pub(super) fn for_test_with_driver(
338        engine_id: EngineId,
339        provider_device_identity: ProviderDeviceIdentity,
340        event_domain_id: EventDomainId,
341        storage_class: StorageClass,
342        event_domain_driver: Arc<dyn super::EventDomainDriver>,
343    ) -> Arc<Self> {
344        let ingress = super::InputIngressContract::new(
345            super::InputPlacementContract::new(|_, _| true),
346            super::InputSignatureContract::new(|_, _, _, _| true),
347            super::RuntimeInputContract::new(|_, _| true),
348            super::ResidentOutputContract::new(|_, _| true),
349        );
350        let contract = super::ExecutableEngineContract::new(
351            provider_device_identity,
352            CoreCapabilityBundle::default(),
353            tenferro_cpu::CpuBackend::new(),
354            event_domain_driver,
355            ingress,
356            None,
357        );
358        let binding = super::ProviderExecutableBinding::new(
359            engine_id,
360            HardwareClassId::new("tenferro.test.schedule.hardware").expect("test hardware class"),
361            Arc::from(vec![storage_class.clone()]),
362            storage_class,
363            contract,
364        )
365        .expect("test executable binding");
366        Arc::new(Self {
367            metadata: FrozenEngineMetadata {
368                candidate_token: Arc::new(CandidateRegistrationToken),
369                identity: event_domain_id.registration_identity(),
370                event_domain_id,
371            },
372            binding,
373        })
374    }
375}
376
377impl FrozenEngineSlot {
378    fn to_registration(&self) -> Result<EngineRegistration, RuntimeConfigError> {
379        let metadata = self.metadata();
380        let registration = match self {
381            Self::PreparationOnly(snapshot) => {
382                EngineRegistration::from_state(EngineRegistrationState::PreparationOnly {
383                    binding: snapshot.binding.clone(),
384                })
385            }
386            Self::Executable(snapshot) => EngineRegistration::from_state(
387                EngineRegistrationState::Executable(snapshot.binding.clone()),
388            ),
389        };
390        Ok(registration.with_candidate_token(Arc::clone(&metadata.candidate_token)))
391    }
392}
393
394impl fmt::Debug for FrozenEngineSlot {
395    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
396        let metadata = self.metadata();
397        formatter
398            .debug_struct("FrozenEngineSlot")
399            .field("engine_id", self.engine_id())
400            .field("registration_identity", &metadata.identity)
401            .field("event_domain_id", &metadata.event_domain_id)
402            .field("context_identity", &self.context_identity())
403            .field("hardware_class", self.hardware_class())
404            .field(
405                "state",
406                &match self {
407                    Self::PreparationOnly(_) => "preparation-only",
408                    Self::Executable(_) => "executable",
409                },
410            )
411            .finish_non_exhaustive()
412    }
413}
414
415/// Immutable runtime configuration snapshot.
416///
417/// A snapshot is readable after later reconfiguration because the runtime
418/// publishes a new `Arc` instead of mutating existing slots.
419///
420/// # Examples
421///
422/// ```
423/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
424/// use tenferro_runtime::RuntimeConfigBuilder;
425///
426/// let runtime = RuntimeConfigBuilder::new().build()?;
427/// let snapshot = runtime.snapshot()?;
428/// assert_eq!(snapshot.engine_count(), 0);
429/// # Ok(())
430/// # }
431/// ```
432#[derive(Clone)]
433pub struct RuntimeConfigSnapshot {
434    runtime_id: RuntimeId,
435    epoch: RuntimeEpoch,
436    policy: ExecutionPolicy,
437    engines: Arc<[FrozenEngineSlot]>,
438    engine_indices: BTreeMap<EngineId, usize>,
439    extensions: FrozenExtensionSlots,
440    transfers: FrozenTransferRegistry,
441    cache_owners: Arc<[FrozenCacheOwner]>,
442}
443
444impl RuntimeConfigSnapshot {
445    /// Return the runtime identity that published this snapshot.
446    pub fn runtime_id(&self) -> RuntimeId {
447        self.runtime_id
448    }
449
450    /// Return the epoch for this immutable snapshot.
451    pub fn epoch(&self) -> RuntimeEpoch {
452        self.epoch
453    }
454
455    /// Return the execution policy captured by this snapshot.
456    pub fn execution_policy(&self) -> &ExecutionPolicy {
457        &self.policy
458    }
459
460    /// Return the number of direct engine slots.
461    pub fn engine_count(&self) -> usize {
462        self.engines.len()
463    }
464
465    /// Return the number of installed extension modules.
466    pub fn extension_module_count(&self) -> usize {
467        self.extensions.module_count()
468    }
469
470    /// Return the number of registered transfer providers.
471    pub fn transfer_provider_count(&self) -> usize {
472        self.transfers.len()
473    }
474
475    /// Return whether this snapshot contains an extension engine for a family.
476    #[doc(hidden)]
477    pub fn has_extension_family(&self, family_id: &'static str) -> bool {
478        self.extensions.has_family(family_id)
479    }
480
481    /// Return an immutable view of a registered engine slot.
482    pub fn engine(&self, id: &EngineId) -> Option<EngineSnapshotView<'_>> {
483        self.engine_indices
484            .get(id)
485            .map(|&index| EngineSnapshotView {
486                slot: &self.engines[index],
487            })
488    }
489
490    #[cfg(test)]
491    pub(crate) fn engine_ids_for_test(&self) -> impl Iterator<Item = &EngineId> {
492        self.engines.iter().map(FrozenEngineSlot::engine_id)
493    }
494
495    #[cfg(test)]
496    pub(crate) fn transfer_routes_for_test(&self) -> impl Iterator<Item = &ResolvedTransferRoute> {
497        self.transfers.iter().map(|(route, _)| route)
498    }
499
500    pub(super) fn engine_views_for_preparation(
501        &self,
502    ) -> impl Iterator<Item = EngineSnapshotView<'_>> + '_ {
503        self.engines.iter().map(|slot| EngineSnapshotView { slot })
504    }
505
506    pub(super) fn extension_slot_for_preparation(
507        &self,
508        family_id: ExtensionFamilyId,
509        engine_id: &EngineId,
510    ) -> Option<ExtensionEngineSnapshotView<'_>> {
511        self.extensions.slot_for_preparation(family_id, engine_id)
512    }
513
514    pub(super) fn transfer_registry_for_preparation(&self) -> FrozenTransferRegistry {
515        self.transfers.clone()
516    }
517
518    #[cfg(test)]
519    pub(crate) fn extension_slots_for_test(
520        &self,
521    ) -> impl Iterator<
522        Item = (
523            &ExtensionModuleId,
524            ExtensionFamilyId,
525            &EngineId,
526            RegistrationIdentity,
527        ),
528    > {
529        self.extensions.slots_for_test()
530    }
531
532    #[cfg(test)]
533    pub(crate) fn extension_slot_identity_for_test(
534        &self,
535        family_id: ExtensionFamilyId,
536        engine_id: &EngineId,
537    ) -> Option<RegistrationIdentity> {
538        self.extensions.slot_identity_for_test(family_id, engine_id)
539    }
540
541    #[cfg(test)]
542    pub(crate) fn extension_slot_full_for_test(
543        &self,
544        family_id: ExtensionFamilyId,
545        engine_id: &EngineId,
546    ) -> Option<ExtensionSlotFullForTest<'_>> {
547        self.extensions.slot_full_for_test(family_id, engine_id)
548    }
549
550    #[cfg(test)]
551    pub(super) fn cache_owners_for_test(&self) -> &[FrozenCacheOwner] {
552        &self.cache_owners
553    }
554
555    pub(super) fn cache_owners_for_runtime(&self) -> &[FrozenCacheOwner] {
556        &self.cache_owners
557    }
558}
559
560impl fmt::Debug for RuntimeConfigSnapshot {
561    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
562        formatter
563            .debug_struct("RuntimeConfigSnapshot")
564            .field("runtime_id", &self.runtime_id)
565            .field("epoch", &self.epoch)
566            .field("execution_policy", &self.policy)
567            .field("engine_count", &self.engines.len())
568            .field("extension_module_count", &self.extensions.module_count())
569            .field("extension_engine_count", &self.extensions.engine_count())
570            .field("transfer_provider_count", &self.transfers.len())
571            .field("cache_owner_count", &self.cache_owners.len())
572            .finish_non_exhaustive()
573    }
574}
575
576struct RuntimeState {
577    runtime_id: RuntimeId,
578    issuer: NonZeroU64,
579    next_registration_ordinal: AtomicU64,
580    active: RwLock<Arc<RuntimeConfigSnapshot>>,
581    published_epoch: AtomicU64,
582    caches: RuntimeCacheSet<PreparedEntryKey, PreparedProgram>,
583}
584
585/// Runtime owner for immutable configuration snapshots.
586///
587/// # Examples
588///
589/// ```
590/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
591/// use tenferro_runtime::Runtime;
592///
593/// let runtime = Runtime::builder().build()?;
594/// assert_eq!(runtime.snapshot()?.engine_count(), 0);
595/// # Ok(())
596/// # }
597/// ```
598#[derive(Clone)]
599pub struct Runtime(Arc<RuntimeState>);
600
601impl Runtime {
602    /// Return a consuming runtime configuration builder.
603    pub fn builder() -> RuntimeConfigBuilder {
604        RuntimeConfigBuilder::new()
605    }
606
607    /// Return this runtime's opaque identity.
608    pub fn id(&self) -> RuntimeId {
609        self.0.runtime_id
610    }
611
612    /// Clone the current immutable runtime snapshot.
613    ///
614    /// # Errors
615    ///
616    /// Returns [`RuntimeStateError::Poisoned`] when the active snapshot lock was
617    /// poisoned by another thread.
618    pub fn snapshot(&self) -> Result<Arc<RuntimeConfigSnapshot>, RuntimeStateError> {
619        self.0
620            .active
621            .read()
622            .map(|snapshot| Arc::clone(&snapshot))
623            .map_err(|_| RuntimeStateError::Poisoned {
624                lock: "runtime.active",
625            })
626    }
627
628    /// Return the currently published runtime epoch without locking the active
629    /// snapshot.
630    ///
631    /// # Errors
632    ///
633    /// Returns [`RuntimeStateError`] only if runtime state invariants have been
634    /// violated internally.
635    pub fn epoch(&self) -> Result<RuntimeEpoch, RuntimeStateError> {
636        match NonZeroU64::new(self.0.published_epoch.load(Ordering::Acquire)) {
637            Some(value) => Ok(RuntimeEpoch::from_nonzero(value)),
638            None => Err(RuntimeStateError::Poisoned {
639                lock: "runtime.published_epoch",
640            }),
641        }
642    }
643
644    /// Return current prepared-plan cache limits.
645    ///
646    /// # Examples
647    ///
648    /// ```
649    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
650    /// use tenferro_runtime::Runtime;
651    ///
652    /// let runtime = Runtime::builder().build()?;
653    /// assert_eq!(runtime.prepared_cache_limits()?, Default::default());
654    /// # Ok(())
655    /// # }
656    /// ```
657    ///
658    /// # Errors
659    ///
660    /// Returns [`RuntimeStateError`] when the runtime-owned prepared cache state
661    /// cannot be accessed.
662    pub fn prepared_cache_limits(&self) -> Result<PreparedPlanCacheLimits, RuntimeStateError> {
663        self.0.caches.prepared().limits()
664    }
665
666    /// Replace current prepared-plan cache limits and evict retained entries
667    /// until the new limits are satisfied.
668    ///
669    /// # Examples
670    ///
671    /// ```
672    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
673    /// use std::num::NonZeroUsize;
674    /// use tenferro_runtime::{PreparedPlanCacheLimits, Runtime};
675    ///
676    /// let runtime = Runtime::builder().build()?;
677    /// runtime.set_prepared_cache_limits(PreparedPlanCacheLimits {
678    ///     max_entries: NonZeroUsize::new(1).unwrap(),
679    ///     max_retained_bytes: NonZeroUsize::new(1024).unwrap(),
680    ///     max_in_flight_entries: NonZeroUsize::new(1).unwrap(),
681    ///     max_queued_distinct_keys: NonZeroUsize::new(1).unwrap(),
682    /// })?;
683    /// assert_eq!(runtime.prepared_cache_limits()?.max_entries.get(), 1);
684    /// # Ok(())
685    /// # }
686    /// ```
687    ///
688    /// # Errors
689    ///
690    /// Returns [`RuntimeStateError`] when the runtime-owned prepared cache state
691    /// cannot be accessed.
692    pub fn set_prepared_cache_limits(
693        &self,
694        limits: PreparedPlanCacheLimits,
695    ) -> Result<(), RuntimeStateError> {
696        self.0.caches.prepared().set_limits(limits)
697    }
698
699    /// Clear the runtime-owned prepared-plan cache.
700    ///
701    /// # Examples
702    ///
703    /// ```
704    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
705    /// use tenferro_runtime::Runtime;
706    ///
707    /// let runtime = Runtime::builder().build()?;
708    /// runtime.clear_prepared_cache()?;
709    /// # Ok(())
710    /// # }
711    /// ```
712    ///
713    /// # Errors
714    ///
715    /// Returns [`RuntimeStateError`] when the runtime-owned prepared cache state
716    /// cannot be accessed.
717    pub fn clear_prepared_cache(&self) -> Result<(), RuntimeStateError> {
718        self.0.caches.prepared().clear()
719    }
720
721    /// Return aggregate cache statistics for the runtime and registered cache
722    /// owners.
723    ///
724    /// # Examples
725    ///
726    /// ```
727    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
728    /// use tenferro_runtime::Runtime;
729    ///
730    /// let runtime = Runtime::builder().build()?;
731    /// assert_eq!(runtime.cache_stats()?.prepared_plans.entries, 0);
732    /// # Ok(())
733    /// # }
734    /// ```
735    ///
736    /// # Errors
737    ///
738    /// Returns [`RuntimeCacheError`] when the runtime cache or a registered
739    /// cache owner cannot report statistics.
740    pub fn cache_stats(&self) -> Result<RuntimeCacheStats, RuntimeCacheError> {
741        super::preparation::cache_stats(self, &self.0.caches)
742    }
743
744    /// Clear runtime-owned prepared plans and all registered engine/extension
745    /// cache owners.
746    ///
747    /// # Examples
748    ///
749    /// ```
750    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
751    /// use tenferro_runtime::Runtime;
752    ///
753    /// let runtime = Runtime::builder().build()?;
754    /// runtime.clear_caches()?;
755    /// # Ok(())
756    /// # }
757    /// ```
758    ///
759    /// # Errors
760    ///
761    /// Returns [`RuntimeCacheError`] when the runtime cache or a registered
762    /// cache owner cannot be cleared.
763    pub fn clear_caches(&self) -> Result<(), RuntimeCacheError> {
764        super::preparation::clear_caches(self, &self.0.caches)
765    }
766
767    #[allow(
768        dead_code,
769        reason = "Phase 5 graph execution consumes crate-private prepared programs"
770    )]
771    pub(crate) fn prepare_for(
772        &self,
773        frozen: &FrozenProgram,
774        signature: &InputSignature,
775        options: &PrepareOptions,
776    ) -> PreparedProgramResult<Arc<PreparedProgram>> {
777        super::preparation::prepare_for(self, &self.0.caches, frozen, signature, options)
778    }
779
780    pub(crate) fn prepare_compiled_for(
781        &self,
782        program: &CompiledGraph,
783        signature: &InputSignature,
784        options: &PrepareOptions,
785    ) -> PreparedProgramResult<Arc<PreparedProgram>> {
786        super::preparation::prepare_compiled_for(self, &self.0.caches, program, signature, options)
787    }
788
789    /// Run a compiled graph through runtime-owned prepared execution.
790    ///
791    /// # Examples
792    ///
793    /// ```
794    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
795    /// use tenferro_runtime::{Runtime, TracedTensor, GraphCompiler};
796    ///
797    /// let runtime = Runtime::builder().build()?;
798    /// let x = TracedTensor::from_vec_col_major(vec![1], vec![1.0_f64])?;
799    /// let program = GraphCompiler::new().compile(&x)?;
800    /// let error = runtime.run_compiled(&program, &[]).unwrap_err();
801    /// assert!(error.to_string().contains("no eligible engine"));
802    /// # Ok(())
803    /// # }
804    /// ```
805    ///
806    /// # Errors
807    ///
808    /// Returns [`crate::Error::UnboundPlaceholder`] when no explicit inputs are
809    /// supplied and a semantic input has no bound default tensor.
810    /// Returns [`crate::Error::GraphInputCountMismatch`],
811    /// [`crate::Error::PlaceholderDtypeMismatch`],
812    /// [`crate::Error::PlaceholderRankMismatch`],
813    /// [`crate::Error::PlaceholderShapeMismatch`], or
814    /// [`crate::Error::PlaceholderShapeBoundExceeded`] when ordered runtime
815    /// inputs do not match the compiled graph metadata.
816    /// Returns [`crate::Error::RuntimeState`] when runtime preparation, schedule
817    /// validation, snapshot access, stale epoch checks, or execution-bridge
818    /// resolution fails, including [`crate::PrepareError::NoInputIngress`]
819    /// when no engine accepts an input's physical backend/allocation domain,
820    /// [`crate::PrepareError::MissingTransferProvider`] when ingress cannot
821    /// reach its first scheduled consumer, a runtime with no eligible engine,
822    /// or no execution bridge for the prepared engine. Backend execution may
823    /// also return concrete backend variants such as
824    /// [`crate::Error::Unsupported`], [`crate::Error::Validation`], or
825    /// [`crate::Error::Extension`].
826    pub fn run_compiled(
827        &self,
828        program: &CompiledGraph,
829        inputs: &[&Tensor],
830    ) -> crate::Result<Vec<Tensor>> {
831        super::execution::run_compiled(self, program, inputs)
832    }
833
834    /// Prepare a compiled graph for repeated execution with the same runtime.
835    ///
836    /// Preparation validates the supplied input metadata, selects a runtime
837    /// engine, and caches the staged execution plan. Use [`Self::run_prepared`]
838    /// for steady-state execution when the same compiled graph is run many
839    /// times.
840    ///
841    /// # Errors
842    ///
843    /// Returns [`crate::Error::UnboundPlaceholder`] when no explicit inputs are
844    /// supplied and a semantic input has no bound default tensor.
845    /// Returns [`crate::Error::GraphInputCountMismatch`],
846    /// [`crate::Error::PlaceholderDtypeMismatch`],
847    /// [`crate::Error::PlaceholderRankMismatch`],
848    /// [`crate::Error::PlaceholderShapeMismatch`], or
849    /// [`crate::Error::PlaceholderShapeBoundExceeded`] when ordered runtime
850    /// inputs do not match the compiled graph metadata.
851    /// Returns [`crate::Error::RuntimeState`] when runtime preparation, schedule
852    /// validation, snapshot access, stale epoch checks, or execution-bridge
853    /// resolution fails, including [`crate::PrepareError::NoInputIngress`]
854    /// when no engine accepts an input's physical backend/allocation domain,
855    /// [`crate::PrepareError::MissingTransferProvider`] when ingress cannot
856    /// reach its first scheduled consumer, a runtime with no eligible engine,
857    /// or no execution bridge for the prepared engine.
858    pub fn prepare_compiled(
859        &self,
860        program: &CompiledGraph,
861        inputs: &[&Tensor],
862    ) -> crate::Result<super::execution::PreparedCompiledGraph> {
863        super::execution::prepare_compiled(self, program, inputs)
864    }
865
866    /// Run a graph previously prepared by [`Self::prepare_compiled`].
867    ///
868    /// # Errors
869    ///
870    /// Returns metadata validation errors for incompatible inputs, a runtime
871    /// state error with [`crate::InputIngressContractError`] as its typed source
872    /// when an input's physical residency does not match the prepared ingress,
873    /// or a runtime state error if the prepared handle belongs to a different
874    /// runtime or a stale runtime epoch.
875    pub fn run_prepared(
876        &self,
877        prepared: &super::execution::PreparedCompiledGraph,
878        inputs: &[&Tensor],
879    ) -> crate::Result<Vec<Tensor>> {
880        super::execution::run_prepared(self, prepared, inputs)
881    }
882
883    /// Submit a compiled graph for asynchronous runtime-owned execution.
884    ///
885    /// Dropping the returned handle detaches the observer without blocking.
886    /// Use [`super::execution::ExecutionHandle::wait`] to observe completion.
887    ///
888    /// # Errors
889    ///
890    /// Returns the same [`crate::PrepareError::InputSignature`],
891    /// [`crate::PrepareError::Specialization`],
892    /// [`crate::PrepareError::NoEligibleEngine`],
893    /// [`crate::PrepareError::NoInputIngress`], and
894    /// [`crate::PrepareError::MissingTransferProvider`] failures as
895    /// [`Self::run_compiled`] before the worker is submitted. Returns a runtime
896    /// state error with [`crate::SubmissionError`] as its typed source if the
897    /// operating system rejects worker creation after admission.
898    pub fn submit(
899        &self,
900        program: &CompiledGraph,
901        inputs: &[&Tensor],
902    ) -> crate::Result<super::execution::ExecutionHandle> {
903        super::execution::submit(self, program, inputs)
904    }
905
906    /// Run a compiled graph and preserve lazy owned output views.
907    ///
908    /// # Examples
909    ///
910    /// ```
911    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
912    /// use tenferro_runtime::{Runtime, TracedTensor, GraphCompiler};
913    ///
914    /// let runtime = Runtime::builder().build()?;
915    /// let x = TracedTensor::from_vec_col_major(vec![1], vec![1.0_f64])?;
916    /// let program = GraphCompiler::new().compile(&x)?;
917    /// let error = runtime.run_compiled_values(&program, &[]).unwrap_err();
918    /// assert!(error.to_string().contains("no eligible engine"));
919    /// # Ok(())
920    /// # }
921    /// ```
922    ///
923    /// # Errors
924    ///
925    /// Returns [`crate::Error::UnboundPlaceholder`] when no explicit inputs are
926    /// supplied and a semantic input has no bound default tensor.
927    /// Returns [`crate::Error::GraphInputCountMismatch`],
928    /// [`crate::Error::PlaceholderDtypeMismatch`],
929    /// [`crate::Error::PlaceholderRankMismatch`],
930    /// [`crate::Error::PlaceholderShapeMismatch`], or
931    /// [`crate::Error::PlaceholderShapeBoundExceeded`] when ordered runtime
932    /// inputs do not match the compiled graph metadata.
933    /// Returns [`crate::Error::RuntimeState`] when runtime preparation, schedule
934    /// validation, snapshot access, stale epoch checks, or execution-bridge
935    /// resolution fails, including [`crate::PrepareError::NoInputIngress`]
936    /// when no engine accepts an input's physical backend/allocation domain,
937    /// [`crate::PrepareError::MissingTransferProvider`] when ingress cannot
938    /// reach its first scheduled consumer, a runtime with no eligible engine,
939    /// or no execution bridge for the prepared engine. Backend execution may
940    /// also return concrete backend variants such as
941    /// [`crate::Error::Unsupported`], [`crate::Error::Validation`], or
942    /// [`crate::Error::Extension`].
943    pub fn run_compiled_values(
944        &self,
945        program: &CompiledGraph,
946        inputs: &[&Tensor],
947    ) -> crate::Result<Vec<TensorValue>> {
948        super::execution::run_compiled_values(self, program, inputs)
949    }
950
951    /// Transactionally edit and publish runtime configuration.
952    ///
953    /// No user callback runs while the publication lock is held. If another
954    /// writer publishes over the same base snapshot, this call returns
955    /// [`RuntimeReconfigureError::ConcurrentReconfiguration`] and publishes
956    /// nothing.
957    ///
958    /// # Errors
959    ///
960    /// Returns [`RuntimeReconfigureError`] when state access, edit validation,
961    /// identity allocation, epoch advancement, or compare-and-publish fails.
962    /// Invalid transfer endpoints are reported as the typed
963    /// [`RuntimeConfigError::UnknownTransferEndpointEngine`] or
964    /// [`RuntimeConfigError::UnsupportedTransferEndpointStorage`] source of
965    /// [`RuntimeReconfigureError::Edit`].
966    pub fn reconfigure(
967        &self,
968        edit: impl FnOnce(&mut RuntimeReconfiguration<'_>) -> Result<(), RuntimeConfigError>,
969    ) -> Result<RuntimeEpoch, RuntimeReconfigureError> {
970        let base = self
971            .snapshot()
972            .map_err(|source| RuntimeReconfigureError::State { source })?;
973        let mut candidate = CandidateConfig::from_snapshot(&base)
974            .map_err(|source| RuntimeReconfigureError::Edit { source })?;
975        let mut changed = false;
976        {
977            let mut reconfiguration = RuntimeReconfiguration {
978                candidate: &mut candidate,
979                changed: &mut changed,
980            };
981            edit(&mut reconfiguration)
982                .map_err(|source| RuntimeReconfigureError::Edit { source })?;
983        }
984
985        if !changed {
986            return Ok(base.epoch());
987        }
988        let next_identity_ordinal = NonZeroU64::new(
989            self.0.next_registration_ordinal.load(Ordering::SeqCst),
990        )
991        .ok_or(RuntimeReconfigureError::Edit {
992            source: RuntimeConfigError::IdentityExhausted,
993        })?;
994        let (bound_candidate, post_ordinal) =
995            validate_candidate(candidate, self.0.issuer, next_identity_ordinal)
996                .map_err(|source| RuntimeReconfigureError::Edit { source })?;
997
998        let next_epoch =
999            base.epoch()
1000                .checked_next()
1001                .ok_or(RuntimeReconfigureError::EpochExhausted {
1002                    current: base.epoch(),
1003                })?;
1004
1005        let mut guard = self
1006            .0
1007            .active
1008            .write()
1009            .map_err(|_| RuntimeReconfigureError::State {
1010                source: RuntimeStateError::Poisoned {
1011                    lock: "runtime.active",
1012                },
1013            })?;
1014        if !Arc::ptr_eq(&*guard, &base) {
1015            return Err(RuntimeReconfigureError::ConcurrentReconfiguration {
1016                base: base.epoch(),
1017                current: guard.epoch(),
1018            });
1019        }
1020
1021        let next_snapshot = Arc::new(
1022            freeze_candidate(self.0.runtime_id, next_epoch, bound_candidate)
1023                .map_err(|source| RuntimeReconfigureError::Edit { source })?,
1024        );
1025
1026        self.0
1027            .next_registration_ordinal
1028            .store(post_ordinal.get(), Ordering::SeqCst);
1029        *guard = next_snapshot;
1030        self.0
1031            .published_epoch
1032            .store(next_epoch.get().get(), Ordering::Release);
1033        Ok(next_epoch)
1034    }
1035
1036    #[cfg(test)]
1037    pub(crate) fn force_epoch_for_test(&self, epoch: RuntimeEpoch) {
1038        let mut guard = self.0.active.write().expect("test runtime lock");
1039        let mut replacement = (**guard).clone();
1040        replacement.epoch = epoch;
1041        *guard = Arc::new(replacement);
1042        self.0
1043            .published_epoch
1044            .store(epoch.get().get(), Ordering::Release);
1045    }
1046
1047    #[cfg(test)]
1048    pub(crate) fn force_next_registration_ordinal_for_test(&self, next: NonZeroU64) {
1049        self.0
1050            .next_registration_ordinal
1051            .store(next.get(), Ordering::SeqCst);
1052    }
1053
1054    #[cfg(test)]
1055    pub(crate) fn poison_active_lock_for_test(&self) {
1056        let state = Arc::clone(&self.0);
1057        let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || {
1058            let _guard = state.active.write().expect("test runtime lock");
1059            panic!("poison runtime.active for test");
1060        }));
1061    }
1062}
1063
1064impl fmt::Debug for Runtime {
1065    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1066        formatter
1067            .debug_struct("Runtime")
1068            .field("runtime_id", &self.0.runtime_id)
1069            .field("published_epoch", &self.epoch().ok())
1070            .finish_non_exhaustive()
1071    }
1072}
1073
1074/// Consuming builder for an immutable runtime configuration.
1075///
1076/// # Examples
1077///
1078/// ```
1079/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1080/// use tenferro_runtime::RuntimeConfigBuilder;
1081///
1082/// let runtime = RuntimeConfigBuilder::new().build()?;
1083/// assert_eq!(runtime.snapshot()?.engine_count(), 0);
1084/// # Ok(())
1085/// # }
1086/// ```
1087pub struct RuntimeConfigBuilder {
1088    candidate: CandidateConfig,
1089}
1090
1091impl RuntimeConfigBuilder {
1092    /// Create an empty runtime builder.
1093    pub fn new() -> Self {
1094        Self {
1095            candidate: CandidateConfig::empty(),
1096        }
1097    }
1098
1099    /// Replace the execution policy in the candidate configuration.
1100    pub fn execution_policy(&mut self, value: ExecutionPolicy) -> &mut Self {
1101        self.candidate.policy = value;
1102        self
1103    }
1104
1105    /// Register a new engine candidate.
1106    ///
1107    /// # Errors
1108    ///
1109    /// Returns [`RuntimeConfigError::DuplicateEngine`] if a different candidate
1110    /// with the same engine ID is already present.
1111    pub fn register_engine(
1112        &mut self,
1113        value: EngineRegistration,
1114    ) -> Result<&mut Self, RuntimeConfigError> {
1115        let mut changed = false;
1116        register_engine_candidate(&mut self.candidate, value, &mut changed)?;
1117        Ok(self)
1118    }
1119
1120    /// Explicitly replace an existing engine candidate.
1121    ///
1122    /// # Errors
1123    ///
1124    /// Returns [`RuntimeConfigError::MissingEngine`] if the engine ID is absent.
1125    pub fn replace_engine(
1126        &mut self,
1127        value: EngineRegistration,
1128    ) -> Result<&mut Self, RuntimeConfigError> {
1129        let mut changed = false;
1130        replace_engine_candidate(&mut self.candidate, value, &mut changed)?;
1131        Ok(self)
1132    }
1133
1134    /// Remove an existing engine candidate.
1135    ///
1136    /// # Errors
1137    ///
1138    /// Returns [`RuntimeConfigError::MissingEngine`] if the engine ID is absent.
1139    pub fn remove_engine(&mut self, id: &EngineId) -> Result<&mut Self, RuntimeConfigError> {
1140        let mut changed = false;
1141        remove_engine_candidate(&mut self.candidate, id, &mut changed)?;
1142        Ok(self)
1143    }
1144
1145    /// Install an extension module transaction.
1146    ///
1147    /// # Errors
1148    ///
1149    /// Returns [`RuntimeConfigError::ExtensionModule`] when module configuration
1150    /// fails or a distinct module already uses the same module ID.
1151    pub fn install_extension_module(
1152        &mut self,
1153        value: Arc<dyn ExtensionModule>,
1154    ) -> Result<&mut Self, RuntimeConfigError> {
1155        let mut changed = false;
1156        install_extension_module_candidate(&mut self.candidate, value, &mut changed)?;
1157        Ok(self)
1158    }
1159
1160    /// Register a transfer provider keyed by source and destination endpoints.
1161    ///
1162    /// # Examples
1163    ///
1164    /// ```
1165    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1166    /// use std::sync::Arc;
1167    /// use tenferro_runtime::{
1168    ///     assemble_preparation_only_engine_registration, CoreCapabilityBundle, EngineId,
1169    ///     EngineRegistration, EngineRegistrationMetadata, Error, ExecutionContextIdentity,
1170    ///     HardwareClassId, PreparationOnlyEngineRegistrationConfig, ProviderDeviceIdentity,
1171    ///     ProviderId, Runtime, StorageClass, TransferEndpoint, TransferProvider, TransferRequest,
1172    /// };
1173    ///
1174    /// #[derive(Debug)]
1175    /// struct ExampleProvider;
1176    ///
1177    /// impl TransferProvider for ExampleProvider {
1178    ///     fn transfer_blocking(
1179    ///         &self,
1180    ///         _request: TransferRequest<'_>,
1181    ///     ) -> tenferro_runtime::Result<tenferro_tensor::Tensor> {
1182    ///         Err(Error::Internal("the example does not execute a transfer".into()))
1183    ///     }
1184    /// }
1185    ///
1186    /// fn registration(
1187    ///     id: EngineId,
1188    ///     target: &str,
1189    ///     storage: &StorageClass,
1190    /// ) -> Result<EngineRegistration, tenferro_runtime::RuntimeConfigError> {
1191    ///     let metadata = EngineRegistrationMetadata::new(
1192    ///         id,
1193    ///         ProviderDeviceIdentity::new(ProviderId::new("example.provider")?, target)?,
1194    ///         HardwareClassId::new("example.hardware")?,
1195    ///         Arc::from([storage.clone()]),
1196    ///         storage.clone(),
1197    ///         CoreCapabilityBundle::default(),
1198    ///     );
1199    ///     assemble_preparation_only_engine_registration(
1200    ///         PreparationOnlyEngineRegistrationConfig::new(
1201    ///             metadata,
1202    ///             ExecutionContextIdentity::of::<()>(),
1203    ///         ),
1204    ///     )
1205    /// }
1206    ///
1207    /// let storage = StorageClass::new("example.storage.host")?;
1208    /// let source_id = EngineId::new("example.engine.source")?;
1209    /// let destination_id = EngineId::new("example.engine.destination")?;
1210    /// let source = registration(
1211    ///     source_id.clone(),
1212    ///     "source-0",
1213    ///     &storage,
1214    /// )?;
1215    /// let destination = registration(
1216    ///     destination_id.clone(),
1217    ///     "destination-0",
1218    ///     &storage,
1219    /// )?;
1220    /// let mut builder = Runtime::builder();
1221    /// builder.register_engine(source)?;
1222    /// builder.register_engine(destination)?;
1223    /// builder.register_transfer_provider(
1224    ///     TransferEndpoint::new(source_id, storage.clone()),
1225    ///     TransferEndpoint::new(destination_id, storage),
1226    ///     Arc::new(ExampleProvider),
1227    /// )?;
1228    /// let runtime = builder.build()?;
1229    /// assert_eq!(runtime.snapshot()?.transfer_provider_count(), 1);
1230    /// # Ok(())
1231    /// # }
1232    /// ```
1233    ///
1234    /// # Errors
1235    ///
1236    /// Returns [`RuntimeConfigError::ConflictingRegistration`] if a different
1237    /// provider is already registered for the same endpoint pair. The complete
1238    /// endpoint pair is validated when [`Self::build`] freezes the candidate.
1239    pub fn register_transfer_provider(
1240        &mut self,
1241        source: TransferEndpoint,
1242        destination: TransferEndpoint,
1243        provider: Arc<dyn TransferProvider>,
1244    ) -> Result<&mut Self, RuntimeConfigError> {
1245        let mut changed = false;
1246        register_transfer_provider_candidate(
1247            &mut self.candidate,
1248            source,
1249            destination,
1250            provider,
1251            &mut changed,
1252        )?;
1253        Ok(self)
1254    }
1255
1256    /// Remove the transfer provider for an endpoint pair.
1257    ///
1258    /// This explicit removal is required before changing an engine's physical
1259    /// binding. The route can then be registered again against the replacement
1260    /// binding in the same candidate transaction.
1261    ///
1262    /// # Errors
1263    ///
1264    /// Returns [`RuntimeConfigError::MissingTransferProvider`] when the exact
1265    /// endpoint pair is not registered.
1266    pub fn remove_transfer_provider(
1267        &mut self,
1268        source: TransferEndpoint,
1269        destination: TransferEndpoint,
1270    ) -> Result<&mut Self, RuntimeConfigError> {
1271        let mut changed = false;
1272        remove_transfer_provider_candidate(&mut self.candidate, source, destination, &mut changed)?;
1273        Ok(self)
1274    }
1275
1276    /// Replace an extension module transaction, installing it when absent.
1277    ///
1278    /// # Errors
1279    ///
1280    /// Returns [`RuntimeConfigError::ExtensionModule`] when module configuration
1281    /// fails.
1282    pub fn replace_extension_module(
1283        &mut self,
1284        value: Arc<dyn ExtensionModule>,
1285    ) -> Result<&mut Self, RuntimeConfigError> {
1286        let mut changed = false;
1287        replace_extension_module_candidate(&mut self.candidate, value, &mut changed)?;
1288        Ok(self)
1289    }
1290
1291    /// Remove an extension module candidate if present.
1292    ///
1293    /// # Errors
1294    ///
1295    /// This method currently has no failing absent-module path; it returns
1296    /// [`RuntimeConfigError`] only for future validated module removal failures.
1297    pub fn remove_extension_module(
1298        &mut self,
1299        id: &ExtensionModuleId,
1300    ) -> Result<&mut Self, RuntimeConfigError> {
1301        let mut changed = false;
1302        remove_extension_module_candidate(&mut self.candidate, id, &mut changed)?;
1303        Ok(self)
1304    }
1305
1306    /// Build and publish the initial runtime snapshot.
1307    ///
1308    /// # Errors
1309    ///
1310    /// Returns [`RuntimeConfigError::IdentityExhausted`] if runtime or
1311    /// registration identity allocation would wrap, or
1312    /// [`RuntimeConfigError::UnknownTransferEndpointEngine`] or
1313    /// [`RuntimeConfigError::UnsupportedTransferEndpointStorage`] if a
1314    /// registered transfer endpoint is invalid for the complete candidate.
1315    pub fn build(self) -> Result<Runtime, RuntimeConfigError> {
1316        let runtime_id = RuntimeId::from_nonzero(allocate_nonzero(&NEXT_RUNTIME_ID)?);
1317        let issuer = allocate_nonzero(&NEXT_REGISTRATION_ISSUER)?;
1318        let (bound_candidate, post_ordinal) =
1319            validate_candidate(self.candidate, issuer, INITIAL_REGISTRATION_ORDINAL)?;
1320        let epoch = RuntimeEpoch::one();
1321        let snapshot = Arc::new(freeze_candidate(runtime_id, epoch, bound_candidate)?);
1322        let state = RuntimeState {
1323            runtime_id,
1324            issuer,
1325            next_registration_ordinal: AtomicU64::new(post_ordinal.get()),
1326            active: RwLock::new(snapshot),
1327            published_epoch: AtomicU64::new(epoch.get().get()),
1328            caches: RuntimeCacheSet::new(PreparedPlanCacheLimits::default()),
1329        };
1330        Ok(Runtime(Arc::new(state)))
1331    }
1332}
1333
1334impl Default for RuntimeConfigBuilder {
1335    fn default() -> Self {
1336        Self::new()
1337    }
1338}
1339
1340impl fmt::Debug for RuntimeConfigBuilder {
1341    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1342        formatter
1343            .debug_struct("RuntimeConfigBuilder")
1344            .field("execution_policy", &self.candidate.policy)
1345            .field("engine_count", &self.candidate.engines.len())
1346            .field("extension_module_count", &self.candidate.modules.len())
1347            .field("transfer_provider_count", &self.candidate.transfers.len())
1348            .finish_non_exhaustive()
1349    }
1350}
1351
1352/// Non-escapable reconfiguration edit view.
1353pub struct RuntimeReconfiguration<'a> {
1354    candidate: &'a mut CandidateConfig,
1355    changed: &'a mut bool,
1356}
1357
1358impl RuntimeReconfiguration<'_> {
1359    /// Replace the candidate execution policy.
1360    pub fn execution_policy(&mut self, policy: ExecutionPolicy) -> &mut Self {
1361        if self.candidate.policy != policy {
1362            self.candidate.policy = policy;
1363            *self.changed = true;
1364        }
1365        self
1366    }
1367
1368    /// Register a new engine in this reconfiguration.
1369    ///
1370    /// # Errors
1371    ///
1372    /// Returns [`RuntimeConfigError::DuplicateEngine`] if a different candidate
1373    /// with the same engine ID is already present.
1374    pub fn register_engine(
1375        &mut self,
1376        value: EngineRegistration,
1377    ) -> Result<&mut Self, RuntimeConfigError> {
1378        register_engine_candidate(self.candidate, value, self.changed)?;
1379        Ok(self)
1380    }
1381
1382    /// Replace an existing engine in this reconfiguration.
1383    ///
1384    /// # Errors
1385    ///
1386    /// Returns [`RuntimeConfigError::MissingEngine`] if the engine ID is absent.
1387    pub fn replace_engine(
1388        &mut self,
1389        value: EngineRegistration,
1390    ) -> Result<&mut Self, RuntimeConfigError> {
1391        replace_engine_candidate(self.candidate, value, self.changed)?;
1392        Ok(self)
1393    }
1394
1395    /// Remove an existing engine in this reconfiguration.
1396    ///
1397    /// # Errors
1398    ///
1399    /// Returns [`RuntimeConfigError::MissingEngine`] if the engine ID is absent.
1400    pub fn remove_engine(&mut self, id: &EngineId) -> Result<&mut Self, RuntimeConfigError> {
1401        remove_engine_candidate(self.candidate, id, self.changed)?;
1402        Ok(self)
1403    }
1404
1405    /// Install an extension module in this reconfiguration.
1406    ///
1407    /// # Errors
1408    ///
1409    /// Returns [`RuntimeConfigError::ExtensionModule`] when module configuration
1410    /// fails or a distinct module already uses the same module ID.
1411    pub fn install_extension_module(
1412        &mut self,
1413        value: Arc<dyn ExtensionModule>,
1414    ) -> Result<&mut Self, RuntimeConfigError> {
1415        install_extension_module_candidate(self.candidate, value, self.changed)?;
1416        Ok(self)
1417    }
1418
1419    /// Register a transfer provider in this reconfiguration by endpoint pair.
1420    ///
1421    /// # Examples
1422    ///
1423    /// ```
1424    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1425    /// use std::sync::Arc;
1426    /// use tenferro_runtime::{
1427    ///     assemble_preparation_only_engine_registration, CoreCapabilityBundle, EngineId,
1428    ///     EngineRegistration, EngineRegistrationMetadata, Error, ExecutionContextIdentity,
1429    ///     HardwareClassId, PreparationOnlyEngineRegistrationConfig, ProviderDeviceIdentity,
1430    ///     ProviderId, Runtime, StorageClass, TransferEndpoint, TransferProvider, TransferRequest,
1431    /// };
1432    ///
1433    /// #[derive(Debug)]
1434    /// struct ExampleProvider;
1435    ///
1436    /// impl TransferProvider for ExampleProvider {
1437    ///     fn transfer_blocking(
1438    ///         &self,
1439    ///         _request: TransferRequest<'_>,
1440    ///     ) -> tenferro_runtime::Result<tenferro_tensor::Tensor> {
1441    ///         Err(Error::Internal("the example does not execute a transfer".into()))
1442    ///     }
1443    /// }
1444    ///
1445    /// fn registration(
1446    ///     id: &str,
1447    ///     storage: &StorageClass,
1448    /// ) -> Result<EngineRegistration, tenferro_runtime::RuntimeConfigError> {
1449    ///     let metadata = EngineRegistrationMetadata::new(
1450    ///         EngineId::new(id)?,
1451    ///         ProviderDeviceIdentity::new(
1452    ///             ProviderId::new("example.provider")?,
1453    ///             format!("engine:{id}"),
1454    ///         )?,
1455    ///         HardwareClassId::new("example.hardware")?,
1456    ///         Arc::from([storage.clone()]),
1457    ///         storage.clone(),
1458    ///         CoreCapabilityBundle::default(),
1459    ///     );
1460    ///     Ok(assemble_preparation_only_engine_registration(
1461    ///         PreparationOnlyEngineRegistrationConfig::new(
1462    ///             metadata,
1463    ///             ExecutionContextIdentity::of::<()>(),
1464    ///         ),
1465    ///     )?)
1466    /// }
1467    ///
1468    /// let storage = StorageClass::new("example.storage.host")?;
1469    /// let source_id = EngineId::new("example.engine.source")?;
1470    /// let destination_id = EngineId::new("example.engine.destination")?;
1471    /// let mut builder = Runtime::builder();
1472    /// builder.register_engine(registration(source_id.as_str(), &storage)?)?;
1473    /// builder.register_engine(registration(destination_id.as_str(), &storage)?)?;
1474    /// let runtime = builder.build()?;
1475    /// runtime.reconfigure(|edit| {
1476    ///     edit.register_transfer_provider(
1477    ///         TransferEndpoint::new(source_id, storage.clone()),
1478    ///         TransferEndpoint::new(destination_id, storage),
1479    ///         Arc::new(ExampleProvider),
1480    ///     )?;
1481    ///     Ok(())
1482    /// })?;
1483    /// assert_eq!(runtime.snapshot()?.transfer_provider_count(), 1);
1484    /// # Ok(())
1485    /// # }
1486    /// ```
1487    ///
1488    /// # Errors
1489    ///
1490    /// Returns [`RuntimeConfigError::ConflictingRegistration`] if a different
1491    /// provider is already registered for the same endpoint pair. The complete
1492    /// endpoint pair is validated before this reconfiguration is published.
1493    pub fn register_transfer_provider(
1494        &mut self,
1495        source: TransferEndpoint,
1496        destination: TransferEndpoint,
1497        provider: Arc<dyn TransferProvider>,
1498    ) -> Result<&mut Self, RuntimeConfigError> {
1499        register_transfer_provider_candidate(
1500            self.candidate,
1501            source,
1502            destination,
1503            provider,
1504            self.changed,
1505        )?;
1506        Ok(self)
1507    }
1508
1509    /// Remove the transfer provider for an endpoint pair in this
1510    /// reconfiguration.
1511    ///
1512    /// # Errors
1513    ///
1514    /// Returns [`RuntimeConfigError::MissingTransferProvider`] when the exact
1515    /// endpoint pair is not registered.
1516    pub fn remove_transfer_provider(
1517        &mut self,
1518        source: TransferEndpoint,
1519        destination: TransferEndpoint,
1520    ) -> Result<&mut Self, RuntimeConfigError> {
1521        remove_transfer_provider_candidate(self.candidate, source, destination, self.changed)?;
1522        Ok(self)
1523    }
1524
1525    /// Replace an extension module in this reconfiguration, installing when
1526    /// absent.
1527    ///
1528    /// # Errors
1529    ///
1530    /// Returns [`RuntimeConfigError::ExtensionModule`] when module configuration
1531    /// fails.
1532    pub fn replace_extension_module(
1533        &mut self,
1534        value: Arc<dyn ExtensionModule>,
1535    ) -> Result<&mut Self, RuntimeConfigError> {
1536        replace_extension_module_candidate(self.candidate, value, self.changed)?;
1537        Ok(self)
1538    }
1539
1540    /// Remove an extension module if present.
1541    ///
1542    /// # Errors
1543    ///
1544    /// This method currently has no failing absent-module path; it returns
1545    /// [`RuntimeConfigError`] only for future validated module removal failures.
1546    pub fn remove_extension_module(
1547        &mut self,
1548        id: &ExtensionModuleId,
1549    ) -> Result<&mut Self, RuntimeConfigError> {
1550        remove_extension_module_candidate(self.candidate, id, self.changed)?;
1551        Ok(self)
1552    }
1553}
1554
1555impl fmt::Debug for RuntimeReconfiguration<'_> {
1556    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1557        formatter
1558            .debug_struct("RuntimeReconfiguration")
1559            .field("engine_count", &self.candidate.engines.len())
1560            .field("extension_module_count", &self.candidate.modules.len())
1561            .field("transfer_provider_count", &self.candidate.transfers.len())
1562            .field("changed", &*self.changed)
1563            .finish_non_exhaustive()
1564    }
1565}
1566
1567/// Borrowed immutable view of one engine slot in a runtime snapshot.
1568///
1569/// # Examples
1570///
1571/// ```
1572/// use std::fmt::Debug;
1573/// use tenferro_runtime::EngineSnapshotView;
1574///
1575/// fn requires_debug<T: Debug>() {}
1576/// requires_debug::<EngineSnapshotView<'_>>();
1577/// ```
1578#[derive(Clone, Copy)]
1579pub struct EngineSnapshotView<'a> {
1580    slot: &'a FrozenEngineSlot,
1581}
1582
1583impl<'a> EngineSnapshotView<'a> {
1584    /// Return the engine ID for this slot.
1585    pub fn engine_id(&self) -> &'a EngineId {
1586        self.slot.engine_id()
1587    }
1588
1589    /// Return the immutable provider/device binding for this engine slot.
1590    ///
1591    /// # Examples
1592    ///
1593    /// ```
1594    /// # fn inspect(view: tenferro_runtime::EngineSnapshotView<'_>) {
1595    /// let _ = view.provider_device_identity();
1596    /// # }
1597    /// ```
1598    pub fn provider_device_identity(&self) -> &'a super::ProviderDeviceIdentity {
1599        self.slot.provider_device_identity()
1600    }
1601
1602    /// Return the runtime-local registration identity for this slot.
1603    pub fn registration_identity(&self) -> RegistrationIdentity {
1604        self.slot.metadata().identity
1605    }
1606
1607    /// Return the execution-context identity required by this slot.
1608    pub fn context_identity(&self) -> ExecutionContextIdentity {
1609        self.slot.context_identity()
1610    }
1611
1612    /// Return this engine's runtime event domain.
1613    pub fn event_domain_id(&self) -> EventDomainId {
1614        self.slot.metadata().event_domain_id
1615    }
1616
1617    pub(super) fn executable_witness(&self) -> Option<&'a Arc<ExecutableEngineSnapshot>> {
1618        self.slot.executable()
1619    }
1620
1621    /// Return the hardware class for this slot.
1622    pub fn hardware_class(&self) -> &'a HardwareClassId {
1623        self.slot.hardware_class()
1624    }
1625
1626    /// Return direct core capability slots for this engine.
1627    pub fn capabilities(&self) -> &'a CoreCapabilityBundle {
1628        self.slot.capabilities()
1629    }
1630
1631    pub(super) fn storage_classes(&self) -> &'a [StorageClass] {
1632        self.slot.storage_classes()
1633    }
1634
1635    pub(super) fn default_storage_class(&self) -> &'a StorageClass {
1636        self.slot.default_storage_class()
1637    }
1638
1639    pub(super) fn accepts_input_signature(
1640        &self,
1641        input: &super::InputSignatureEntry,
1642        storage_class: &StorageClass,
1643    ) -> bool {
1644        self.slot
1645            .executable()
1646            .is_some_and(|snapshot| snapshot.accepts_input_signature(input, storage_class))
1647    }
1648
1649    #[cfg(test)]
1650    pub(crate) fn has_execution_engine_for_test(&self) -> bool {
1651        self.slot.executable().is_some()
1652    }
1653}
1654
1655impl fmt::Debug for EngineSnapshotView<'_> {
1656    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1657        formatter
1658            .debug_struct("EngineSnapshotView")
1659            .field("engine_id", self.engine_id())
1660            .field("registration_identity", &self.registration_identity())
1661            .field("context_identity", &self.context_identity())
1662            .field("hardware_class", self.hardware_class())
1663            .field("capabilities", self.capabilities())
1664            .finish()
1665    }
1666}
1667
1668fn default_execution_policy() -> ExecutionPolicy {
1669    ExecutionPolicy::new(super::Determinism::Fast, None, 0)
1670}
1671
1672fn register_engine_candidate(
1673    candidate: &mut CandidateConfig,
1674    registration: EngineRegistration,
1675    changed: &mut bool,
1676) -> Result<(), RuntimeConfigError> {
1677    let engine_id = registration.engine_id().clone();
1678    match candidate.engines.get(&engine_id) {
1679        Some(existing) if existing.registration.candidate_identical(&registration) => Ok(()),
1680        Some(_) => Err(RuntimeConfigError::DuplicateEngine { engine_id }),
1681        None => {
1682            ensure_unique_provider_device_target(candidate, &registration)?;
1683            candidate.engines.insert(
1684                engine_id,
1685                CandidateEngineRecord {
1686                    registration,
1687                    identity: CandidateRegistrationIdentity::New,
1688                },
1689            );
1690            *changed = true;
1691            Ok(())
1692        }
1693    }
1694}
1695
1696fn replace_engine_candidate(
1697    candidate: &mut CandidateConfig,
1698    registration: EngineRegistration,
1699    changed: &mut bool,
1700) -> Result<(), RuntimeConfigError> {
1701    let engine_id = registration.engine_id().clone();
1702    let Some(existing) = candidate.engines.get(&engine_id) else {
1703        return Err(RuntimeConfigError::MissingEngine { engine_id });
1704    };
1705    if existing.registration.candidate_identical(&registration) {
1706        return Ok(());
1707    }
1708    if existing.registration.provider_device_identity() != registration.provider_device_identity() {
1709        return Err(RuntimeConfigError::EngineTargetRebind {
1710            engine_id,
1711            current: existing.registration.provider_device_identity().clone(),
1712            replacement: registration.provider_device_identity().clone(),
1713        });
1714    }
1715    ensure_unique_provider_device_target_except(candidate, &registration, &engine_id)?;
1716    candidate.engines.insert(
1717        engine_id,
1718        CandidateEngineRecord {
1719            registration,
1720            identity: CandidateRegistrationIdentity::New,
1721        },
1722    );
1723    *changed = true;
1724    Ok(())
1725}
1726
1727fn remove_engine_candidate(
1728    candidate: &mut CandidateConfig,
1729    id: &EngineId,
1730    changed: &mut bool,
1731) -> Result<(), RuntimeConfigError> {
1732    match candidate.engines.remove(id) {
1733        Some(_) => {
1734            *changed = true;
1735            Ok(())
1736        }
1737        None => Err(RuntimeConfigError::MissingEngine {
1738            engine_id: id.clone(),
1739        }),
1740    }
1741}
1742
1743fn install_extension_module_candidate(
1744    candidate: &mut CandidateConfig,
1745    module: Arc<dyn ExtensionModule>,
1746    changed: &mut bool,
1747) -> Result<(), RuntimeConfigError> {
1748    let module_id = module.module_id().clone();
1749    match candidate.modules.get(&module_id) {
1750        Some(existing) if existing.module_identical(&module) => Ok(()),
1751        Some(_) => Err(RuntimeConfigError::ExtensionModule {
1752            source: ExtensionModuleError::ConflictingModule { module_id },
1753        }),
1754        None => {
1755            let record = configure_module(module)
1756                .map_err(|source| RuntimeConfigError::ExtensionModule { source })?;
1757            candidate.modules.insert(module_id, record);
1758            *changed = true;
1759            Ok(())
1760        }
1761    }
1762}
1763
1764fn replace_extension_module_candidate(
1765    candidate: &mut CandidateConfig,
1766    module: Arc<dyn ExtensionModule>,
1767    changed: &mut bool,
1768) -> Result<(), RuntimeConfigError> {
1769    let module_id = module.module_id().clone();
1770    match candidate.modules.get(&module_id) {
1771        Some(existing) if existing.module_identical(&module) => Ok(()),
1772        _ => {
1773            let record = configure_module(module)
1774                .map_err(|source| RuntimeConfigError::ExtensionModule { source })?;
1775            candidate.modules.insert(module_id, record);
1776            *changed = true;
1777            Ok(())
1778        }
1779    }
1780}
1781
1782fn remove_extension_module_candidate(
1783    candidate: &mut CandidateConfig,
1784    id: &ExtensionModuleId,
1785    changed: &mut bool,
1786) -> Result<(), RuntimeConfigError> {
1787    if candidate.modules.remove(id).is_some() {
1788        *changed = true;
1789    }
1790    Ok(())
1791}
1792
1793fn register_transfer_provider_candidate(
1794    candidate: &mut CandidateConfig,
1795    source: TransferEndpoint,
1796    destination: TransferEndpoint,
1797    provider: Arc<dyn TransferProvider>,
1798    changed: &mut bool,
1799) -> Result<(), RuntimeConfigError> {
1800    let key = TransferRoute::new(source, destination);
1801    match candidate.transfers.get(&key) {
1802        Some(existing) if Arc::ptr_eq(&existing.provider, &provider) => Ok(()),
1803        Some(_) => Err(RuntimeConfigError::ConflictingRegistration {
1804            key: RegistrationKey::TransferProvider {
1805                source: key.source().clone(),
1806                destination: key.destination().clone(),
1807            },
1808        }),
1809        None => {
1810            candidate.transfers.insert(
1811                key,
1812                CandidateTransferRecord {
1813                    provider,
1814                    binding: CandidateTransferBinding::New,
1815                },
1816            );
1817            *changed = true;
1818            Ok(())
1819        }
1820    }
1821}
1822
1823fn remove_transfer_provider_candidate(
1824    candidate: &mut CandidateConfig,
1825    source: TransferEndpoint,
1826    destination: TransferEndpoint,
1827    changed: &mut bool,
1828) -> Result<(), RuntimeConfigError> {
1829    let key = TransferRoute::new(source, destination);
1830    if candidate.transfers.remove(&key).is_none() {
1831        return Err(RuntimeConfigError::MissingTransferProvider {
1832            source_endpoint: key.source().clone(),
1833            destination: key.destination().clone(),
1834        });
1835    }
1836    *changed = true;
1837    Ok(())
1838}
1839
1840fn ensure_unique_provider_device_target(
1841    candidate: &CandidateConfig,
1842    registration: &EngineRegistration,
1843) -> Result<(), RuntimeConfigError> {
1844    ensure_unique_provider_device_target_except(candidate, registration, registration.engine_id())
1845}
1846
1847fn ensure_unique_provider_device_target_except(
1848    candidate: &CandidateConfig,
1849    registration: &EngineRegistration,
1850    ignored_engine_id: &EngineId,
1851) -> Result<(), RuntimeConfigError> {
1852    if let Some((first_engine_id, _)) = candidate.engines.iter().find(|(engine_id, record)| {
1853        *engine_id != ignored_engine_id
1854            && record.registration.provider_device_identity()
1855                == registration.provider_device_identity()
1856    }) {
1857        return Err(RuntimeConfigError::DuplicateProviderDeviceTarget {
1858            provider_device_identity: registration.provider_device_identity().clone(),
1859            first_engine_id: first_engine_id.clone(),
1860            duplicate_engine_id: registration.engine_id().clone(),
1861        });
1862    }
1863    Ok(())
1864}
1865
1866fn validate_candidate(
1867    candidate: CandidateConfig,
1868    issuer: NonZeroU64,
1869    next_ordinal: NonZeroU64,
1870) -> Result<(BoundCandidateConfig, NonZeroU64), RuntimeConfigError> {
1871    let mut seen_targets = BTreeMap::<ProviderDeviceIdentity, EngineId>::new();
1872    for (engine_id, record) in &candidate.engines {
1873        if let Some(first_engine_id) = seen_targets.insert(
1874            record.registration.provider_device_identity().clone(),
1875            engine_id.clone(),
1876        ) {
1877            return Err(RuntimeConfigError::DuplicateProviderDeviceTarget {
1878                provider_device_identity: record.registration.provider_device_identity().clone(),
1879                first_engine_id,
1880                duplicate_engine_id: engine_id.clone(),
1881            });
1882        }
1883    }
1884
1885    let mut bound_transfers = BTreeMap::new();
1886    for (route, record) in &candidate.transfers {
1887        let source_binding = validate_transfer_endpoint(&candidate, route.source())?;
1888        let destination_binding = validate_transfer_endpoint(&candidate, route.destination())?;
1889        let preserved = match &record.binding {
1890            CandidateTransferBinding::New => None,
1891            CandidateTransferBinding::Preserved {
1892                source,
1893                destination,
1894            } => Some((source, destination)),
1895        };
1896        if let Some((registered_source, registered_destination)) = preserved {
1897            if registered_source != &source_binding {
1898                return Err(RuntimeConfigError::StaleTransferRoute {
1899                    source_endpoint: route.source().clone(),
1900                    destination: route.destination().clone(),
1901                    endpoint: route.source().clone(),
1902                    registered: Box::new(registered_source.clone()),
1903                    current: Box::new(source_binding.clone()),
1904                });
1905            }
1906            if registered_destination != &destination_binding {
1907                return Err(RuntimeConfigError::StaleTransferRoute {
1908                    source_endpoint: route.source().clone(),
1909                    destination: route.destination().clone(),
1910                    endpoint: route.destination().clone(),
1911                    registered: Box::new(registered_destination.clone()),
1912                    current: Box::new(destination_binding.clone()),
1913                });
1914            }
1915        }
1916        bound_transfers.insert(
1917            route.clone(),
1918            BoundCandidateTransferRecord {
1919                provider: Arc::clone(&record.provider),
1920                source: source_binding,
1921                destination: destination_binding,
1922            },
1923        );
1924    }
1925    let mut seen = BTreeMap::<(ExtensionFamilyId, EngineId), ExtensionModuleId>::new();
1926    for (module_id, module) in &candidate.modules {
1927        for family_engine in module.engines.keys() {
1928            if seen
1929                .insert(
1930                    (family_engine.0, family_engine.1.clone()),
1931                    module_id.clone(),
1932                )
1933                .is_some()
1934            {
1935                return Err(RuntimeConfigError::ConflictingRegistration {
1936                    key: RegistrationKey::ExtensionEngine {
1937                        family: family_engine.0,
1938                        engine: family_engine.1.clone(),
1939                    },
1940                });
1941            }
1942        }
1943    }
1944
1945    let CandidateConfig {
1946        policy,
1947        engines,
1948        modules,
1949        transfers: _,
1950    } = candidate;
1951    let mut allocator = RegistrationIdentityAllocator::new(issuer, next_ordinal);
1952    let engines = engines
1953        .into_iter()
1954        .map(|(engine_id, record)| {
1955            let identity = match record.identity {
1956                CandidateRegistrationIdentity::New => allocator.allocate()?,
1957                CandidateRegistrationIdentity::Preserved(identity) => identity,
1958            };
1959            Ok((
1960                engine_id,
1961                BoundCandidateEngineRecord {
1962                    registration: record.registration,
1963                    identity,
1964                },
1965            ))
1966        })
1967        .collect::<Result<BTreeMap<_, _>, RuntimeConfigError>>()?;
1968    let modules = modules
1969        .into_iter()
1970        .map(|(module_id, module)| {
1971            let mut allocate = || allocator.allocate();
1972            Ok((module_id, bind_candidate_module(module, &mut allocate)?))
1973        })
1974        .collect::<Result<BTreeMap<_, _>, RuntimeConfigError>>()?;
1975    Ok((
1976        BoundCandidateConfig {
1977            policy,
1978            engines,
1979            modules,
1980            transfers: bound_transfers,
1981        },
1982        allocator.next_ordinal(),
1983    ))
1984}
1985
1986fn validate_transfer_endpoint(
1987    candidate: &CandidateConfig,
1988    endpoint: &TransferEndpoint,
1989) -> Result<ProviderDeviceIdentity, RuntimeConfigError> {
1990    let Some(engine) = candidate.engines.get(endpoint.engine_id()) else {
1991        return Err(RuntimeConfigError::UnknownTransferEndpointEngine {
1992            endpoint: endpoint.clone(),
1993        });
1994    };
1995    if !engine
1996        .registration
1997        .storage_classes()
1998        .contains(endpoint.storage_class())
1999    {
2000        return Err(RuntimeConfigError::UnsupportedTransferEndpointStorage {
2001            endpoint: endpoint.clone(),
2002        });
2003    }
2004    Ok(engine.registration.provider_device_identity().clone())
2005}
2006
2007struct RegistrationIdentityAllocator {
2008    issuer: NonZeroU64,
2009    next: NonZeroU64,
2010}
2011
2012impl RegistrationIdentityAllocator {
2013    fn new(issuer: NonZeroU64, next: NonZeroU64) -> Self {
2014        Self { issuer, next }
2015    }
2016
2017    fn allocate(&mut self) -> Result<RegistrationIdentity, RuntimeConfigError> {
2018        let identity = RegistrationIdentity::new(self.issuer, self.next);
2019        let next = self
2020            .next
2021            .get()
2022            .checked_add(1)
2023            .and_then(NonZeroU64::new)
2024            .ok_or(RuntimeConfigError::IdentityExhausted)?;
2025        self.next = next;
2026        Ok(identity)
2027    }
2028
2029    fn next_ordinal(&self) -> NonZeroU64 {
2030        self.next
2031    }
2032}
2033
2034fn freeze_candidate(
2035    runtime_id: RuntimeId,
2036    epoch: RuntimeEpoch,
2037    candidate: BoundCandidateConfig,
2038) -> Result<RuntimeConfigSnapshot, RuntimeConfigError> {
2039    let mut engines = Vec::with_capacity(candidate.engines.len());
2040    let mut engine_indices = BTreeMap::new();
2041    let mut engine_locations = BTreeMap::new();
2042    let mut cache_owners = Vec::new();
2043    for (index, (engine_id, record)) in candidate.engines.into_iter().enumerate() {
2044        let BoundCandidateEngineRecord {
2045            registration,
2046            identity,
2047        } = record;
2048        let event_domain_id = EventDomainId::new(runtime_id, epoch, identity);
2049        let (state, candidate_token) = registration.into_state_and_token();
2050        let provider_device_identity = state.provider_device_identity().clone();
2051        let metadata = FrozenEngineMetadata {
2052            candidate_token,
2053            identity,
2054            event_domain_id,
2055        };
2056        let frozen = match state {
2057            EngineRegistrationState::PreparationOnly { binding } => {
2058                FrozenEngineSlot::PreparationOnly(Arc::new(PreparationOnlyEngineSnapshot {
2059                    metadata,
2060                    binding,
2061                }))
2062            }
2063            EngineRegistrationState::Executable(binding) => {
2064                if let Some(owner) = binding.contract().cache_owner().cloned() {
2065                    cache_owners.push(FrozenCacheOwner {
2066                        id: engine_cache_owner_id(&engine_id),
2067                        kind: FrozenCacheOwnerKind::Engine,
2068                        owner,
2069                    });
2070                }
2071                cache_owners.push(FrozenCacheOwner {
2072                    id: engine_extension_cache_owner_id(&engine_id),
2073                    kind: FrozenCacheOwnerKind::Extension,
2074                    owner: execution::extension_cache_owner(binding.contract().executor().clone()),
2075                });
2076                FrozenEngineSlot::Executable(Arc::new(ExecutableEngineSnapshot {
2077                    metadata,
2078                    binding,
2079                }))
2080            }
2081        };
2082        engine_locations.insert(
2083            engine_id.clone(),
2084            (provider_device_identity, event_domain_id),
2085        );
2086        engine_indices.insert(engine_id, index);
2087        engines.push(frozen);
2088    }
2089    let extensions = freeze_extension_slots(candidate.modules)?;
2090    for (id, owner) in extensions.cache_owner_records() {
2091        cache_owners.push(FrozenCacheOwner {
2092            id,
2093            kind: FrozenCacheOwnerKind::Extension,
2094            owner,
2095        });
2096    }
2097    let mut transfers = BTreeMap::new();
2098    for (route, record) in candidate.transfers {
2099        let BoundCandidateTransferRecord {
2100            provider,
2101            source: source_binding,
2102            destination: destination_binding,
2103        } = record;
2104        let (_, source_event_domain_id) = bound_engine_location(&engine_locations, route.source())?;
2105        let (_, destination_event_domain_id) =
2106            bound_engine_location(&engine_locations, route.destination())?;
2107        let resolved_route = ResolvedTransferRoute::new(
2108            ResolvedTransferEndpoint::new(
2109                route.source().clone(),
2110                source_binding,
2111                *source_event_domain_id,
2112            ),
2113            ResolvedTransferEndpoint::new(
2114                route.destination().clone(),
2115                destination_binding,
2116                *destination_event_domain_id,
2117            ),
2118        );
2119        transfers.insert(resolved_route, provider);
2120    }
2121    Ok(RuntimeConfigSnapshot {
2122        runtime_id,
2123        epoch,
2124        policy: candidate.policy,
2125        engines: engines.into(),
2126        engine_indices,
2127        extensions,
2128        transfers: FrozenTransferRegistry::new(transfers),
2129        cache_owners: cache_owners.into(),
2130    })
2131}
2132
2133fn bound_engine_location<'a>(
2134    locations: &'a BTreeMap<EngineId, (ProviderDeviceIdentity, EventDomainId)>,
2135    endpoint: &TransferEndpoint,
2136) -> Result<&'a (ProviderDeviceIdentity, EventDomainId), RuntimeConfigError> {
2137    locations
2138        .get(endpoint.engine_id())
2139        .ok_or_else(|| RuntimeConfigError::BoundCandidateInvariant {
2140            endpoint: endpoint.clone(),
2141        })
2142}
2143
2144fn engine_cache_owner_id(engine_id: &EngineId) -> CacheOwnerId {
2145    let id = engine_id.as_str();
2146    CacheOwnerId::from_canonical_owner_id(Arc::<str>::from(format!("engine[{}]:{id}", id.len())))
2147}
2148
2149fn engine_extension_cache_owner_id(engine_id: &EngineId) -> CacheOwnerId {
2150    let id = engine_id.as_str();
2151    CacheOwnerId::from_canonical_owner_id(Arc::<str>::from(format!(
2152        "extension-executor[{}]:{id}",
2153        id.len()
2154    )))
2155}
2156
2157fn allocate_nonzero(counter: &AtomicU64) -> Result<NonZeroU64, RuntimeConfigError> {
2158    let value = counter
2159        .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |next| {
2160            next.checked_add(1)
2161        })
2162        .map_err(|_| RuntimeConfigError::IdentityExhausted)?;
2163    NonZeroU64::new(value).ok_or(RuntimeConfigError::IdentityExhausted)
2164}
2165
2166#[cfg(test)]
2167mod freeze_tests {
2168    use crate::{ProviderId, TransferRequest};
2169
2170    use super::*;
2171
2172    #[derive(Debug)]
2173    struct FreezeTestContext;
2174
2175    #[derive(Debug)]
2176    struct FreezeTestProvider;
2177
2178    impl TransferProvider for FreezeTestProvider {
2179        fn transfer_blocking(
2180            &self,
2181            _request: TransferRequest<'_>,
2182        ) -> crate::Result<tenferro_tensor::Tensor> {
2183            Err(crate::Error::Internal("freeze test provider".into()))
2184        }
2185    }
2186
2187    fn registration(
2188        engine_id: &str,
2189        target: &str,
2190    ) -> Result<EngineRegistration, RuntimeConfigError> {
2191        let engine_id = EngineId::new(engine_id).map_err(RuntimeConfigError::from)?;
2192        let storage =
2193            StorageClass::new("tenferro.test.freeze.storage").map_err(RuntimeConfigError::from)?;
2194        Ok(EngineRegistration::preparation_only(
2195            super::super::ProviderPreparationBinding::new(
2196                engine_id,
2197                ProviderDeviceIdentity::new(
2198                    ProviderId::new("tenferro.test.freeze.provider")
2199                        .map_err(RuntimeConfigError::from)?,
2200                    target,
2201                )
2202                .map_err(RuntimeConfigError::from)?,
2203                ExecutionContextIdentity::of::<FreezeTestContext>(),
2204                HardwareClassId::new("tenferro.test.freeze.hardware")
2205                    .map_err(RuntimeConfigError::from)?,
2206                Arc::from(vec![storage.clone()]),
2207                storage,
2208                CoreCapabilityBundle::default(),
2209            )?,
2210        ))
2211    }
2212
2213    fn candidate(binding: CandidateTransferBinding) -> Result<CandidateConfig, RuntimeConfigError> {
2214        let source_id =
2215            EngineId::new("tenferro.test.freeze.source").map_err(RuntimeConfigError::from)?;
2216        let destination_id =
2217            EngineId::new("tenferro.test.freeze.destination").map_err(RuntimeConfigError::from)?;
2218        let storage =
2219            StorageClass::new("tenferro.test.freeze.storage").map_err(RuntimeConfigError::from)?;
2220        let source_endpoint = TransferEndpoint::new(source_id.clone(), storage.clone());
2221        let destination_endpoint = TransferEndpoint::new(destination_id.clone(), storage);
2222        let mut candidate = CandidateConfig::empty();
2223        let mut changed = false;
2224        register_engine_candidate(
2225            &mut candidate,
2226            registration(source_id.as_str(), "freeze-source")?,
2227            &mut changed,
2228        )?;
2229        register_engine_candidate(
2230            &mut candidate,
2231            registration(destination_id.as_str(), "freeze-destination")?,
2232            &mut changed,
2233        )?;
2234        register_transfer_provider_candidate(
2235            &mut candidate,
2236            source_endpoint.clone(),
2237            destination_endpoint.clone(),
2238            Arc::new(FreezeTestProvider),
2239            &mut changed,
2240        )?;
2241        candidate
2242            .transfers
2243            .get_mut(&TransferRoute::new(source_endpoint, destination_endpoint))
2244            .expect("registered route")
2245            .binding = binding;
2246        Ok(candidate)
2247    }
2248
2249    #[test]
2250    fn validation_owns_stale_route_rejection_and_bound_freeze_is_total() {
2251        let wrong_source = ProviderDeviceIdentity::new(
2252            ProviderId::new("tenferro.test.freeze.provider").unwrap(),
2253            "different-source",
2254        )
2255        .unwrap();
2256        let preserved = CandidateTransferBinding::Preserved {
2257            source: wrong_source,
2258            destination: ProviderDeviceIdentity::new(
2259                ProviderId::new("tenferro.test.freeze.provider").unwrap(),
2260                "freeze-destination",
2261            )
2262            .unwrap(),
2263        };
2264        let result = validate_candidate(
2265            candidate(preserved).unwrap(),
2266            NonZeroU64::new(1).unwrap(),
2267            NonZeroU64::new(1).unwrap(),
2268        );
2269        let error = match result {
2270            Ok(_) => panic!("candidate validation must reject stale preserved bindings"),
2271            Err(error) => error,
2272        };
2273        assert!(matches!(
2274            error,
2275            RuntimeConfigError::StaleTransferRoute { .. }
2276        ));
2277
2278        let (bound, _) = validate_candidate(
2279            candidate(CandidateTransferBinding::New).unwrap(),
2280            NonZeroU64::new(1).unwrap(),
2281            NonZeroU64::new(1).unwrap(),
2282        )
2283        .expect("validation must produce a complete bound candidate");
2284        freeze_candidate(
2285            RuntimeId::from_nonzero(NonZeroU64::new(1).unwrap()),
2286            RuntimeEpoch::one(),
2287            bound,
2288        )
2289        .expect("a bound candidate must freeze without semantic route revalidation");
2290    }
2291
2292    #[test]
2293    fn frozen_engine_slots_are_arc_sized() {
2294        let slot_size = std::mem::size_of::<FrozenEngineSlot>();
2295        let arc_size = std::mem::size_of::<Arc<()>>();
2296
2297        assert!(
2298            slot_size <= 2 * arc_size,
2299            "frozen engine slots should keep immutable snapshot payloads behind Arc: slot_size={slot_size}, arc_size={arc_size}",
2300        );
2301    }
2302}