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