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