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 pub fn engine(&self, id: &EngineId) -> Option<EngineSnapshotView<'_>> {
483 self.engine_indices
484 .get(id)
485 .map(|&index| EngineSnapshotView {
486 slot: &self.engines[index],
487 })
488 }
489
490 #[cfg(test)]
491 pub(crate) fn engine_ids_for_test(&self) -> impl Iterator<Item = &EngineId> {
492 self.engines.iter().map(FrozenEngineSlot::engine_id)
493 }
494
495 #[cfg(test)]
496 pub(crate) fn transfer_routes_for_test(&self) -> impl Iterator<Item = &ResolvedTransferRoute> {
497 self.transfers.iter().map(|(route, _)| route)
498 }
499
500 pub(super) fn engine_views_for_preparation(
501 &self,
502 ) -> impl Iterator<Item = EngineSnapshotView<'_>> + '_ {
503 self.engines.iter().map(|slot| EngineSnapshotView { slot })
504 }
505
506 pub(super) fn extension_slot_for_preparation(
507 &self,
508 family_id: ExtensionFamilyId,
509 engine_id: &EngineId,
510 ) -> Option<ExtensionEngineSnapshotView<'_>> {
511 self.extensions.slot_for_preparation(family_id, engine_id)
512 }
513
514 pub(super) fn transfer_registry_for_preparation(&self) -> FrozenTransferRegistry {
515 self.transfers.clone()
516 }
517
518 #[cfg(test)]
519 pub(crate) fn extension_slots_for_test(
520 &self,
521 ) -> impl Iterator<
522 Item = (
523 &ExtensionModuleId,
524 ExtensionFamilyId,
525 &EngineId,
526 RegistrationIdentity,
527 ),
528 > {
529 self.extensions.slots_for_test()
530 }
531
532 #[cfg(test)]
533 pub(crate) fn extension_slot_identity_for_test(
534 &self,
535 family_id: ExtensionFamilyId,
536 engine_id: &EngineId,
537 ) -> Option<RegistrationIdentity> {
538 self.extensions.slot_identity_for_test(family_id, engine_id)
539 }
540
541 #[cfg(test)]
542 pub(crate) fn extension_slot_full_for_test(
543 &self,
544 family_id: ExtensionFamilyId,
545 engine_id: &EngineId,
546 ) -> Option<ExtensionSlotFullForTest<'_>> {
547 self.extensions.slot_full_for_test(family_id, engine_id)
548 }
549
550 #[cfg(test)]
551 pub(super) fn cache_owners_for_test(&self) -> &[FrozenCacheOwner] {
552 &self.cache_owners
553 }
554
555 pub(super) fn cache_owners_for_runtime(&self) -> &[FrozenCacheOwner] {
556 &self.cache_owners
557 }
558}
559
560impl fmt::Debug for RuntimeConfigSnapshot {
561 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
562 formatter
563 .debug_struct("RuntimeConfigSnapshot")
564 .field("runtime_id", &self.runtime_id)
565 .field("epoch", &self.epoch)
566 .field("execution_policy", &self.policy)
567 .field("engine_count", &self.engines.len())
568 .field("extension_module_count", &self.extensions.module_count())
569 .field("extension_engine_count", &self.extensions.engine_count())
570 .field("transfer_provider_count", &self.transfers.len())
571 .field("cache_owner_count", &self.cache_owners.len())
572 .finish_non_exhaustive()
573 }
574}
575
576struct RuntimeState {
577 runtime_id: RuntimeId,
578 issuer: NonZeroU64,
579 next_registration_ordinal: AtomicU64,
580 active: RwLock<Arc<RuntimeConfigSnapshot>>,
581 published_epoch: AtomicU64,
582 caches: RuntimeCacheSet<PreparedEntryKey, PreparedProgram>,
583}
584
585#[derive(Clone)]
599pub struct Runtime(Arc<RuntimeState>);
600
601impl Runtime {
602 pub fn builder() -> RuntimeConfigBuilder {
604 RuntimeConfigBuilder::new()
605 }
606
607 pub fn id(&self) -> RuntimeId {
609 self.0.runtime_id
610 }
611
612 pub fn snapshot(&self) -> Result<Arc<RuntimeConfigSnapshot>, RuntimeStateError> {
619 self.0
620 .active
621 .read()
622 .map(|snapshot| Arc::clone(&snapshot))
623 .map_err(|_| RuntimeStateError::Poisoned {
624 lock: "runtime.active",
625 })
626 }
627
628 pub fn epoch(&self) -> Result<RuntimeEpoch, RuntimeStateError> {
636 match NonZeroU64::new(self.0.published_epoch.load(Ordering::Acquire)) {
637 Some(value) => Ok(RuntimeEpoch::from_nonzero(value)),
638 None => Err(RuntimeStateError::Poisoned {
639 lock: "runtime.published_epoch",
640 }),
641 }
642 }
643
644 pub fn prepared_cache_limits(&self) -> Result<PreparedPlanCacheLimits, RuntimeStateError> {
663 self.0.caches.prepared().limits()
664 }
665
666 pub fn set_prepared_cache_limits(
693 &self,
694 limits: PreparedPlanCacheLimits,
695 ) -> Result<(), RuntimeStateError> {
696 self.0.caches.prepared().set_limits(limits)
697 }
698
699 pub fn clear_prepared_cache(&self) -> Result<(), RuntimeStateError> {
718 self.0.caches.prepared().clear()
719 }
720
721 pub fn cache_stats(&self) -> Result<RuntimeCacheStats, RuntimeCacheError> {
741 super::preparation::cache_stats(self, &self.0.caches)
742 }
743
744 pub fn clear_caches(&self) -> Result<(), RuntimeCacheError> {
764 super::preparation::clear_caches(self, &self.0.caches)
765 }
766
767 #[allow(
768 dead_code,
769 reason = "Phase 5 graph execution consumes crate-private prepared programs"
770 )]
771 pub(crate) fn prepare_for(
772 &self,
773 frozen: &FrozenProgram,
774 signature: &InputSignature,
775 options: &PrepareOptions,
776 ) -> PreparedProgramResult<Arc<PreparedProgram>> {
777 super::preparation::prepare_for(self, &self.0.caches, frozen, signature, options)
778 }
779
780 pub(crate) fn prepare_compiled_for(
781 &self,
782 program: &CompiledGraph,
783 signature: &InputSignature,
784 options: &PrepareOptions,
785 ) -> PreparedProgramResult<Arc<PreparedProgram>> {
786 super::preparation::prepare_compiled_for(self, &self.0.caches, program, signature, options)
787 }
788
789 pub fn run_compiled(
827 &self,
828 program: &CompiledGraph,
829 inputs: &[&Tensor],
830 ) -> crate::Result<Vec<Tensor>> {
831 super::execution::run_compiled(self, program, inputs)
832 }
833
834 pub fn prepare_compiled(
859 &self,
860 program: &CompiledGraph,
861 inputs: &[&Tensor],
862 ) -> crate::Result<super::execution::PreparedCompiledGraph> {
863 super::execution::prepare_compiled(self, program, inputs)
864 }
865
866 pub fn run_prepared(
876 &self,
877 prepared: &super::execution::PreparedCompiledGraph,
878 inputs: &[&Tensor],
879 ) -> crate::Result<Vec<Tensor>> {
880 super::execution::run_prepared(self, prepared, inputs)
881 }
882
883 pub fn submit(
899 &self,
900 program: &CompiledGraph,
901 inputs: &[&Tensor],
902 ) -> crate::Result<super::execution::ExecutionHandle> {
903 super::execution::submit(self, program, inputs)
904 }
905
906 pub fn run_compiled_values(
944 &self,
945 program: &CompiledGraph,
946 inputs: &[&Tensor],
947 ) -> crate::Result<Vec<TensorValue>> {
948 super::execution::run_compiled_values(self, program, inputs)
949 }
950
951 pub fn reconfigure(
967 &self,
968 edit: impl FnOnce(&mut RuntimeReconfiguration<'_>) -> Result<(), RuntimeConfigError>,
969 ) -> Result<RuntimeEpoch, RuntimeReconfigureError> {
970 let base = self
971 .snapshot()
972 .map_err(|source| RuntimeReconfigureError::State { source })?;
973 let mut candidate = CandidateConfig::from_snapshot(&base)
974 .map_err(|source| RuntimeReconfigureError::Edit { source })?;
975 let mut changed = false;
976 {
977 let mut reconfiguration = RuntimeReconfiguration {
978 candidate: &mut candidate,
979 changed: &mut changed,
980 };
981 edit(&mut reconfiguration)
982 .map_err(|source| RuntimeReconfigureError::Edit { source })?;
983 }
984
985 if !changed {
986 return Ok(base.epoch());
987 }
988 let next_identity_ordinal = NonZeroU64::new(
989 self.0.next_registration_ordinal.load(Ordering::SeqCst),
990 )
991 .ok_or(RuntimeReconfigureError::Edit {
992 source: RuntimeConfigError::IdentityExhausted,
993 })?;
994 let (bound_candidate, post_ordinal) =
995 validate_candidate(candidate, self.0.issuer, next_identity_ordinal)
996 .map_err(|source| RuntimeReconfigureError::Edit { source })?;
997
998 let next_epoch =
999 base.epoch()
1000 .checked_next()
1001 .ok_or(RuntimeReconfigureError::EpochExhausted {
1002 current: base.epoch(),
1003 })?;
1004
1005 let mut guard = self
1006 .0
1007 .active
1008 .write()
1009 .map_err(|_| RuntimeReconfigureError::State {
1010 source: RuntimeStateError::Poisoned {
1011 lock: "runtime.active",
1012 },
1013 })?;
1014 if !Arc::ptr_eq(&*guard, &base) {
1015 return Err(RuntimeReconfigureError::ConcurrentReconfiguration {
1016 base: base.epoch(),
1017 current: guard.epoch(),
1018 });
1019 }
1020
1021 let next_snapshot = Arc::new(
1022 freeze_candidate(self.0.runtime_id, next_epoch, bound_candidate)
1023 .map_err(|source| RuntimeReconfigureError::Edit { source })?,
1024 );
1025
1026 self.0
1027 .next_registration_ordinal
1028 .store(post_ordinal.get(), Ordering::SeqCst);
1029 *guard = next_snapshot;
1030 self.0
1031 .published_epoch
1032 .store(next_epoch.get().get(), Ordering::Release);
1033 Ok(next_epoch)
1034 }
1035
1036 #[cfg(test)]
1037 pub(crate) fn force_epoch_for_test(&self, epoch: RuntimeEpoch) {
1038 let mut guard = self.0.active.write().expect("test runtime lock");
1039 let mut replacement = (**guard).clone();
1040 replacement.epoch = epoch;
1041 *guard = Arc::new(replacement);
1042 self.0
1043 .published_epoch
1044 .store(epoch.get().get(), Ordering::Release);
1045 }
1046
1047 #[cfg(test)]
1048 pub(crate) fn force_next_registration_ordinal_for_test(&self, next: NonZeroU64) {
1049 self.0
1050 .next_registration_ordinal
1051 .store(next.get(), Ordering::SeqCst);
1052 }
1053
1054 #[cfg(test)]
1055 pub(crate) fn poison_active_lock_for_test(&self) {
1056 let state = Arc::clone(&self.0);
1057 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || {
1058 let _guard = state.active.write().expect("test runtime lock");
1059 panic!("poison runtime.active for test");
1060 }));
1061 }
1062}
1063
1064impl fmt::Debug for Runtime {
1065 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1066 formatter
1067 .debug_struct("Runtime")
1068 .field("runtime_id", &self.0.runtime_id)
1069 .field("published_epoch", &self.epoch().ok())
1070 .finish_non_exhaustive()
1071 }
1072}
1073
1074pub struct RuntimeConfigBuilder {
1088 candidate: CandidateConfig,
1089}
1090
1091impl RuntimeConfigBuilder {
1092 pub fn new() -> Self {
1094 Self {
1095 candidate: CandidateConfig::empty(),
1096 }
1097 }
1098
1099 pub fn execution_policy(&mut self, value: ExecutionPolicy) -> &mut Self {
1101 self.candidate.policy = value;
1102 self
1103 }
1104
1105 pub fn register_engine(
1112 &mut self,
1113 value: EngineRegistration,
1114 ) -> Result<&mut Self, RuntimeConfigError> {
1115 let mut changed = false;
1116 register_engine_candidate(&mut self.candidate, value, &mut changed)?;
1117 Ok(self)
1118 }
1119
1120 pub fn replace_engine(
1126 &mut self,
1127 value: EngineRegistration,
1128 ) -> Result<&mut Self, RuntimeConfigError> {
1129 let mut changed = false;
1130 replace_engine_candidate(&mut self.candidate, value, &mut changed)?;
1131 Ok(self)
1132 }
1133
1134 pub fn remove_engine(&mut self, id: &EngineId) -> Result<&mut Self, RuntimeConfigError> {
1140 let mut changed = false;
1141 remove_engine_candidate(&mut self.candidate, id, &mut changed)?;
1142 Ok(self)
1143 }
1144
1145 pub fn install_extension_module(
1152 &mut self,
1153 value: Arc<dyn ExtensionModule>,
1154 ) -> Result<&mut Self, RuntimeConfigError> {
1155 let mut changed = false;
1156 install_extension_module_candidate(&mut self.candidate, value, &mut changed)?;
1157 Ok(self)
1158 }
1159
1160 pub fn register_transfer_provider(
1240 &mut self,
1241 source: TransferEndpoint,
1242 destination: TransferEndpoint,
1243 provider: Arc<dyn TransferProvider>,
1244 ) -> Result<&mut Self, RuntimeConfigError> {
1245 let mut changed = false;
1246 register_transfer_provider_candidate(
1247 &mut self.candidate,
1248 source,
1249 destination,
1250 provider,
1251 &mut changed,
1252 )?;
1253 Ok(self)
1254 }
1255
1256 pub fn remove_transfer_provider(
1267 &mut self,
1268 source: TransferEndpoint,
1269 destination: TransferEndpoint,
1270 ) -> Result<&mut Self, RuntimeConfigError> {
1271 let mut changed = false;
1272 remove_transfer_provider_candidate(&mut self.candidate, source, destination, &mut changed)?;
1273 Ok(self)
1274 }
1275
1276 pub fn replace_extension_module(
1283 &mut self,
1284 value: Arc<dyn ExtensionModule>,
1285 ) -> Result<&mut Self, RuntimeConfigError> {
1286 let mut changed = false;
1287 replace_extension_module_candidate(&mut self.candidate, value, &mut changed)?;
1288 Ok(self)
1289 }
1290
1291 pub fn remove_extension_module(
1298 &mut self,
1299 id: &ExtensionModuleId,
1300 ) -> Result<&mut Self, RuntimeConfigError> {
1301 let mut changed = false;
1302 remove_extension_module_candidate(&mut self.candidate, id, &mut changed)?;
1303 Ok(self)
1304 }
1305
1306 pub fn build(self) -> Result<Runtime, RuntimeConfigError> {
1316 let runtime_id = RuntimeId::from_nonzero(allocate_nonzero(&NEXT_RUNTIME_ID)?);
1317 let issuer = allocate_nonzero(&NEXT_REGISTRATION_ISSUER)?;
1318 let (bound_candidate, post_ordinal) =
1319 validate_candidate(self.candidate, issuer, INITIAL_REGISTRATION_ORDINAL)?;
1320 let epoch = RuntimeEpoch::one();
1321 let snapshot = Arc::new(freeze_candidate(runtime_id, epoch, bound_candidate)?);
1322 let state = RuntimeState {
1323 runtime_id,
1324 issuer,
1325 next_registration_ordinal: AtomicU64::new(post_ordinal.get()),
1326 active: RwLock::new(snapshot),
1327 published_epoch: AtomicU64::new(epoch.get().get()),
1328 caches: RuntimeCacheSet::new(PreparedPlanCacheLimits::default()),
1329 };
1330 Ok(Runtime(Arc::new(state)))
1331 }
1332}
1333
1334impl Default for RuntimeConfigBuilder {
1335 fn default() -> Self {
1336 Self::new()
1337 }
1338}
1339
1340impl fmt::Debug for RuntimeConfigBuilder {
1341 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1342 formatter
1343 .debug_struct("RuntimeConfigBuilder")
1344 .field("execution_policy", &self.candidate.policy)
1345 .field("engine_count", &self.candidate.engines.len())
1346 .field("extension_module_count", &self.candidate.modules.len())
1347 .field("transfer_provider_count", &self.candidate.transfers.len())
1348 .finish_non_exhaustive()
1349 }
1350}
1351
1352pub struct RuntimeReconfiguration<'a> {
1354 candidate: &'a mut CandidateConfig,
1355 changed: &'a mut bool,
1356}
1357
1358impl RuntimeReconfiguration<'_> {
1359 pub fn execution_policy(&mut self, policy: ExecutionPolicy) -> &mut Self {
1361 if self.candidate.policy != policy {
1362 self.candidate.policy = policy;
1363 *self.changed = true;
1364 }
1365 self
1366 }
1367
1368 pub fn register_engine(
1375 &mut self,
1376 value: EngineRegistration,
1377 ) -> Result<&mut Self, RuntimeConfigError> {
1378 register_engine_candidate(self.candidate, value, self.changed)?;
1379 Ok(self)
1380 }
1381
1382 pub fn replace_engine(
1388 &mut self,
1389 value: EngineRegistration,
1390 ) -> Result<&mut Self, RuntimeConfigError> {
1391 replace_engine_candidate(self.candidate, value, self.changed)?;
1392 Ok(self)
1393 }
1394
1395 pub fn remove_engine(&mut self, id: &EngineId) -> Result<&mut Self, RuntimeConfigError> {
1401 remove_engine_candidate(self.candidate, id, self.changed)?;
1402 Ok(self)
1403 }
1404
1405 pub fn install_extension_module(
1412 &mut self,
1413 value: Arc<dyn ExtensionModule>,
1414 ) -> Result<&mut Self, RuntimeConfigError> {
1415 install_extension_module_candidate(self.candidate, value, self.changed)?;
1416 Ok(self)
1417 }
1418
1419 pub fn register_transfer_provider(
1494 &mut self,
1495 source: TransferEndpoint,
1496 destination: TransferEndpoint,
1497 provider: Arc<dyn TransferProvider>,
1498 ) -> Result<&mut Self, RuntimeConfigError> {
1499 register_transfer_provider_candidate(
1500 self.candidate,
1501 source,
1502 destination,
1503 provider,
1504 self.changed,
1505 )?;
1506 Ok(self)
1507 }
1508
1509 pub fn remove_transfer_provider(
1517 &mut self,
1518 source: TransferEndpoint,
1519 destination: TransferEndpoint,
1520 ) -> Result<&mut Self, RuntimeConfigError> {
1521 remove_transfer_provider_candidate(self.candidate, source, destination, self.changed)?;
1522 Ok(self)
1523 }
1524
1525 pub fn replace_extension_module(
1533 &mut self,
1534 value: Arc<dyn ExtensionModule>,
1535 ) -> Result<&mut Self, RuntimeConfigError> {
1536 replace_extension_module_candidate(self.candidate, value, self.changed)?;
1537 Ok(self)
1538 }
1539
1540 pub fn remove_extension_module(
1547 &mut self,
1548 id: &ExtensionModuleId,
1549 ) -> Result<&mut Self, RuntimeConfigError> {
1550 remove_extension_module_candidate(self.candidate, id, self.changed)?;
1551 Ok(self)
1552 }
1553}
1554
1555impl fmt::Debug for RuntimeReconfiguration<'_> {
1556 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1557 formatter
1558 .debug_struct("RuntimeReconfiguration")
1559 .field("engine_count", &self.candidate.engines.len())
1560 .field("extension_module_count", &self.candidate.modules.len())
1561 .field("transfer_provider_count", &self.candidate.transfers.len())
1562 .field("changed", &*self.changed)
1563 .finish_non_exhaustive()
1564 }
1565}
1566
1567#[derive(Clone, Copy)]
1579pub struct EngineSnapshotView<'a> {
1580 slot: &'a FrozenEngineSlot,
1581}
1582
1583impl<'a> EngineSnapshotView<'a> {
1584 pub fn engine_id(&self) -> &'a EngineId {
1586 self.slot.engine_id()
1587 }
1588
1589 pub fn provider_device_identity(&self) -> &'a super::ProviderDeviceIdentity {
1599 self.slot.provider_device_identity()
1600 }
1601
1602 pub fn registration_identity(&self) -> RegistrationIdentity {
1604 self.slot.metadata().identity
1605 }
1606
1607 pub fn context_identity(&self) -> ExecutionContextIdentity {
1609 self.slot.context_identity()
1610 }
1611
1612 pub fn event_domain_id(&self) -> EventDomainId {
1614 self.slot.metadata().event_domain_id
1615 }
1616
1617 pub(super) fn executable_witness(&self) -> Option<&'a Arc<ExecutableEngineSnapshot>> {
1618 self.slot.executable()
1619 }
1620
1621 pub fn hardware_class(&self) -> &'a HardwareClassId {
1623 self.slot.hardware_class()
1624 }
1625
1626 pub fn capabilities(&self) -> &'a CoreCapabilityBundle {
1628 self.slot.capabilities()
1629 }
1630
1631 pub(super) fn storage_classes(&self) -> &'a [StorageClass] {
1632 self.slot.storage_classes()
1633 }
1634
1635 pub(super) fn default_storage_class(&self) -> &'a StorageClass {
1636 self.slot.default_storage_class()
1637 }
1638
1639 pub(super) fn accepts_input_signature(
1640 &self,
1641 input: &super::InputSignatureEntry,
1642 storage_class: &StorageClass,
1643 ) -> bool {
1644 self.slot
1645 .executable()
1646 .is_some_and(|snapshot| snapshot.accepts_input_signature(input, storage_class))
1647 }
1648
1649 #[cfg(test)]
1650 pub(crate) fn has_execution_engine_for_test(&self) -> bool {
1651 self.slot.executable().is_some()
1652 }
1653}
1654
1655impl fmt::Debug for EngineSnapshotView<'_> {
1656 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1657 formatter
1658 .debug_struct("EngineSnapshotView")
1659 .field("engine_id", self.engine_id())
1660 .field("registration_identity", &self.registration_identity())
1661 .field("context_identity", &self.context_identity())
1662 .field("hardware_class", self.hardware_class())
1663 .field("capabilities", self.capabilities())
1664 .finish()
1665 }
1666}
1667
1668fn default_execution_policy() -> ExecutionPolicy {
1669 ExecutionPolicy::new(super::Determinism::Fast, None, 0)
1670}
1671
1672fn register_engine_candidate(
1673 candidate: &mut CandidateConfig,
1674 registration: EngineRegistration,
1675 changed: &mut bool,
1676) -> Result<(), RuntimeConfigError> {
1677 let engine_id = registration.engine_id().clone();
1678 match candidate.engines.get(&engine_id) {
1679 Some(existing) if existing.registration.candidate_identical(®istration) => Ok(()),
1680 Some(_) => Err(RuntimeConfigError::DuplicateEngine { engine_id }),
1681 None => {
1682 ensure_unique_provider_device_target(candidate, ®istration)?;
1683 candidate.engines.insert(
1684 engine_id,
1685 CandidateEngineRecord {
1686 registration,
1687 identity: CandidateRegistrationIdentity::New,
1688 },
1689 );
1690 *changed = true;
1691 Ok(())
1692 }
1693 }
1694}
1695
1696fn replace_engine_candidate(
1697 candidate: &mut CandidateConfig,
1698 registration: EngineRegistration,
1699 changed: &mut bool,
1700) -> Result<(), RuntimeConfigError> {
1701 let engine_id = registration.engine_id().clone();
1702 let Some(existing) = candidate.engines.get(&engine_id) else {
1703 return Err(RuntimeConfigError::MissingEngine { engine_id });
1704 };
1705 if existing.registration.candidate_identical(®istration) {
1706 return Ok(());
1707 }
1708 if existing.registration.provider_device_identity() != registration.provider_device_identity() {
1709 return Err(RuntimeConfigError::EngineTargetRebind {
1710 engine_id,
1711 current: existing.registration.provider_device_identity().clone(),
1712 replacement: registration.provider_device_identity().clone(),
1713 });
1714 }
1715 ensure_unique_provider_device_target_except(candidate, ®istration, &engine_id)?;
1716 candidate.engines.insert(
1717 engine_id,
1718 CandidateEngineRecord {
1719 registration,
1720 identity: CandidateRegistrationIdentity::New,
1721 },
1722 );
1723 *changed = true;
1724 Ok(())
1725}
1726
1727fn remove_engine_candidate(
1728 candidate: &mut CandidateConfig,
1729 id: &EngineId,
1730 changed: &mut bool,
1731) -> Result<(), RuntimeConfigError> {
1732 match candidate.engines.remove(id) {
1733 Some(_) => {
1734 *changed = true;
1735 Ok(())
1736 }
1737 None => Err(RuntimeConfigError::MissingEngine {
1738 engine_id: id.clone(),
1739 }),
1740 }
1741}
1742
1743fn install_extension_module_candidate(
1744 candidate: &mut CandidateConfig,
1745 module: Arc<dyn ExtensionModule>,
1746 changed: &mut bool,
1747) -> Result<(), RuntimeConfigError> {
1748 let module_id = module.module_id().clone();
1749 match candidate.modules.get(&module_id) {
1750 Some(existing) if existing.module_identical(&module) => Ok(()),
1751 Some(_) => Err(RuntimeConfigError::ExtensionModule {
1752 source: ExtensionModuleError::ConflictingModule { module_id },
1753 }),
1754 None => {
1755 let record = configure_module(module)
1756 .map_err(|source| RuntimeConfigError::ExtensionModule { source })?;
1757 candidate.modules.insert(module_id, record);
1758 *changed = true;
1759 Ok(())
1760 }
1761 }
1762}
1763
1764fn replace_extension_module_candidate(
1765 candidate: &mut CandidateConfig,
1766 module: Arc<dyn ExtensionModule>,
1767 changed: &mut bool,
1768) -> Result<(), RuntimeConfigError> {
1769 let module_id = module.module_id().clone();
1770 match candidate.modules.get(&module_id) {
1771 Some(existing) if existing.module_identical(&module) => Ok(()),
1772 _ => {
1773 let record = configure_module(module)
1774 .map_err(|source| RuntimeConfigError::ExtensionModule { source })?;
1775 candidate.modules.insert(module_id, record);
1776 *changed = true;
1777 Ok(())
1778 }
1779 }
1780}
1781
1782fn remove_extension_module_candidate(
1783 candidate: &mut CandidateConfig,
1784 id: &ExtensionModuleId,
1785 changed: &mut bool,
1786) -> Result<(), RuntimeConfigError> {
1787 if candidate.modules.remove(id).is_some() {
1788 *changed = true;
1789 }
1790 Ok(())
1791}
1792
1793fn register_transfer_provider_candidate(
1794 candidate: &mut CandidateConfig,
1795 source: TransferEndpoint,
1796 destination: TransferEndpoint,
1797 provider: Arc<dyn TransferProvider>,
1798 changed: &mut bool,
1799) -> Result<(), RuntimeConfigError> {
1800 let key = TransferRoute::new(source, destination);
1801 match candidate.transfers.get(&key) {
1802 Some(existing) if Arc::ptr_eq(&existing.provider, &provider) => Ok(()),
1803 Some(_) => Err(RuntimeConfigError::ConflictingRegistration {
1804 key: RegistrationKey::TransferProvider {
1805 source: key.source().clone(),
1806 destination: key.destination().clone(),
1807 },
1808 }),
1809 None => {
1810 candidate.transfers.insert(
1811 key,
1812 CandidateTransferRecord {
1813 provider,
1814 binding: CandidateTransferBinding::New,
1815 },
1816 );
1817 *changed = true;
1818 Ok(())
1819 }
1820 }
1821}
1822
1823fn remove_transfer_provider_candidate(
1824 candidate: &mut CandidateConfig,
1825 source: TransferEndpoint,
1826 destination: TransferEndpoint,
1827 changed: &mut bool,
1828) -> Result<(), RuntimeConfigError> {
1829 let key = TransferRoute::new(source, destination);
1830 if candidate.transfers.remove(&key).is_none() {
1831 return Err(RuntimeConfigError::MissingTransferProvider {
1832 source_endpoint: key.source().clone(),
1833 destination: key.destination().clone(),
1834 });
1835 }
1836 *changed = true;
1837 Ok(())
1838}
1839
1840fn ensure_unique_provider_device_target(
1841 candidate: &CandidateConfig,
1842 registration: &EngineRegistration,
1843) -> Result<(), RuntimeConfigError> {
1844 ensure_unique_provider_device_target_except(candidate, registration, registration.engine_id())
1845}
1846
1847fn ensure_unique_provider_device_target_except(
1848 candidate: &CandidateConfig,
1849 registration: &EngineRegistration,
1850 ignored_engine_id: &EngineId,
1851) -> Result<(), RuntimeConfigError> {
1852 if let Some((first_engine_id, _)) = candidate.engines.iter().find(|(engine_id, record)| {
1853 *engine_id != ignored_engine_id
1854 && record.registration.provider_device_identity()
1855 == registration.provider_device_identity()
1856 }) {
1857 return Err(RuntimeConfigError::DuplicateProviderDeviceTarget {
1858 provider_device_identity: registration.provider_device_identity().clone(),
1859 first_engine_id: first_engine_id.clone(),
1860 duplicate_engine_id: registration.engine_id().clone(),
1861 });
1862 }
1863 Ok(())
1864}
1865
1866fn validate_candidate(
1867 candidate: CandidateConfig,
1868 issuer: NonZeroU64,
1869 next_ordinal: NonZeroU64,
1870) -> Result<(BoundCandidateConfig, NonZeroU64), RuntimeConfigError> {
1871 let mut seen_targets = BTreeMap::<ProviderDeviceIdentity, EngineId>::new();
1872 for (engine_id, record) in &candidate.engines {
1873 if let Some(first_engine_id) = seen_targets.insert(
1874 record.registration.provider_device_identity().clone(),
1875 engine_id.clone(),
1876 ) {
1877 return Err(RuntimeConfigError::DuplicateProviderDeviceTarget {
1878 provider_device_identity: record.registration.provider_device_identity().clone(),
1879 first_engine_id,
1880 duplicate_engine_id: engine_id.clone(),
1881 });
1882 }
1883 }
1884
1885 let mut bound_transfers = BTreeMap::new();
1886 for (route, record) in &candidate.transfers {
1887 let source_binding = validate_transfer_endpoint(&candidate, route.source())?;
1888 let destination_binding = validate_transfer_endpoint(&candidate, route.destination())?;
1889 let preserved = match &record.binding {
1890 CandidateTransferBinding::New => None,
1891 CandidateTransferBinding::Preserved {
1892 source,
1893 destination,
1894 } => Some((source, destination)),
1895 };
1896 if let Some((registered_source, registered_destination)) = preserved {
1897 if registered_source != &source_binding {
1898 return Err(RuntimeConfigError::StaleTransferRoute {
1899 source_endpoint: route.source().clone(),
1900 destination: route.destination().clone(),
1901 endpoint: route.source().clone(),
1902 registered: Box::new(registered_source.clone()),
1903 current: Box::new(source_binding.clone()),
1904 });
1905 }
1906 if registered_destination != &destination_binding {
1907 return Err(RuntimeConfigError::StaleTransferRoute {
1908 source_endpoint: route.source().clone(),
1909 destination: route.destination().clone(),
1910 endpoint: route.destination().clone(),
1911 registered: Box::new(registered_destination.clone()),
1912 current: Box::new(destination_binding.clone()),
1913 });
1914 }
1915 }
1916 bound_transfers.insert(
1917 route.clone(),
1918 BoundCandidateTransferRecord {
1919 provider: Arc::clone(&record.provider),
1920 source: source_binding,
1921 destination: destination_binding,
1922 },
1923 );
1924 }
1925 let mut seen = BTreeMap::<(ExtensionFamilyId, EngineId), ExtensionModuleId>::new();
1926 for (module_id, module) in &candidate.modules {
1927 for family_engine in module.engines.keys() {
1928 if seen
1929 .insert(
1930 (family_engine.0, family_engine.1.clone()),
1931 module_id.clone(),
1932 )
1933 .is_some()
1934 {
1935 return Err(RuntimeConfigError::ConflictingRegistration {
1936 key: RegistrationKey::ExtensionEngine {
1937 family: family_engine.0,
1938 engine: family_engine.1.clone(),
1939 },
1940 });
1941 }
1942 }
1943 }
1944
1945 let CandidateConfig {
1946 policy,
1947 engines,
1948 modules,
1949 transfers: _,
1950 } = candidate;
1951 let mut allocator = RegistrationIdentityAllocator::new(issuer, next_ordinal);
1952 let engines = engines
1953 .into_iter()
1954 .map(|(engine_id, record)| {
1955 let identity = match record.identity {
1956 CandidateRegistrationIdentity::New => allocator.allocate()?,
1957 CandidateRegistrationIdentity::Preserved(identity) => identity,
1958 };
1959 Ok((
1960 engine_id,
1961 BoundCandidateEngineRecord {
1962 registration: record.registration,
1963 identity,
1964 },
1965 ))
1966 })
1967 .collect::<Result<BTreeMap<_, _>, RuntimeConfigError>>()?;
1968 let modules = modules
1969 .into_iter()
1970 .map(|(module_id, module)| {
1971 let mut allocate = || allocator.allocate();
1972 Ok((module_id, bind_candidate_module(module, &mut allocate)?))
1973 })
1974 .collect::<Result<BTreeMap<_, _>, RuntimeConfigError>>()?;
1975 Ok((
1976 BoundCandidateConfig {
1977 policy,
1978 engines,
1979 modules,
1980 transfers: bound_transfers,
1981 },
1982 allocator.next_ordinal(),
1983 ))
1984}
1985
1986fn validate_transfer_endpoint(
1987 candidate: &CandidateConfig,
1988 endpoint: &TransferEndpoint,
1989) -> Result<ProviderDeviceIdentity, RuntimeConfigError> {
1990 let Some(engine) = candidate.engines.get(endpoint.engine_id()) else {
1991 return Err(RuntimeConfigError::UnknownTransferEndpointEngine {
1992 endpoint: endpoint.clone(),
1993 });
1994 };
1995 if !engine
1996 .registration
1997 .storage_classes()
1998 .contains(endpoint.storage_class())
1999 {
2000 return Err(RuntimeConfigError::UnsupportedTransferEndpointStorage {
2001 endpoint: endpoint.clone(),
2002 });
2003 }
2004 Ok(engine.registration.provider_device_identity().clone())
2005}
2006
2007struct RegistrationIdentityAllocator {
2008 issuer: NonZeroU64,
2009 next: NonZeroU64,
2010}
2011
2012impl RegistrationIdentityAllocator {
2013 fn new(issuer: NonZeroU64, next: NonZeroU64) -> Self {
2014 Self { issuer, next }
2015 }
2016
2017 fn allocate(&mut self) -> Result<RegistrationIdentity, RuntimeConfigError> {
2018 let identity = RegistrationIdentity::new(self.issuer, self.next);
2019 let next = self
2020 .next
2021 .get()
2022 .checked_add(1)
2023 .and_then(NonZeroU64::new)
2024 .ok_or(RuntimeConfigError::IdentityExhausted)?;
2025 self.next = next;
2026 Ok(identity)
2027 }
2028
2029 fn next_ordinal(&self) -> NonZeroU64 {
2030 self.next
2031 }
2032}
2033
2034fn freeze_candidate(
2035 runtime_id: RuntimeId,
2036 epoch: RuntimeEpoch,
2037 candidate: BoundCandidateConfig,
2038) -> Result<RuntimeConfigSnapshot, RuntimeConfigError> {
2039 let mut engines = Vec::with_capacity(candidate.engines.len());
2040 let mut engine_indices = BTreeMap::new();
2041 let mut engine_locations = BTreeMap::new();
2042 let mut cache_owners = Vec::new();
2043 for (index, (engine_id, record)) in candidate.engines.into_iter().enumerate() {
2044 let BoundCandidateEngineRecord {
2045 registration,
2046 identity,
2047 } = record;
2048 let event_domain_id = EventDomainId::new(runtime_id, epoch, identity);
2049 let (state, candidate_token) = registration.into_state_and_token();
2050 let provider_device_identity = state.provider_device_identity().clone();
2051 let metadata = FrozenEngineMetadata {
2052 candidate_token,
2053 identity,
2054 event_domain_id,
2055 };
2056 let frozen = match state {
2057 EngineRegistrationState::PreparationOnly { binding } => {
2058 FrozenEngineSlot::PreparationOnly(Arc::new(PreparationOnlyEngineSnapshot {
2059 metadata,
2060 binding,
2061 }))
2062 }
2063 EngineRegistrationState::Executable(binding) => {
2064 if let Some(owner) = binding.contract().cache_owner().cloned() {
2065 cache_owners.push(FrozenCacheOwner {
2066 id: engine_cache_owner_id(&engine_id),
2067 kind: FrozenCacheOwnerKind::Engine,
2068 owner,
2069 });
2070 }
2071 cache_owners.push(FrozenCacheOwner {
2072 id: engine_extension_cache_owner_id(&engine_id),
2073 kind: FrozenCacheOwnerKind::Extension,
2074 owner: execution::extension_cache_owner(binding.contract().executor().clone()),
2075 });
2076 FrozenEngineSlot::Executable(Arc::new(ExecutableEngineSnapshot {
2077 metadata,
2078 binding,
2079 }))
2080 }
2081 };
2082 engine_locations.insert(
2083 engine_id.clone(),
2084 (provider_device_identity, event_domain_id),
2085 );
2086 engine_indices.insert(engine_id, index);
2087 engines.push(frozen);
2088 }
2089 let extensions = freeze_extension_slots(candidate.modules)?;
2090 for (id, owner) in extensions.cache_owner_records() {
2091 cache_owners.push(FrozenCacheOwner {
2092 id,
2093 kind: FrozenCacheOwnerKind::Extension,
2094 owner,
2095 });
2096 }
2097 let mut transfers = BTreeMap::new();
2098 for (route, record) in candidate.transfers {
2099 let BoundCandidateTransferRecord {
2100 provider,
2101 source: source_binding,
2102 destination: destination_binding,
2103 } = record;
2104 let (_, source_event_domain_id) = bound_engine_location(&engine_locations, route.source())?;
2105 let (_, destination_event_domain_id) =
2106 bound_engine_location(&engine_locations, route.destination())?;
2107 let resolved_route = ResolvedTransferRoute::new(
2108 ResolvedTransferEndpoint::new(
2109 route.source().clone(),
2110 source_binding,
2111 *source_event_domain_id,
2112 ),
2113 ResolvedTransferEndpoint::new(
2114 route.destination().clone(),
2115 destination_binding,
2116 *destination_event_domain_id,
2117 ),
2118 );
2119 transfers.insert(resolved_route, provider);
2120 }
2121 Ok(RuntimeConfigSnapshot {
2122 runtime_id,
2123 epoch,
2124 policy: candidate.policy,
2125 engines: engines.into(),
2126 engine_indices,
2127 extensions,
2128 transfers: FrozenTransferRegistry::new(transfers),
2129 cache_owners: cache_owners.into(),
2130 })
2131}
2132
2133fn bound_engine_location<'a>(
2134 locations: &'a BTreeMap<EngineId, (ProviderDeviceIdentity, EventDomainId)>,
2135 endpoint: &TransferEndpoint,
2136) -> Result<&'a (ProviderDeviceIdentity, EventDomainId), RuntimeConfigError> {
2137 locations
2138 .get(endpoint.engine_id())
2139 .ok_or_else(|| RuntimeConfigError::BoundCandidateInvariant {
2140 endpoint: endpoint.clone(),
2141 })
2142}
2143
2144fn engine_cache_owner_id(engine_id: &EngineId) -> CacheOwnerId {
2145 let id = engine_id.as_str();
2146 CacheOwnerId::from_canonical_owner_id(Arc::<str>::from(format!("engine[{}]:{id}", id.len())))
2147}
2148
2149fn engine_extension_cache_owner_id(engine_id: &EngineId) -> CacheOwnerId {
2150 let id = engine_id.as_str();
2151 CacheOwnerId::from_canonical_owner_id(Arc::<str>::from(format!(
2152 "extension-executor[{}]:{id}",
2153 id.len()
2154 )))
2155}
2156
2157fn allocate_nonzero(counter: &AtomicU64) -> Result<NonZeroU64, RuntimeConfigError> {
2158 let value = counter
2159 .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |next| {
2160 next.checked_add(1)
2161 })
2162 .map_err(|_| RuntimeConfigError::IdentityExhausted)?;
2163 NonZeroU64::new(value).ok_or(RuntimeConfigError::IdentityExhausted)
2164}
2165
2166#[cfg(test)]
2167mod freeze_tests {
2168 use crate::{ProviderId, TransferRequest};
2169
2170 use super::*;
2171
2172 #[derive(Debug)]
2173 struct FreezeTestContext;
2174
2175 #[derive(Debug)]
2176 struct FreezeTestProvider;
2177
2178 impl TransferProvider for FreezeTestProvider {
2179 fn transfer_blocking(
2180 &self,
2181 _request: TransferRequest<'_>,
2182 ) -> crate::Result<tenferro_tensor::Tensor> {
2183 Err(crate::Error::Internal("freeze test provider".into()))
2184 }
2185 }
2186
2187 fn registration(
2188 engine_id: &str,
2189 target: &str,
2190 ) -> Result<EngineRegistration, RuntimeConfigError> {
2191 let engine_id = EngineId::new(engine_id).map_err(RuntimeConfigError::from)?;
2192 let storage =
2193 StorageClass::new("tenferro.test.freeze.storage").map_err(RuntimeConfigError::from)?;
2194 Ok(EngineRegistration::preparation_only(
2195 super::super::ProviderPreparationBinding::new(
2196 engine_id,
2197 ProviderDeviceIdentity::new(
2198 ProviderId::new("tenferro.test.freeze.provider")
2199 .map_err(RuntimeConfigError::from)?,
2200 target,
2201 )
2202 .map_err(RuntimeConfigError::from)?,
2203 ExecutionContextIdentity::of::<FreezeTestContext>(),
2204 HardwareClassId::new("tenferro.test.freeze.hardware")
2205 .map_err(RuntimeConfigError::from)?,
2206 Arc::from(vec![storage.clone()]),
2207 storage,
2208 CoreCapabilityBundle::default(),
2209 )?,
2210 ))
2211 }
2212
2213 fn candidate(binding: CandidateTransferBinding) -> Result<CandidateConfig, RuntimeConfigError> {
2214 let source_id =
2215 EngineId::new("tenferro.test.freeze.source").map_err(RuntimeConfigError::from)?;
2216 let destination_id =
2217 EngineId::new("tenferro.test.freeze.destination").map_err(RuntimeConfigError::from)?;
2218 let storage =
2219 StorageClass::new("tenferro.test.freeze.storage").map_err(RuntimeConfigError::from)?;
2220 let source_endpoint = TransferEndpoint::new(source_id.clone(), storage.clone());
2221 let destination_endpoint = TransferEndpoint::new(destination_id.clone(), storage);
2222 let mut candidate = CandidateConfig::empty();
2223 let mut changed = false;
2224 register_engine_candidate(
2225 &mut candidate,
2226 registration(source_id.as_str(), "freeze-source")?,
2227 &mut changed,
2228 )?;
2229 register_engine_candidate(
2230 &mut candidate,
2231 registration(destination_id.as_str(), "freeze-destination")?,
2232 &mut changed,
2233 )?;
2234 register_transfer_provider_candidate(
2235 &mut candidate,
2236 source_endpoint.clone(),
2237 destination_endpoint.clone(),
2238 Arc::new(FreezeTestProvider),
2239 &mut changed,
2240 )?;
2241 candidate
2242 .transfers
2243 .get_mut(&TransferRoute::new(source_endpoint, destination_endpoint))
2244 .expect("registered route")
2245 .binding = binding;
2246 Ok(candidate)
2247 }
2248
2249 #[test]
2250 fn validation_owns_stale_route_rejection_and_bound_freeze_is_total() {
2251 let wrong_source = ProviderDeviceIdentity::new(
2252 ProviderId::new("tenferro.test.freeze.provider").unwrap(),
2253 "different-source",
2254 )
2255 .unwrap();
2256 let preserved = CandidateTransferBinding::Preserved {
2257 source: wrong_source,
2258 destination: ProviderDeviceIdentity::new(
2259 ProviderId::new("tenferro.test.freeze.provider").unwrap(),
2260 "freeze-destination",
2261 )
2262 .unwrap(),
2263 };
2264 let result = validate_candidate(
2265 candidate(preserved).unwrap(),
2266 NonZeroU64::new(1).unwrap(),
2267 NonZeroU64::new(1).unwrap(),
2268 );
2269 let error = match result {
2270 Ok(_) => panic!("candidate validation must reject stale preserved bindings"),
2271 Err(error) => error,
2272 };
2273 assert!(matches!(
2274 error,
2275 RuntimeConfigError::StaleTransferRoute { .. }
2276 ));
2277
2278 let (bound, _) = validate_candidate(
2279 candidate(CandidateTransferBinding::New).unwrap(),
2280 NonZeroU64::new(1).unwrap(),
2281 NonZeroU64::new(1).unwrap(),
2282 )
2283 .expect("validation must produce a complete bound candidate");
2284 freeze_candidate(
2285 RuntimeId::from_nonzero(NonZeroU64::new(1).unwrap()),
2286 RuntimeEpoch::one(),
2287 bound,
2288 )
2289 .expect("a bound candidate must freeze without semantic route revalidation");
2290 }
2291
2292 #[test]
2293 fn frozen_engine_slots_are_arc_sized() {
2294 let slot_size = std::mem::size_of::<FrozenEngineSlot>();
2295 let arc_size = std::mem::size_of::<Arc<()>>();
2296
2297 assert!(
2298 slot_size <= 2 * arc_size,
2299 "frozen engine slots should keep immutable snapshot payloads behind Arc: slot_size={slot_size}, arc_size={arc_size}",
2300 );
2301 }
2302}