1use std::collections::BTreeMap;
2use std::fmt;
3use std::num::NonZeroU64;
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::sync::{Arc, RwLock};
6
7use tenferro_tensor::{Tensor, TensorValue};
8
9use crate::graph::CompiledGraph;
10use crate::program::FrozenProgram;
11
12use super::cache::{PreparedPlanCacheLimits, RuntimeCacheSet};
13use super::cache_owner::{FrozenCacheOwner, FrozenCacheOwnerKind};
14use super::engine_registration::{CandidateRegistrationToken, EngineRegistrationState};
15use super::execution;
16#[cfg(test)]
17use super::extension::ExtensionSlotFullForTest;
18use super::extension::{
19 bind_candidate_module, configure_module, freeze_extension_slots, BoundCandidateModuleRecord,
20 CandidateModuleRecord, CandidateRegistrationIdentity, ExtensionEngineSnapshotView,
21 ExtensionFamilyId, FrozenExtensionSlots,
22};
23use super::preparation::{PreparedEntryKey, PreparedProgram, PreparedProgramResult};
24use super::schedule::EventDomainId;
25use super::{
26 CacheOwnerId, CoreCapabilityBundle, EngineId, EngineRegistration, ExecutionContextIdentity,
27 ExecutionPolicy, ExtensionModule, ExtensionModuleError, ExtensionModuleId,
28 FrozenTransferRegistry, HardwareClassId, InputSignature, PrepareOptions,
29 ProviderDeviceIdentity, RegistrationIdentity, RegistrationKey, ResolvedTransferEndpoint,
30 ResolvedTransferRoute, RuntimeCacheError, RuntimeCacheStats, RuntimeConfigError, RuntimeEpoch,
31 RuntimeId, RuntimeReconfigureError, RuntimeStateError, StorageClass, TransferEndpoint,
32 TransferProvider, TransferRoute,
33};
34
35static NEXT_RUNTIME_ID: AtomicU64 = AtomicU64::new(1);
36static NEXT_REGISTRATION_ISSUER: AtomicU64 = AtomicU64::new(1);
37const INITIAL_REGISTRATION_ORDINAL: NonZeroU64 = NonZeroU64::MIN;
38
39#[derive(Clone, Debug)]
40struct CandidateEngineRecord {
41 registration: EngineRegistration,
42 identity: CandidateRegistrationIdentity,
43}
44
45#[derive(Clone, Debug)]
46struct BoundCandidateEngineRecord {
47 registration: EngineRegistration,
48 identity: RegistrationIdentity,
49}
50
51#[derive(Clone, Debug)]
52enum CandidateTransferBinding {
53 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 executor(&self) -> &Arc<dyn super::execution::ErasedTensorBackendExecutor> {
255 self.binding.contract().executor()
256 }
257
258 pub(super) fn event_domain_driver(&self) -> &Arc<dyn super::EventDomainDriver> {
259 self.binding.contract().event_domain_driver()
260 }
261
262 #[cfg(test)]
263 pub(super) fn has_executor(&self) -> bool {
264 true
265 }
266
267 #[cfg(test)]
268 pub(super) fn has_event_domain_driver(&self) -> bool {
269 true
270 }
271
272 pub(super) fn accepts_input_placement(
273 &self,
274 placement: &tenferro_tensor::Placement,
275 storage_class: &StorageClass,
276 ) -> bool {
277 self.binding.storage_classes().contains(storage_class)
278 && self
279 .binding
280 .contract()
281 .accepts_input_placement(placement, storage_class)
282 }
283
284 pub(super) fn accepts_input_signature(
285 &self,
286 input: &super::InputSignatureEntry,
287 storage_class: &StorageClass,
288 ) -> bool {
289 self.binding.storage_classes().contains(storage_class)
290 && self
291 .binding
292 .contract()
293 .accepts_input_signature(input, storage_class)
294 }
295
296 pub(super) fn accepts_runtime_input(
297 &self,
298 input: &tenferro_tensor::TensorRead<'_>,
299 storage_class: &StorageClass,
300 ) -> bool {
301 self.binding.storage_classes().contains(storage_class)
302 && self
303 .binding
304 .contract()
305 .accepts_runtime_input(input, storage_class)
306 }
307
308 pub(super) fn owns_resident_tensor(
309 &self,
310 input: &tenferro_tensor::TensorRead<'_>,
311 storage_class: &StorageClass,
312 ) -> bool {
313 self.binding.storage_classes().contains(storage_class)
314 && self
315 .binding
316 .contract()
317 .owns_resident_tensor(input, storage_class)
318 }
319
320 #[cfg(test)]
321 pub(super) fn for_test(
322 engine_id: EngineId,
323 provider_device_identity: ProviderDeviceIdentity,
324 event_domain_id: EventDomainId,
325 storage_class: StorageClass,
326 ) -> Arc<Self> {
327 Self::for_test_with_driver(
328 engine_id,
329 provider_device_identity,
330 event_domain_id,
331 storage_class,
332 Arc::new(super::ImmediateEventDomainDriver::new()),
333 )
334 }
335
336 #[cfg(test)]
337 pub(super) fn for_test_with_driver(
338 engine_id: EngineId,
339 provider_device_identity: ProviderDeviceIdentity,
340 event_domain_id: EventDomainId,
341 storage_class: StorageClass,
342 event_domain_driver: Arc<dyn super::EventDomainDriver>,
343 ) -> Arc<Self> {
344 let ingress = super::InputIngressContract::new(
345 super::InputPlacementContract::new(|_, _| true),
346 super::InputSignatureContract::new(|_, _, _, _| true),
347 super::RuntimeInputContract::new(|_, _| true),
348 super::ResidentOutputContract::new(|_, _| true),
349 );
350 let contract = super::ExecutableEngineContract::new(
351 provider_device_identity,
352 CoreCapabilityBundle::default(),
353 tenferro_cpu::CpuBackend::new(),
354 event_domain_driver,
355 ingress,
356 None,
357 );
358 let binding = super::ProviderExecutableBinding::new(
359 engine_id,
360 HardwareClassId::new("tenferro.test.schedule.hardware").expect("test hardware class"),
361 Arc::from(vec![storage_class.clone()]),
362 storage_class,
363 contract,
364 )
365 .expect("test executable binding");
366 Arc::new(Self {
367 metadata: FrozenEngineMetadata {
368 candidate_token: Arc::new(CandidateRegistrationToken),
369 identity: event_domain_id.registration_identity(),
370 event_domain_id,
371 },
372 binding,
373 })
374 }
375}
376
377impl FrozenEngineSlot {
378 fn to_registration(&self) -> Result<EngineRegistration, RuntimeConfigError> {
379 let metadata = self.metadata();
380 let registration = match self {
381 Self::PreparationOnly(snapshot) => {
382 EngineRegistration::from_state(EngineRegistrationState::PreparationOnly {
383 binding: snapshot.binding.clone(),
384 })
385 }
386 Self::Executable(snapshot) => EngineRegistration::from_state(
387 EngineRegistrationState::Executable(snapshot.binding.clone()),
388 ),
389 };
390 Ok(registration.with_candidate_token(Arc::clone(&metadata.candidate_token)))
391 }
392}
393
394impl fmt::Debug for FrozenEngineSlot {
395 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
396 let metadata = self.metadata();
397 formatter
398 .debug_struct("FrozenEngineSlot")
399 .field("engine_id", self.engine_id())
400 .field("registration_identity", &metadata.identity)
401 .field("event_domain_id", &metadata.event_domain_id)
402 .field("context_identity", &self.context_identity())
403 .field("hardware_class", self.hardware_class())
404 .field(
405 "state",
406 &match self {
407 Self::PreparationOnly(_) => "preparation-only",
408 Self::Executable(_) => "executable",
409 },
410 )
411 .finish_non_exhaustive()
412 }
413}
414
415#[derive(Clone)]
433pub struct RuntimeConfigSnapshot {
434 runtime_id: RuntimeId,
435 epoch: RuntimeEpoch,
436 policy: ExecutionPolicy,
437 engines: Arc<[FrozenEngineSlot]>,
438 engine_indices: BTreeMap<EngineId, usize>,
439 extensions: FrozenExtensionSlots,
440 transfers: FrozenTransferRegistry,
441 cache_owners: Arc<[FrozenCacheOwner]>,
442}
443
444impl RuntimeConfigSnapshot {
445 pub fn runtime_id(&self) -> RuntimeId {
447 self.runtime_id
448 }
449
450 pub fn epoch(&self) -> RuntimeEpoch {
452 self.epoch
453 }
454
455 pub fn execution_policy(&self) -> &ExecutionPolicy {
457 &self.policy
458 }
459
460 pub fn engine_count(&self) -> usize {
462 self.engines.len()
463 }
464
465 pub fn extension_module_count(&self) -> usize {
467 self.extensions.module_count()
468 }
469
470 pub fn transfer_provider_count(&self) -> usize {
472 self.transfers.len()
473 }
474
475 #[doc(hidden)]
477 pub fn has_extension_family(&self, family_id: &'static str) -> bool {
478 self.extensions.has_family(family_id)
479 }
480
481 #[doc(hidden)]
500 pub fn has_extension_engine(&self, family_id: &'static str, engine_id: &EngineId) -> bool {
501 self.extensions.has_engine(family_id, engine_id)
502 }
503
504 pub fn engine(&self, id: &EngineId) -> Option<EngineSnapshotView<'_>> {
506 self.engine_indices
507 .get(id)
508 .map(|&index| EngineSnapshotView {
509 slot: &self.engines[index],
510 })
511 }
512
513 #[cfg(test)]
514 pub(crate) fn engine_ids_for_test(&self) -> impl Iterator<Item = &EngineId> {
515 self.engines.iter().map(FrozenEngineSlot::engine_id)
516 }
517
518 #[cfg(test)]
519 pub(crate) fn transfer_routes_for_test(&self) -> impl Iterator<Item = &ResolvedTransferRoute> {
520 self.transfers.iter().map(|(route, _)| route)
521 }
522
523 pub(super) fn engine_views_for_preparation(
524 &self,
525 ) -> impl Iterator<Item = EngineSnapshotView<'_>> + '_ {
526 self.engines.iter().map(|slot| EngineSnapshotView { slot })
527 }
528
529 pub(super) fn extension_slot_for_preparation(
530 &self,
531 family_id: ExtensionFamilyId,
532 engine_id: &EngineId,
533 ) -> Option<ExtensionEngineSnapshotView<'_>> {
534 self.extensions.slot_for_preparation(family_id, engine_id)
535 }
536
537 pub(super) fn transfer_registry_for_preparation(&self) -> FrozenTransferRegistry {
538 self.transfers.clone()
539 }
540
541 #[cfg(test)]
542 pub(crate) fn extension_slots_for_test(
543 &self,
544 ) -> impl Iterator<
545 Item = (
546 &ExtensionModuleId,
547 ExtensionFamilyId,
548 &EngineId,
549 RegistrationIdentity,
550 ),
551 > {
552 self.extensions.slots_for_test()
553 }
554
555 #[cfg(test)]
556 pub(crate) fn extension_slot_identity_for_test(
557 &self,
558 family_id: ExtensionFamilyId,
559 engine_id: &EngineId,
560 ) -> Option<RegistrationIdentity> {
561 self.extensions.slot_identity_for_test(family_id, engine_id)
562 }
563
564 #[cfg(test)]
565 pub(crate) fn extension_slot_full_for_test(
566 &self,
567 family_id: ExtensionFamilyId,
568 engine_id: &EngineId,
569 ) -> Option<ExtensionSlotFullForTest<'_>> {
570 self.extensions.slot_full_for_test(family_id, engine_id)
571 }
572
573 #[cfg(test)]
574 pub(super) fn cache_owners_for_test(&self) -> &[FrozenCacheOwner] {
575 &self.cache_owners
576 }
577
578 pub(super) fn cache_owners_for_runtime(&self) -> &[FrozenCacheOwner] {
579 &self.cache_owners
580 }
581}
582
583impl fmt::Debug for RuntimeConfigSnapshot {
584 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
585 formatter
586 .debug_struct("RuntimeConfigSnapshot")
587 .field("runtime_id", &self.runtime_id)
588 .field("epoch", &self.epoch)
589 .field("execution_policy", &self.policy)
590 .field("engine_count", &self.engines.len())
591 .field("extension_module_count", &self.extensions.module_count())
592 .field("extension_engine_count", &self.extensions.engine_count())
593 .field("transfer_provider_count", &self.transfers.len())
594 .field("cache_owner_count", &self.cache_owners.len())
595 .finish_non_exhaustive()
596 }
597}
598
599struct RuntimeState {
600 runtime_id: RuntimeId,
601 issuer: NonZeroU64,
602 next_registration_ordinal: AtomicU64,
603 active: RwLock<Arc<RuntimeConfigSnapshot>>,
604 published_epoch: AtomicU64,
605 caches: RuntimeCacheSet<PreparedEntryKey, PreparedProgram>,
606}
607
608#[derive(Clone)]
622pub struct Runtime(Arc<RuntimeState>);
623
624impl Runtime {
625 pub fn builder() -> RuntimeConfigBuilder {
627 RuntimeConfigBuilder::new()
628 }
629
630 pub fn id(&self) -> RuntimeId {
632 self.0.runtime_id
633 }
634
635 pub fn snapshot(&self) -> Result<Arc<RuntimeConfigSnapshot>, RuntimeStateError> {
642 self.0
643 .active
644 .read()
645 .map(|snapshot| Arc::clone(&snapshot))
646 .map_err(|_| RuntimeStateError::Poisoned {
647 lock: "runtime.active",
648 })
649 }
650
651 pub fn epoch(&self) -> Result<RuntimeEpoch, RuntimeStateError> {
659 match NonZeroU64::new(self.0.published_epoch.load(Ordering::Acquire)) {
660 Some(value) => Ok(RuntimeEpoch::from_nonzero(value)),
661 None => Err(RuntimeStateError::Poisoned {
662 lock: "runtime.published_epoch",
663 }),
664 }
665 }
666
667 pub fn prepared_cache_limits(&self) -> Result<PreparedPlanCacheLimits, RuntimeStateError> {
686 self.0.caches.prepared().limits()
687 }
688
689 pub fn set_prepared_cache_limits(
716 &self,
717 limits: PreparedPlanCacheLimits,
718 ) -> Result<(), RuntimeStateError> {
719 self.0.caches.prepared().set_limits(limits)
720 }
721
722 pub fn clear_prepared_cache(&self) -> Result<(), RuntimeStateError> {
741 self.0.caches.prepared().clear()
742 }
743
744 pub fn cache_stats(&self) -> Result<RuntimeCacheStats, RuntimeCacheError> {
764 super::preparation::cache_stats(self, &self.0.caches)
765 }
766
767 pub fn clear_caches(&self) -> Result<(), RuntimeCacheError> {
787 super::preparation::clear_caches(self, &self.0.caches)
788 }
789
790 #[allow(
791 dead_code,
792 reason = "Phase 5 graph execution consumes crate-private prepared programs"
793 )]
794 pub(crate) fn prepare_for(
795 &self,
796 frozen: &FrozenProgram,
797 signature: &InputSignature,
798 options: &PrepareOptions,
799 ) -> PreparedProgramResult<Arc<PreparedProgram>> {
800 super::preparation::prepare_for(self, &self.0.caches, frozen, signature, options)
801 }
802
803 pub(crate) fn prepare_compiled_for(
804 &self,
805 program: &CompiledGraph,
806 signature: &InputSignature,
807 options: &PrepareOptions,
808 ) -> PreparedProgramResult<Arc<PreparedProgram>> {
809 super::preparation::prepare_compiled_for(self, &self.0.caches, program, signature, options)
810 }
811
812 pub fn run_compiled(
854 &self,
855 program: &CompiledGraph,
856 inputs: &[&Tensor],
857 ) -> crate::Result<Vec<Tensor>> {
858 super::execution::run_compiled(self, program, inputs)
859 }
860
861 pub fn execute_scoped_read_only<'env>(
875 &self,
876 program: &CompiledGraph,
877 inputs: super::execution::ScopedReadInputs<'env>,
878 ) -> std::result::Result<
879 super::execution::ScopedExecutionOutcome<'env>,
880 super::execution::ScopedSubmitRejected<'env>,
881 > {
882 super::execution::execute_scoped_read_only(self, program, inputs)
883 }
884
885 pub fn prepare_compiled(
910 &self,
911 program: &CompiledGraph,
912 inputs: &[&Tensor],
913 ) -> crate::Result<super::execution::PreparedCompiledGraph> {
914 super::execution::prepare_compiled(self, program, inputs)
915 }
916
917 pub fn run_prepared(
927 &self,
928 prepared: &super::execution::PreparedCompiledGraph,
929 inputs: &[&Tensor],
930 ) -> crate::Result<Vec<Tensor>> {
931 super::execution::run_prepared(self, prepared, inputs)
932 }
933
934 pub fn submit(
950 &self,
951 program: &CompiledGraph,
952 inputs: super::execution::ExecutionInputs,
953 ) -> std::result::Result<super::execution::ExecutionHandle, super::execution::SubmitError> {
954 super::execution::submit(self, program, inputs)
955 }
956
957 pub fn run_compiled_values(
995 &self,
996 program: &CompiledGraph,
997 inputs: &[&Tensor],
998 ) -> crate::Result<Vec<TensorValue>> {
999 super::execution::run_compiled_values(self, program, inputs)
1000 }
1001
1002 pub fn reconfigure(
1018 &self,
1019 edit: impl FnOnce(&mut RuntimeReconfiguration<'_>) -> Result<(), RuntimeConfigError>,
1020 ) -> Result<RuntimeEpoch, RuntimeReconfigureError> {
1021 let base = self
1022 .snapshot()
1023 .map_err(|source| RuntimeReconfigureError::State { source })?;
1024 let mut candidate = CandidateConfig::from_snapshot(&base)
1025 .map_err(|source| RuntimeReconfigureError::Edit { source })?;
1026 let mut changed = false;
1027 {
1028 let mut reconfiguration = RuntimeReconfiguration {
1029 candidate: &mut candidate,
1030 changed: &mut changed,
1031 };
1032 edit(&mut reconfiguration)
1033 .map_err(|source| RuntimeReconfigureError::Edit { source })?;
1034 }
1035
1036 if !changed {
1037 return Ok(base.epoch());
1038 }
1039 let next_identity_ordinal = NonZeroU64::new(
1040 self.0.next_registration_ordinal.load(Ordering::SeqCst),
1041 )
1042 .ok_or(RuntimeReconfigureError::Edit {
1043 source: RuntimeConfigError::IdentityExhausted,
1044 })?;
1045 let (bound_candidate, post_ordinal) =
1046 validate_candidate(candidate, self.0.issuer, next_identity_ordinal)
1047 .map_err(|source| RuntimeReconfigureError::Edit { source })?;
1048
1049 let next_epoch =
1050 base.epoch()
1051 .checked_next()
1052 .ok_or(RuntimeReconfigureError::EpochExhausted {
1053 current: base.epoch(),
1054 })?;
1055
1056 let mut guard = self
1057 .0
1058 .active
1059 .write()
1060 .map_err(|_| RuntimeReconfigureError::State {
1061 source: RuntimeStateError::Poisoned {
1062 lock: "runtime.active",
1063 },
1064 })?;
1065 if !Arc::ptr_eq(&*guard, &base) {
1066 return Err(RuntimeReconfigureError::ConcurrentReconfiguration {
1067 base: base.epoch(),
1068 current: guard.epoch(),
1069 });
1070 }
1071
1072 let next_snapshot = Arc::new(
1073 freeze_candidate(self.0.runtime_id, next_epoch, bound_candidate)
1074 .map_err(|source| RuntimeReconfigureError::Edit { source })?,
1075 );
1076
1077 self.0
1078 .next_registration_ordinal
1079 .store(post_ordinal.get(), Ordering::SeqCst);
1080 *guard = next_snapshot;
1081 self.0
1082 .published_epoch
1083 .store(next_epoch.get().get(), Ordering::Release);
1084 Ok(next_epoch)
1085 }
1086
1087 #[cfg(test)]
1088 pub(crate) fn force_epoch_for_test(&self, epoch: RuntimeEpoch) {
1089 let mut guard = self.0.active.write().expect("test runtime lock");
1090 let mut replacement = (**guard).clone();
1091 replacement.epoch = epoch;
1092 *guard = Arc::new(replacement);
1093 self.0
1094 .published_epoch
1095 .store(epoch.get().get(), Ordering::Release);
1096 }
1097
1098 #[cfg(test)]
1099 pub(crate) fn force_next_registration_ordinal_for_test(&self, next: NonZeroU64) {
1100 self.0
1101 .next_registration_ordinal
1102 .store(next.get(), Ordering::SeqCst);
1103 }
1104
1105 #[cfg(test)]
1106 pub(crate) fn poison_active_lock_for_test(&self) {
1107 let state = Arc::clone(&self.0);
1108 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || {
1109 let _guard = state.active.write().expect("test runtime lock");
1110 panic!("poison runtime.active for test");
1111 }));
1112 }
1113}
1114
1115impl fmt::Debug for Runtime {
1116 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1117 formatter
1118 .debug_struct("Runtime")
1119 .field("runtime_id", &self.0.runtime_id)
1120 .field("published_epoch", &self.epoch().ok())
1121 .finish_non_exhaustive()
1122 }
1123}
1124
1125pub struct RuntimeConfigBuilder {
1139 candidate: CandidateConfig,
1140}
1141
1142impl RuntimeConfigBuilder {
1143 pub fn new() -> Self {
1145 Self {
1146 candidate: CandidateConfig::empty(),
1147 }
1148 }
1149
1150 pub fn execution_policy(&mut self, value: ExecutionPolicy) -> &mut Self {
1152 self.candidate.policy = value;
1153 self
1154 }
1155
1156 pub fn register_engine(
1163 &mut self,
1164 value: EngineRegistration,
1165 ) -> Result<&mut Self, RuntimeConfigError> {
1166 let mut changed = false;
1167 register_engine_candidate(&mut self.candidate, value, &mut changed)?;
1168 Ok(self)
1169 }
1170
1171 pub fn replace_engine(
1177 &mut self,
1178 value: EngineRegistration,
1179 ) -> Result<&mut Self, RuntimeConfigError> {
1180 let mut changed = false;
1181 replace_engine_candidate(&mut self.candidate, value, &mut changed)?;
1182 Ok(self)
1183 }
1184
1185 pub fn remove_engine(&mut self, id: &EngineId) -> Result<&mut Self, RuntimeConfigError> {
1191 let mut changed = false;
1192 remove_engine_candidate(&mut self.candidate, id, &mut changed)?;
1193 Ok(self)
1194 }
1195
1196 pub fn install_extension_module(
1203 &mut self,
1204 value: Arc<dyn ExtensionModule>,
1205 ) -> Result<&mut Self, RuntimeConfigError> {
1206 let mut changed = false;
1207 install_extension_module_candidate(&mut self.candidate, value, &mut changed)?;
1208 Ok(self)
1209 }
1210
1211 pub fn register_transfer_provider(
1291 &mut self,
1292 source: TransferEndpoint,
1293 destination: TransferEndpoint,
1294 provider: Arc<dyn TransferProvider>,
1295 ) -> Result<&mut Self, RuntimeConfigError> {
1296 let mut changed = false;
1297 register_transfer_provider_candidate(
1298 &mut self.candidate,
1299 source,
1300 destination,
1301 provider,
1302 &mut changed,
1303 )?;
1304 Ok(self)
1305 }
1306
1307 pub fn remove_transfer_provider(
1318 &mut self,
1319 source: TransferEndpoint,
1320 destination: TransferEndpoint,
1321 ) -> Result<&mut Self, RuntimeConfigError> {
1322 let mut changed = false;
1323 remove_transfer_provider_candidate(&mut self.candidate, source, destination, &mut changed)?;
1324 Ok(self)
1325 }
1326
1327 pub fn replace_extension_module(
1334 &mut self,
1335 value: Arc<dyn ExtensionModule>,
1336 ) -> Result<&mut Self, RuntimeConfigError> {
1337 let mut changed = false;
1338 replace_extension_module_candidate(&mut self.candidate, value, &mut changed)?;
1339 Ok(self)
1340 }
1341
1342 pub fn remove_extension_module(
1349 &mut self,
1350 id: &ExtensionModuleId,
1351 ) -> Result<&mut Self, RuntimeConfigError> {
1352 let mut changed = false;
1353 remove_extension_module_candidate(&mut self.candidate, id, &mut changed)?;
1354 Ok(self)
1355 }
1356
1357 pub fn build(self) -> Result<Runtime, RuntimeConfigError> {
1367 let runtime_id = RuntimeId::from_nonzero(allocate_nonzero(&NEXT_RUNTIME_ID)?);
1368 let issuer = allocate_nonzero(&NEXT_REGISTRATION_ISSUER)?;
1369 let (bound_candidate, post_ordinal) =
1370 validate_candidate(self.candidate, issuer, INITIAL_REGISTRATION_ORDINAL)?;
1371 let epoch = RuntimeEpoch::one();
1372 let snapshot = Arc::new(freeze_candidate(runtime_id, epoch, bound_candidate)?);
1373 let state = RuntimeState {
1374 runtime_id,
1375 issuer,
1376 next_registration_ordinal: AtomicU64::new(post_ordinal.get()),
1377 active: RwLock::new(snapshot),
1378 published_epoch: AtomicU64::new(epoch.get().get()),
1379 caches: RuntimeCacheSet::new(PreparedPlanCacheLimits::default()),
1380 };
1381 Ok(Runtime(Arc::new(state)))
1382 }
1383}
1384
1385impl Default for RuntimeConfigBuilder {
1386 fn default() -> Self {
1387 Self::new()
1388 }
1389}
1390
1391impl fmt::Debug for RuntimeConfigBuilder {
1392 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1393 formatter
1394 .debug_struct("RuntimeConfigBuilder")
1395 .field("execution_policy", &self.candidate.policy)
1396 .field("engine_count", &self.candidate.engines.len())
1397 .field("extension_module_count", &self.candidate.modules.len())
1398 .field("transfer_provider_count", &self.candidate.transfers.len())
1399 .finish_non_exhaustive()
1400 }
1401}
1402
1403pub struct RuntimeReconfiguration<'a> {
1405 candidate: &'a mut CandidateConfig,
1406 changed: &'a mut bool,
1407}
1408
1409impl RuntimeReconfiguration<'_> {
1410 pub fn execution_policy(&mut self, policy: ExecutionPolicy) -> &mut Self {
1412 if self.candidate.policy != policy {
1413 self.candidate.policy = policy;
1414 *self.changed = true;
1415 }
1416 self
1417 }
1418
1419 pub fn register_engine(
1426 &mut self,
1427 value: EngineRegistration,
1428 ) -> Result<&mut Self, RuntimeConfigError> {
1429 register_engine_candidate(self.candidate, value, self.changed)?;
1430 Ok(self)
1431 }
1432
1433 pub fn replace_engine(
1439 &mut self,
1440 value: EngineRegistration,
1441 ) -> Result<&mut Self, RuntimeConfigError> {
1442 replace_engine_candidate(self.candidate, value, self.changed)?;
1443 Ok(self)
1444 }
1445
1446 pub fn remove_engine(&mut self, id: &EngineId) -> Result<&mut Self, RuntimeConfigError> {
1452 remove_engine_candidate(self.candidate, id, self.changed)?;
1453 Ok(self)
1454 }
1455
1456 pub fn install_extension_module(
1463 &mut self,
1464 value: Arc<dyn ExtensionModule>,
1465 ) -> Result<&mut Self, RuntimeConfigError> {
1466 install_extension_module_candidate(self.candidate, value, self.changed)?;
1467 Ok(self)
1468 }
1469
1470 pub fn register_transfer_provider(
1545 &mut self,
1546 source: TransferEndpoint,
1547 destination: TransferEndpoint,
1548 provider: Arc<dyn TransferProvider>,
1549 ) -> Result<&mut Self, RuntimeConfigError> {
1550 register_transfer_provider_candidate(
1551 self.candidate,
1552 source,
1553 destination,
1554 provider,
1555 self.changed,
1556 )?;
1557 Ok(self)
1558 }
1559
1560 pub fn remove_transfer_provider(
1568 &mut self,
1569 source: TransferEndpoint,
1570 destination: TransferEndpoint,
1571 ) -> Result<&mut Self, RuntimeConfigError> {
1572 remove_transfer_provider_candidate(self.candidate, source, destination, self.changed)?;
1573 Ok(self)
1574 }
1575
1576 pub fn replace_extension_module(
1584 &mut self,
1585 value: Arc<dyn ExtensionModule>,
1586 ) -> Result<&mut Self, RuntimeConfigError> {
1587 replace_extension_module_candidate(self.candidate, value, self.changed)?;
1588 Ok(self)
1589 }
1590
1591 #[doc(hidden)]
1654 pub fn ensure_extension_module_for_engine(
1655 &mut self,
1656 value: Arc<dyn ExtensionModule>,
1657 family_id: &'static str,
1658 engine_id: &EngineId,
1659 ) -> Result<&mut Self, RuntimeConfigError> {
1660 let module_id = value.module_id().clone();
1661 if let Some(existing) = self.candidate.modules.get(&module_id) {
1662 if existing
1663 .engines
1664 .contains_key(&(family_id, engine_id.clone()))
1665 {
1666 return Ok(self);
1667 }
1668 }
1669
1670 let record = configure_module(Arc::clone(&value))
1671 .map_err(|source| RuntimeConfigError::ExtensionModule { source })?;
1672 if !record.engines.contains_key(&(family_id, engine_id.clone())) {
1673 return Err(RuntimeConfigError::MissingExtensionEngine {
1674 module_id,
1675 family_id,
1676 engine_id: engine_id.clone(),
1677 });
1678 }
1679 self.candidate.modules.insert(module_id, record);
1680 *self.changed = true;
1681 Ok(self)
1682 }
1683
1684 #[doc(hidden)]
1744 pub fn replace_extension_module_for_engine(
1745 &mut self,
1746 value: Arc<dyn ExtensionModule>,
1747 family_id: &'static str,
1748 engine_id: &EngineId,
1749 ) -> Result<&mut Self, RuntimeConfigError> {
1750 let module_id = value.module_id().clone();
1751 if let Some(existing) = self.candidate.modules.get(&module_id) {
1752 if existing.module_identical(&value) {
1753 if existing
1754 .engines
1755 .contains_key(&(family_id, engine_id.clone()))
1756 {
1757 return Ok(self);
1758 }
1759 return Err(RuntimeConfigError::MissingExtensionEngine {
1760 module_id,
1761 family_id,
1762 engine_id: engine_id.clone(),
1763 });
1764 }
1765 }
1766
1767 let record = configure_module(Arc::clone(&value))
1768 .map_err(|source| RuntimeConfigError::ExtensionModule { source })?;
1769 if !record.engines.contains_key(&(family_id, engine_id.clone())) {
1770 return Err(RuntimeConfigError::MissingExtensionEngine {
1771 module_id,
1772 family_id,
1773 engine_id: engine_id.clone(),
1774 });
1775 }
1776 self.candidate.modules.insert(module_id, record);
1777 *self.changed = true;
1778 Ok(self)
1779 }
1780
1781 pub fn remove_extension_module(
1788 &mut self,
1789 id: &ExtensionModuleId,
1790 ) -> Result<&mut Self, RuntimeConfigError> {
1791 remove_extension_module_candidate(self.candidate, id, self.changed)?;
1792 Ok(self)
1793 }
1794}
1795
1796impl fmt::Debug for RuntimeReconfiguration<'_> {
1797 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1798 formatter
1799 .debug_struct("RuntimeReconfiguration")
1800 .field("engine_count", &self.candidate.engines.len())
1801 .field("extension_module_count", &self.candidate.modules.len())
1802 .field("transfer_provider_count", &self.candidate.transfers.len())
1803 .field("changed", &*self.changed)
1804 .finish_non_exhaustive()
1805 }
1806}
1807
1808#[derive(Clone, Copy)]
1820pub struct EngineSnapshotView<'a> {
1821 slot: &'a FrozenEngineSlot,
1822}
1823
1824impl<'a> EngineSnapshotView<'a> {
1825 pub fn engine_id(&self) -> &'a EngineId {
1827 self.slot.engine_id()
1828 }
1829
1830 pub fn provider_device_identity(&self) -> &'a super::ProviderDeviceIdentity {
1840 self.slot.provider_device_identity()
1841 }
1842
1843 pub fn registration_identity(&self) -> RegistrationIdentity {
1845 self.slot.metadata().identity
1846 }
1847
1848 pub fn context_identity(&self) -> ExecutionContextIdentity {
1850 self.slot.context_identity()
1851 }
1852
1853 pub fn event_domain_id(&self) -> EventDomainId {
1855 self.slot.metadata().event_domain_id
1856 }
1857
1858 pub(super) fn executable_witness(&self) -> Option<&'a Arc<ExecutableEngineSnapshot>> {
1859 self.slot.executable()
1860 }
1861
1862 pub fn hardware_class(&self) -> &'a HardwareClassId {
1864 self.slot.hardware_class()
1865 }
1866
1867 pub fn capabilities(&self) -> &'a CoreCapabilityBundle {
1869 self.slot.capabilities()
1870 }
1871
1872 pub(super) fn storage_classes(&self) -> &'a [StorageClass] {
1873 self.slot.storage_classes()
1874 }
1875
1876 pub(super) fn default_storage_class(&self) -> &'a StorageClass {
1877 self.slot.default_storage_class()
1878 }
1879
1880 #[doc(hidden)]
1907 pub fn accepts_input_signature(&self, input: &super::InputSignatureEntry) -> bool {
1908 self.slot
1909 .storage_classes()
1910 .iter()
1911 .any(|storage_class| self.accepts_input_signature_for_storage(input, storage_class))
1912 }
1913
1914 pub(super) fn accepts_input_signature_for_storage(
1915 &self,
1916 input: &super::InputSignatureEntry,
1917 storage_class: &StorageClass,
1918 ) -> bool {
1919 self.slot
1920 .executable()
1921 .is_some_and(|snapshot| snapshot.accepts_input_signature(input, storage_class))
1922 }
1923
1924 #[cfg(test)]
1925 pub(crate) fn has_execution_engine_for_test(&self) -> bool {
1926 self.slot.executable().is_some()
1927 }
1928}
1929
1930impl fmt::Debug for EngineSnapshotView<'_> {
1931 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1932 formatter
1933 .debug_struct("EngineSnapshotView")
1934 .field("engine_id", self.engine_id())
1935 .field("registration_identity", &self.registration_identity())
1936 .field("context_identity", &self.context_identity())
1937 .field("hardware_class", self.hardware_class())
1938 .field("capabilities", self.capabilities())
1939 .finish()
1940 }
1941}
1942
1943fn default_execution_policy() -> ExecutionPolicy {
1944 ExecutionPolicy::new(super::Determinism::Fast, None, 0)
1945}
1946
1947fn register_engine_candidate(
1948 candidate: &mut CandidateConfig,
1949 registration: EngineRegistration,
1950 changed: &mut bool,
1951) -> Result<(), RuntimeConfigError> {
1952 let engine_id = registration.engine_id().clone();
1953 match candidate.engines.get(&engine_id) {
1954 Some(existing) if existing.registration.candidate_identical(®istration) => Ok(()),
1955 Some(_) => Err(RuntimeConfigError::DuplicateEngine { engine_id }),
1956 None => {
1957 ensure_unique_provider_device_target(candidate, ®istration)?;
1958 candidate.engines.insert(
1959 engine_id,
1960 CandidateEngineRecord {
1961 registration,
1962 identity: CandidateRegistrationIdentity::New,
1963 },
1964 );
1965 *changed = true;
1966 Ok(())
1967 }
1968 }
1969}
1970
1971fn replace_engine_candidate(
1972 candidate: &mut CandidateConfig,
1973 registration: EngineRegistration,
1974 changed: &mut bool,
1975) -> Result<(), RuntimeConfigError> {
1976 let engine_id = registration.engine_id().clone();
1977 let Some(existing) = candidate.engines.get(&engine_id) else {
1978 return Err(RuntimeConfigError::MissingEngine { engine_id });
1979 };
1980 if existing.registration.candidate_identical(®istration) {
1981 return Ok(());
1982 }
1983 if existing.registration.provider_device_identity() != registration.provider_device_identity() {
1984 return Err(RuntimeConfigError::EngineTargetRebind {
1985 engine_id,
1986 current: existing.registration.provider_device_identity().clone(),
1987 replacement: registration.provider_device_identity().clone(),
1988 });
1989 }
1990 ensure_unique_provider_device_target_except(candidate, ®istration, &engine_id)?;
1991 candidate.engines.insert(
1992 engine_id,
1993 CandidateEngineRecord {
1994 registration,
1995 identity: CandidateRegistrationIdentity::New,
1996 },
1997 );
1998 *changed = true;
1999 Ok(())
2000}
2001
2002fn remove_engine_candidate(
2003 candidate: &mut CandidateConfig,
2004 id: &EngineId,
2005 changed: &mut bool,
2006) -> Result<(), RuntimeConfigError> {
2007 match candidate.engines.remove(id) {
2008 Some(_) => {
2009 *changed = true;
2010 Ok(())
2011 }
2012 None => Err(RuntimeConfigError::MissingEngine {
2013 engine_id: id.clone(),
2014 }),
2015 }
2016}
2017
2018fn install_extension_module_candidate(
2019 candidate: &mut CandidateConfig,
2020 module: Arc<dyn ExtensionModule>,
2021 changed: &mut bool,
2022) -> Result<(), RuntimeConfigError> {
2023 let module_id = module.module_id().clone();
2024 match candidate.modules.get(&module_id) {
2025 Some(existing) if existing.module_identical(&module) => Ok(()),
2026 Some(_) => Err(RuntimeConfigError::ExtensionModule {
2027 source: ExtensionModuleError::ConflictingModule { module_id },
2028 }),
2029 None => {
2030 let record = configure_module(module)
2031 .map_err(|source| RuntimeConfigError::ExtensionModule { source })?;
2032 candidate.modules.insert(module_id, record);
2033 *changed = true;
2034 Ok(())
2035 }
2036 }
2037}
2038
2039fn replace_extension_module_candidate(
2040 candidate: &mut CandidateConfig,
2041 module: Arc<dyn ExtensionModule>,
2042 changed: &mut bool,
2043) -> Result<(), RuntimeConfigError> {
2044 let module_id = module.module_id().clone();
2045 match candidate.modules.get(&module_id) {
2046 Some(existing) if existing.module_identical(&module) => Ok(()),
2047 _ => {
2048 let record = configure_module(module)
2049 .map_err(|source| RuntimeConfigError::ExtensionModule { source })?;
2050 candidate.modules.insert(module_id, record);
2051 *changed = true;
2052 Ok(())
2053 }
2054 }
2055}
2056
2057fn remove_extension_module_candidate(
2058 candidate: &mut CandidateConfig,
2059 id: &ExtensionModuleId,
2060 changed: &mut bool,
2061) -> Result<(), RuntimeConfigError> {
2062 if candidate.modules.remove(id).is_some() {
2063 *changed = true;
2064 }
2065 Ok(())
2066}
2067
2068fn register_transfer_provider_candidate(
2069 candidate: &mut CandidateConfig,
2070 source: TransferEndpoint,
2071 destination: TransferEndpoint,
2072 provider: Arc<dyn TransferProvider>,
2073 changed: &mut bool,
2074) -> Result<(), RuntimeConfigError> {
2075 let key = TransferRoute::new(source, destination);
2076 match candidate.transfers.get(&key) {
2077 Some(existing) if Arc::ptr_eq(&existing.provider, &provider) => Ok(()),
2078 Some(_) => Err(RuntimeConfigError::ConflictingRegistration {
2079 key: RegistrationKey::TransferProvider {
2080 source: key.source().clone(),
2081 destination: key.destination().clone(),
2082 },
2083 }),
2084 None => {
2085 candidate.transfers.insert(
2086 key,
2087 CandidateTransferRecord {
2088 provider,
2089 binding: CandidateTransferBinding::New,
2090 },
2091 );
2092 *changed = true;
2093 Ok(())
2094 }
2095 }
2096}
2097
2098fn remove_transfer_provider_candidate(
2099 candidate: &mut CandidateConfig,
2100 source: TransferEndpoint,
2101 destination: TransferEndpoint,
2102 changed: &mut bool,
2103) -> Result<(), RuntimeConfigError> {
2104 let key = TransferRoute::new(source, destination);
2105 if candidate.transfers.remove(&key).is_none() {
2106 return Err(RuntimeConfigError::MissingTransferProvider {
2107 source_endpoint: key.source().clone(),
2108 destination: key.destination().clone(),
2109 });
2110 }
2111 *changed = true;
2112 Ok(())
2113}
2114
2115fn ensure_unique_provider_device_target(
2116 candidate: &CandidateConfig,
2117 registration: &EngineRegistration,
2118) -> Result<(), RuntimeConfigError> {
2119 ensure_unique_provider_device_target_except(candidate, registration, registration.engine_id())
2120}
2121
2122fn ensure_unique_provider_device_target_except(
2123 candidate: &CandidateConfig,
2124 registration: &EngineRegistration,
2125 ignored_engine_id: &EngineId,
2126) -> Result<(), RuntimeConfigError> {
2127 if let Some((first_engine_id, _)) = candidate.engines.iter().find(|(engine_id, record)| {
2128 *engine_id != ignored_engine_id
2129 && record.registration.provider_device_identity()
2130 == registration.provider_device_identity()
2131 }) {
2132 return Err(RuntimeConfigError::DuplicateProviderDeviceTarget {
2133 provider_device_identity: registration.provider_device_identity().clone(),
2134 first_engine_id: first_engine_id.clone(),
2135 duplicate_engine_id: registration.engine_id().clone(),
2136 });
2137 }
2138 Ok(())
2139}
2140
2141fn validate_candidate(
2142 candidate: CandidateConfig,
2143 issuer: NonZeroU64,
2144 next_ordinal: NonZeroU64,
2145) -> Result<(BoundCandidateConfig, NonZeroU64), RuntimeConfigError> {
2146 let mut seen_targets = BTreeMap::<ProviderDeviceIdentity, EngineId>::new();
2147 for (engine_id, record) in &candidate.engines {
2148 if let Some(first_engine_id) = seen_targets.insert(
2149 record.registration.provider_device_identity().clone(),
2150 engine_id.clone(),
2151 ) {
2152 return Err(RuntimeConfigError::DuplicateProviderDeviceTarget {
2153 provider_device_identity: record.registration.provider_device_identity().clone(),
2154 first_engine_id,
2155 duplicate_engine_id: engine_id.clone(),
2156 });
2157 }
2158 }
2159
2160 let mut bound_transfers = BTreeMap::new();
2161 for (route, record) in &candidate.transfers {
2162 let source_binding = validate_transfer_endpoint(&candidate, route.source())?;
2163 let destination_binding = validate_transfer_endpoint(&candidate, route.destination())?;
2164 let preserved = match &record.binding {
2165 CandidateTransferBinding::New => None,
2166 CandidateTransferBinding::Preserved {
2167 source,
2168 destination,
2169 } => Some((source, destination)),
2170 };
2171 if let Some((registered_source, registered_destination)) = preserved {
2172 if registered_source != &source_binding {
2173 return Err(RuntimeConfigError::StaleTransferRoute {
2174 source_endpoint: route.source().clone(),
2175 destination: route.destination().clone(),
2176 endpoint: route.source().clone(),
2177 registered: Box::new(registered_source.clone()),
2178 current: Box::new(source_binding.clone()),
2179 });
2180 }
2181 if registered_destination != &destination_binding {
2182 return Err(RuntimeConfigError::StaleTransferRoute {
2183 source_endpoint: route.source().clone(),
2184 destination: route.destination().clone(),
2185 endpoint: route.destination().clone(),
2186 registered: Box::new(registered_destination.clone()),
2187 current: Box::new(destination_binding.clone()),
2188 });
2189 }
2190 }
2191 bound_transfers.insert(
2192 route.clone(),
2193 BoundCandidateTransferRecord {
2194 provider: Arc::clone(&record.provider),
2195 source: source_binding,
2196 destination: destination_binding,
2197 },
2198 );
2199 }
2200 let mut seen = BTreeMap::<(ExtensionFamilyId, EngineId), ExtensionModuleId>::new();
2201 for (module_id, module) in &candidate.modules {
2202 for family_engine in module.engines.keys() {
2203 if seen
2204 .insert(
2205 (family_engine.0, family_engine.1.clone()),
2206 module_id.clone(),
2207 )
2208 .is_some()
2209 {
2210 return Err(RuntimeConfigError::ConflictingRegistration {
2211 key: RegistrationKey::ExtensionEngine {
2212 family: family_engine.0,
2213 engine: family_engine.1.clone(),
2214 },
2215 });
2216 }
2217 }
2218 }
2219
2220 let CandidateConfig {
2221 policy,
2222 engines,
2223 modules,
2224 transfers: _,
2225 } = candidate;
2226 let mut allocator = RegistrationIdentityAllocator::new(issuer, next_ordinal);
2227 let engines = engines
2228 .into_iter()
2229 .map(|(engine_id, record)| {
2230 let identity = match record.identity {
2231 CandidateRegistrationIdentity::New => allocator.allocate()?,
2232 CandidateRegistrationIdentity::Preserved(identity) => identity,
2233 };
2234 Ok((
2235 engine_id,
2236 BoundCandidateEngineRecord {
2237 registration: record.registration,
2238 identity,
2239 },
2240 ))
2241 })
2242 .collect::<Result<BTreeMap<_, _>, RuntimeConfigError>>()?;
2243 let modules = modules
2244 .into_iter()
2245 .map(|(module_id, module)| {
2246 let mut allocate = || allocator.allocate();
2247 Ok((module_id, bind_candidate_module(module, &mut allocate)?))
2248 })
2249 .collect::<Result<BTreeMap<_, _>, RuntimeConfigError>>()?;
2250 Ok((
2251 BoundCandidateConfig {
2252 policy,
2253 engines,
2254 modules,
2255 transfers: bound_transfers,
2256 },
2257 allocator.next_ordinal(),
2258 ))
2259}
2260
2261fn validate_transfer_endpoint(
2262 candidate: &CandidateConfig,
2263 endpoint: &TransferEndpoint,
2264) -> Result<ProviderDeviceIdentity, RuntimeConfigError> {
2265 let Some(engine) = candidate.engines.get(endpoint.engine_id()) else {
2266 return Err(RuntimeConfigError::UnknownTransferEndpointEngine {
2267 endpoint: endpoint.clone(),
2268 });
2269 };
2270 if !engine
2271 .registration
2272 .storage_classes()
2273 .contains(endpoint.storage_class())
2274 {
2275 return Err(RuntimeConfigError::UnsupportedTransferEndpointStorage {
2276 endpoint: endpoint.clone(),
2277 });
2278 }
2279 Ok(engine.registration.provider_device_identity().clone())
2280}
2281
2282struct RegistrationIdentityAllocator {
2283 issuer: NonZeroU64,
2284 next: NonZeroU64,
2285}
2286
2287impl RegistrationIdentityAllocator {
2288 fn new(issuer: NonZeroU64, next: NonZeroU64) -> Self {
2289 Self { issuer, next }
2290 }
2291
2292 fn allocate(&mut self) -> Result<RegistrationIdentity, RuntimeConfigError> {
2293 let identity = RegistrationIdentity::new(self.issuer, self.next);
2294 let next = self
2295 .next
2296 .get()
2297 .checked_add(1)
2298 .and_then(NonZeroU64::new)
2299 .ok_or(RuntimeConfigError::IdentityExhausted)?;
2300 self.next = next;
2301 Ok(identity)
2302 }
2303
2304 fn next_ordinal(&self) -> NonZeroU64 {
2305 self.next
2306 }
2307}
2308
2309fn freeze_candidate(
2310 runtime_id: RuntimeId,
2311 epoch: RuntimeEpoch,
2312 candidate: BoundCandidateConfig,
2313) -> Result<RuntimeConfigSnapshot, RuntimeConfigError> {
2314 let mut engines = Vec::with_capacity(candidate.engines.len());
2315 let mut engine_indices = BTreeMap::new();
2316 let mut engine_locations = BTreeMap::new();
2317 let mut cache_owners = Vec::new();
2318 for (index, (engine_id, record)) in candidate.engines.into_iter().enumerate() {
2319 let BoundCandidateEngineRecord {
2320 registration,
2321 identity,
2322 } = record;
2323 let event_domain_id = EventDomainId::new(runtime_id, epoch, identity);
2324 let (state, candidate_token) = registration.into_state_and_token();
2325 let provider_device_identity = state.provider_device_identity().clone();
2326 let metadata = FrozenEngineMetadata {
2327 candidate_token,
2328 identity,
2329 event_domain_id,
2330 };
2331 let frozen = match state {
2332 EngineRegistrationState::PreparationOnly { binding } => {
2333 FrozenEngineSlot::PreparationOnly(Arc::new(PreparationOnlyEngineSnapshot {
2334 metadata,
2335 binding,
2336 }))
2337 }
2338 EngineRegistrationState::Executable(binding) => {
2339 if let Some(owner) = binding.contract().cache_owner().cloned() {
2340 cache_owners.push(FrozenCacheOwner {
2341 id: engine_cache_owner_id(&engine_id),
2342 kind: FrozenCacheOwnerKind::Engine,
2343 owner,
2344 });
2345 }
2346 cache_owners.push(FrozenCacheOwner {
2347 id: engine_extension_cache_owner_id(&engine_id),
2348 kind: FrozenCacheOwnerKind::Extension,
2349 owner: execution::extension_cache_owner(binding.contract().executor().clone()),
2350 });
2351 FrozenEngineSlot::Executable(Arc::new(ExecutableEngineSnapshot {
2352 metadata,
2353 binding,
2354 }))
2355 }
2356 };
2357 engine_locations.insert(
2358 engine_id.clone(),
2359 (provider_device_identity, event_domain_id),
2360 );
2361 engine_indices.insert(engine_id, index);
2362 engines.push(frozen);
2363 }
2364 let extensions = freeze_extension_slots(candidate.modules)?;
2365 for (id, owner) in extensions.cache_owner_records() {
2366 cache_owners.push(FrozenCacheOwner {
2367 id,
2368 kind: FrozenCacheOwnerKind::Extension,
2369 owner,
2370 });
2371 }
2372 let mut transfers = BTreeMap::new();
2373 for (route, record) in candidate.transfers {
2374 let BoundCandidateTransferRecord {
2375 provider,
2376 source: source_binding,
2377 destination: destination_binding,
2378 } = record;
2379 let (_, source_event_domain_id) = bound_engine_location(&engine_locations, route.source())?;
2380 let (_, destination_event_domain_id) =
2381 bound_engine_location(&engine_locations, route.destination())?;
2382 let resolved_route = ResolvedTransferRoute::new(
2383 ResolvedTransferEndpoint::new(
2384 route.source().clone(),
2385 source_binding,
2386 *source_event_domain_id,
2387 ),
2388 ResolvedTransferEndpoint::new(
2389 route.destination().clone(),
2390 destination_binding,
2391 *destination_event_domain_id,
2392 ),
2393 );
2394 transfers.insert(resolved_route, provider);
2395 }
2396 Ok(RuntimeConfigSnapshot {
2397 runtime_id,
2398 epoch,
2399 policy: candidate.policy,
2400 engines: engines.into(),
2401 engine_indices,
2402 extensions,
2403 transfers: FrozenTransferRegistry::new(transfers),
2404 cache_owners: cache_owners.into(),
2405 })
2406}
2407
2408fn bound_engine_location<'a>(
2409 locations: &'a BTreeMap<EngineId, (ProviderDeviceIdentity, EventDomainId)>,
2410 endpoint: &TransferEndpoint,
2411) -> Result<&'a (ProviderDeviceIdentity, EventDomainId), RuntimeConfigError> {
2412 locations
2413 .get(endpoint.engine_id())
2414 .ok_or_else(|| RuntimeConfigError::BoundCandidateInvariant {
2415 endpoint: endpoint.clone(),
2416 })
2417}
2418
2419fn engine_cache_owner_id(engine_id: &EngineId) -> CacheOwnerId {
2420 let id = engine_id.as_str();
2421 CacheOwnerId::from_canonical_owner_id(Arc::<str>::from(format!("engine[{}]:{id}", id.len())))
2422}
2423
2424fn engine_extension_cache_owner_id(engine_id: &EngineId) -> CacheOwnerId {
2425 let id = engine_id.as_str();
2426 CacheOwnerId::from_canonical_owner_id(Arc::<str>::from(format!(
2427 "extension-executor[{}]:{id}",
2428 id.len()
2429 )))
2430}
2431
2432fn allocate_nonzero(counter: &AtomicU64) -> Result<NonZeroU64, RuntimeConfigError> {
2433 let value = counter
2434 .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |next| {
2435 next.checked_add(1)
2436 })
2437 .map_err(|_| RuntimeConfigError::IdentityExhausted)?;
2438 NonZeroU64::new(value).ok_or(RuntimeConfigError::IdentityExhausted)
2439}
2440
2441#[cfg(test)]
2442mod freeze_tests {
2443 use crate::{ProviderId, TransferRequest};
2444
2445 use super::*;
2446
2447 #[derive(Debug)]
2448 struct FreezeTestContext;
2449
2450 #[derive(Debug)]
2451 struct FreezeTestProvider;
2452
2453 impl TransferProvider for FreezeTestProvider {
2454 fn transfer_blocking(
2455 &self,
2456 _request: TransferRequest<'_>,
2457 ) -> crate::Result<tenferro_tensor::Tensor> {
2458 Err(crate::Error::Internal("freeze test provider".into()))
2459 }
2460 }
2461
2462 fn registration(
2463 engine_id: &str,
2464 target: &str,
2465 ) -> Result<EngineRegistration, RuntimeConfigError> {
2466 let engine_id = EngineId::new(engine_id).map_err(RuntimeConfigError::from)?;
2467 let storage =
2468 StorageClass::new("tenferro.test.freeze.storage").map_err(RuntimeConfigError::from)?;
2469 Ok(EngineRegistration::preparation_only(
2470 super::super::ProviderPreparationBinding::new(
2471 engine_id,
2472 ProviderDeviceIdentity::new(
2473 ProviderId::new("tenferro.test.freeze.provider")
2474 .map_err(RuntimeConfigError::from)?,
2475 target,
2476 )
2477 .map_err(RuntimeConfigError::from)?,
2478 ExecutionContextIdentity::of::<FreezeTestContext>(),
2479 HardwareClassId::new("tenferro.test.freeze.hardware")
2480 .map_err(RuntimeConfigError::from)?,
2481 Arc::from(vec![storage.clone()]),
2482 storage,
2483 CoreCapabilityBundle::default(),
2484 )?,
2485 ))
2486 }
2487
2488 fn candidate(binding: CandidateTransferBinding) -> Result<CandidateConfig, RuntimeConfigError> {
2489 let source_id =
2490 EngineId::new("tenferro.test.freeze.source").map_err(RuntimeConfigError::from)?;
2491 let destination_id =
2492 EngineId::new("tenferro.test.freeze.destination").map_err(RuntimeConfigError::from)?;
2493 let storage =
2494 StorageClass::new("tenferro.test.freeze.storage").map_err(RuntimeConfigError::from)?;
2495 let source_endpoint = TransferEndpoint::new(source_id.clone(), storage.clone());
2496 let destination_endpoint = TransferEndpoint::new(destination_id.clone(), storage);
2497 let mut candidate = CandidateConfig::empty();
2498 let mut changed = false;
2499 register_engine_candidate(
2500 &mut candidate,
2501 registration(source_id.as_str(), "freeze-source")?,
2502 &mut changed,
2503 )?;
2504 register_engine_candidate(
2505 &mut candidate,
2506 registration(destination_id.as_str(), "freeze-destination")?,
2507 &mut changed,
2508 )?;
2509 register_transfer_provider_candidate(
2510 &mut candidate,
2511 source_endpoint.clone(),
2512 destination_endpoint.clone(),
2513 Arc::new(FreezeTestProvider),
2514 &mut changed,
2515 )?;
2516 candidate
2517 .transfers
2518 .get_mut(&TransferRoute::new(source_endpoint, destination_endpoint))
2519 .expect("registered route")
2520 .binding = binding;
2521 Ok(candidate)
2522 }
2523
2524 #[test]
2525 fn validation_owns_stale_route_rejection_and_bound_freeze_is_total() {
2526 let wrong_source = ProviderDeviceIdentity::new(
2527 ProviderId::new("tenferro.test.freeze.provider").unwrap(),
2528 "different-source",
2529 )
2530 .unwrap();
2531 let preserved = CandidateTransferBinding::Preserved {
2532 source: wrong_source,
2533 destination: ProviderDeviceIdentity::new(
2534 ProviderId::new("tenferro.test.freeze.provider").unwrap(),
2535 "freeze-destination",
2536 )
2537 .unwrap(),
2538 };
2539 let result = validate_candidate(
2540 candidate(preserved).unwrap(),
2541 NonZeroU64::new(1).unwrap(),
2542 NonZeroU64::new(1).unwrap(),
2543 );
2544 let error = match result {
2545 Ok(_) => panic!("candidate validation must reject stale preserved bindings"),
2546 Err(error) => error,
2547 };
2548 assert!(matches!(
2549 error,
2550 RuntimeConfigError::StaleTransferRoute { .. }
2551 ));
2552
2553 let (bound, _) = validate_candidate(
2554 candidate(CandidateTransferBinding::New).unwrap(),
2555 NonZeroU64::new(1).unwrap(),
2556 NonZeroU64::new(1).unwrap(),
2557 )
2558 .expect("validation must produce a complete bound candidate");
2559 freeze_candidate(
2560 RuntimeId::from_nonzero(NonZeroU64::new(1).unwrap()),
2561 RuntimeEpoch::one(),
2562 bound,
2563 )
2564 .expect("a bound candidate must freeze without semantic route revalidation");
2565 }
2566
2567 #[test]
2568 fn frozen_engine_slots_are_arc_sized() {
2569 let slot_size = std::mem::size_of::<FrozenEngineSlot>();
2570 let arc_size = std::mem::size_of::<Arc<()>>();
2571
2572 assert!(
2573 slot_size <= 2 * arc_size,
2574 "frozen engine slots should keep immutable snapshot payloads behind Arc: slot_size={slot_size}, arc_size={arc_size}",
2575 );
2576 }
2577}