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 New,
55 Preserved {
57 source: ProviderDeviceIdentity,
58 destination: ProviderDeviceIdentity,
59 },
60}
61
62#[derive(Clone, Debug)]
63struct CandidateTransferRecord {
64 provider: Arc<dyn TransferProvider>,
65 binding: CandidateTransferBinding,
66}
67
68struct BoundCandidateTransferRecord {
69 provider: Arc<dyn TransferProvider>,
70 source: ProviderDeviceIdentity,
71 destination: ProviderDeviceIdentity,
72}
73
74#[derive(Clone, Debug)]
75struct CandidateConfig {
76 policy: ExecutionPolicy,
77 engines: BTreeMap<EngineId, CandidateEngineRecord>,
78 modules: BTreeMap<ExtensionModuleId, CandidateModuleRecord>,
79 transfers: BTreeMap<TransferRoute, CandidateTransferRecord>,
80}
81
82struct BoundCandidateConfig {
83 policy: ExecutionPolicy,
84 engines: BTreeMap<EngineId, BoundCandidateEngineRecord>,
85 modules: BTreeMap<ExtensionModuleId, BoundCandidateModuleRecord>,
86 transfers: BTreeMap<TransferRoute, BoundCandidateTransferRecord>,
87}
88
89impl CandidateConfig {
90 fn empty() -> Self {
91 Self {
92 policy: default_execution_policy(),
93 engines: BTreeMap::new(),
94 modules: BTreeMap::new(),
95 transfers: BTreeMap::new(),
96 }
97 }
98
99 fn from_snapshot(snapshot: &RuntimeConfigSnapshot) -> Result<Self, RuntimeConfigError> {
100 let engines = snapshot
101 .engines
102 .iter()
103 .map(|slot| {
104 let registration = slot.to_registration()?;
105 Ok((
106 registration.engine_id().clone(),
107 CandidateEngineRecord {
108 registration,
109 identity: CandidateRegistrationIdentity::Preserved(
110 slot.metadata().identity,
111 ),
112 },
113 ))
114 })
115 .collect::<Result<BTreeMap<_, _>, RuntimeConfigError>>()?;
116 Ok(Self {
117 policy: snapshot.policy.clone(),
118 engines,
119 modules: snapshot.extensions.to_candidate_modules(),
120 transfers: snapshot
121 .transfers
122 .iter()
123 .map(|(resolved_route, provider)| {
124 (
125 TransferRoute::new(
126 resolved_route.source().logical().clone(),
127 resolved_route.destination().logical().clone(),
128 ),
129 CandidateTransferRecord {
130 provider: Arc::clone(provider),
131 binding: CandidateTransferBinding::Preserved {
132 source: resolved_route.source().provider_device_identity().clone(),
133 destination: resolved_route
134 .destination()
135 .provider_device_identity()
136 .clone(),
137 },
138 },
139 )
140 })
141 .collect(),
142 })
143 }
144}
145
146#[derive(Clone, Debug)]
147struct FrozenEngineMetadata {
148 candidate_token: Arc<CandidateRegistrationToken>,
149 identity: RegistrationIdentity,
150 event_domain_id: EventDomainId,
151}
152
153#[derive(Clone)]
154struct PreparationOnlyEngineSnapshot {
155 metadata: FrozenEngineMetadata,
156 binding: super::ProviderPreparationBinding,
157}
158
159#[derive(Clone, Debug)]
160pub(super) struct ExecutableEngineSnapshot {
161 metadata: FrozenEngineMetadata,
162 binding: super::ProviderExecutableBinding,
163}
164
165#[derive(Clone)]
166enum FrozenEngineSlot {
167 PreparationOnly(Arc<PreparationOnlyEngineSnapshot>),
168 Executable(Arc<ExecutableEngineSnapshot>),
169}
170
171impl FrozenEngineSlot {
172 fn metadata(&self) -> &FrozenEngineMetadata {
173 match self {
174 Self::PreparationOnly(snapshot) => &snapshot.metadata,
175 Self::Executable(snapshot) => &snapshot.metadata,
176 }
177 }
178
179 fn provider_device_identity(&self) -> &ProviderDeviceIdentity {
180 match self {
181 Self::PreparationOnly(snapshot) => snapshot.binding.provider_device_identity(),
182 Self::Executable(snapshot) => snapshot.binding.contract().provider_device_identity(),
183 }
184 }
185
186 fn engine_id(&self) -> &EngineId {
187 match self {
188 Self::PreparationOnly(snapshot) => snapshot.binding.engine_id(),
189 Self::Executable(snapshot) => snapshot.binding.engine_id(),
190 }
191 }
192
193 fn hardware_class(&self) -> &HardwareClassId {
194 match self {
195 Self::PreparationOnly(snapshot) => snapshot.binding.hardware_class(),
196 Self::Executable(snapshot) => snapshot.binding.hardware_class(),
197 }
198 }
199
200 fn storage_classes(&self) -> &[StorageClass] {
201 match self {
202 Self::PreparationOnly(snapshot) => snapshot.binding.storage_classes(),
203 Self::Executable(snapshot) => snapshot.binding.storage_classes(),
204 }
205 }
206
207 fn default_storage_class(&self) -> &StorageClass {
208 match self {
209 Self::PreparationOnly(snapshot) => snapshot.binding.default_storage_class(),
210 Self::Executable(snapshot) => snapshot.binding.default_storage_class(),
211 }
212 }
213
214 fn context_identity(&self) -> ExecutionContextIdentity {
215 match self {
216 Self::PreparationOnly(snapshot) => snapshot.binding.context_identity(),
217 Self::Executable(snapshot) => snapshot.binding.contract().context_identity(),
218 }
219 }
220
221 fn capabilities(&self) -> &CoreCapabilityBundle {
222 match self {
223 Self::PreparationOnly(snapshot) => snapshot.binding.capabilities(),
224 Self::Executable(snapshot) => snapshot.binding.contract().capabilities(),
225 }
226 }
227
228 fn executable(&self) -> Option<&Arc<ExecutableEngineSnapshot>> {
229 match self {
230 Self::PreparationOnly(_) => None,
231 Self::Executable(snapshot) => Some(snapshot),
232 }
233 }
234}
235
236impl ExecutableEngineSnapshot {
237 pub(super) fn engine_id(&self) -> &EngineId {
238 self.binding.engine_id()
239 }
240
241 pub(super) fn event_domain_id(&self) -> EventDomainId {
242 self.metadata.event_domain_id
243 }
244
245 pub(super) fn provider_device_identity(&self) -> &ProviderDeviceIdentity {
246 self.binding.contract().provider_device_identity()
247 }
248
249 #[cfg(test)]
250 pub(super) fn context_identity(&self) -> ExecutionContextIdentity {
251 self.binding.contract().context_identity()
252 }
253
254 pub(super) fn 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 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#[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 pub fn runtime_id(&self) -> RuntimeId {
431 self.runtime_id
432 }
433
434 pub fn epoch(&self) -> RuntimeEpoch {
436 self.epoch
437 }
438
439 pub fn execution_policy(&self) -> &ExecutionPolicy {
441 &self.policy
442 }
443
444 pub fn engine_count(&self) -> usize {
446 self.engines.len()
447 }
448
449 pub fn extension_module_count(&self) -> usize {
451 self.extensions.module_count()
452 }
453
454 pub fn transfer_provider_count(&self) -> usize {
456 self.transfers.len()
457 }
458
459 #[doc(hidden)]
461 pub fn has_extension_family(&self, family_id: &'static str) -> bool {
462 self.extensions.has_family(family_id)
463 }
464
465 #[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 #[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 #[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 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#[derive(Clone)]
635pub struct Runtime(Arc<RuntimeState>);
636
637impl Runtime {
638 pub fn builder() -> RuntimeConfigBuilder {
640 RuntimeConfigBuilder::new()
641 }
642
643 pub fn id(&self) -> RuntimeId {
645 self.0.runtime_id
646 }
647
648 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 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 pub fn prepared_cache_limits(&self) -> Result<PreparedPlanCacheLimits, RuntimeStateError> {
699 self.0.caches.prepared().limits()
700 }
701
702 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 pub fn clear_prepared_cache(&self) -> Result<(), RuntimeStateError> {
754 self.0.caches.prepared().clear()
755 }
756
757 pub fn cache_stats(&self) -> Result<RuntimeCacheStats, RuntimeCacheError> {
777 super::preparation::cache_stats(self, &self.0.caches)
778 }
779
780 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 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 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 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 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 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
1199pub struct RuntimeConfigBuilder {
1213 candidate: CandidateConfig,
1214}
1215
1216impl RuntimeConfigBuilder {
1217 pub fn new() -> Self {
1219 Self {
1220 candidate: CandidateConfig::empty(),
1221 }
1222 }
1223
1224 pub fn execution_policy(&mut self, value: ExecutionPolicy) -> &mut Self {
1226 self.candidate.policy = value;
1227 self
1228 }
1229
1230 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 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 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 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 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 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 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 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 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
1477pub struct RuntimeReconfiguration<'a> {
1479 candidate: &'a mut CandidateConfig,
1480 changed: &'a mut bool,
1481}
1482
1483impl RuntimeReconfiguration<'_> {
1484 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 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 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 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 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 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 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 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 #[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 #[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 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#[derive(Clone, Copy)]
1893pub struct EngineSnapshotView<'a> {
1894 slot: &'a FrozenEngineSlot,
1895}
1896
1897impl<'a> EngineSnapshotView<'a> {
1898 pub fn engine_id(&self) -> &'a EngineId {
1900 self.slot.engine_id()
1901 }
1902
1903 pub fn provider_device_identity(&self) -> &'a super::ProviderDeviceIdentity {
1913 self.slot.provider_device_identity()
1914 }
1915
1916 pub fn registration_identity(&self) -> RegistrationIdentity {
1918 self.slot.metadata().identity
1919 }
1920
1921 pub fn context_identity(&self) -> ExecutionContextIdentity {
1923 self.slot.context_identity()
1924 }
1925
1926 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 pub fn hardware_class(&self) -> &'a HardwareClassId {
1937 self.slot.hardware_class()
1938 }
1939
1940 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 #[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(®istration) => Ok(()),
2028 Some(_) => Err(RuntimeConfigError::DuplicateEngine { engine_id }),
2029 None => {
2030 ensure_unique_provider_device_target(candidate, ®istration)?;
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(®istration) {
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, ®istration, &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}