1use std::collections::BTreeMap;
8use std::fmt;
9
10#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
11pub struct ManagedGenerationId(String);
12
13impl ManagedGenerationId {
14 pub fn generate() -> Self {
15 Self(format!("gen_{}", uuid::Uuid::now_v7()))
16 }
17
18 pub fn parse(value: impl Into<String>) -> Result<Self, GenerationValueError> {
19 let value = value.into();
20 let uuid = value
21 .strip_prefix("gen_")
22 .and_then(|suffix| uuid::Uuid::parse_str(suffix).ok())
23 .filter(|uuid| uuid.get_version() == Some(uuid::Version::SortRand))
24 .ok_or(GenerationValueError::InvalidGenerationId)?;
25 let canonical = format!("gen_{uuid}");
26 if value != canonical {
27 return Err(GenerationValueError::InvalidGenerationId);
28 }
29 Ok(Self(value))
30 }
31
32 pub fn as_str(&self) -> &str {
33 &self.0
34 }
35}
36
37impl fmt::Display for ManagedGenerationId {
38 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
39 formatter.write_str(&self.0)
40 }
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
44pub struct ModelStoreProfileId(String);
45
46impl ModelStoreProfileId {
47 pub fn parse(value: impl Into<String>) -> Result<Self, GenerationValueError> {
48 let value = value.into();
49 if value.is_empty()
50 || value.len() > 64
51 || !value
52 .bytes()
53 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-' || byte == b'_')
54 {
55 return Err(GenerationValueError::InvalidProfileId);
56 }
57 Ok(Self(value))
58 }
59
60 pub fn default_embedding() -> Self {
61 Self("default-embedding".to_string())
62 }
63
64 pub fn as_str(&self) -> &str {
65 &self.0
66 }
67}
68
69impl fmt::Display for ModelStoreProfileId {
70 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
71 formatter.write_str(&self.0)
72 }
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
76pub struct StartupEpoch(u64);
77
78impl StartupEpoch {
79 pub const ZERO: Self = Self(0);
80
81 pub fn new(value: u64) -> Result<Self, GenerationValueError> {
82 if value > i64::MAX as u64 {
83 return Err(GenerationValueError::EpochOutOfRange);
84 }
85 Ok(Self(value))
86 }
87
88 pub fn get(self) -> u64 {
89 self.0
90 }
91
92 pub fn next(self) -> Result<Self, GenerationTransitionError> {
93 let next = self
94 .0
95 .checked_add(1)
96 .ok_or(GenerationTransitionError::EpochExhausted)?;
97 Self::new(next).map_err(|_| GenerationTransitionError::EpochExhausted)
98 }
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum ManagedGenerationState {
103 Inactive,
104 Current,
105 Previous,
106 Invalid,
107}
108
109impl ManagedGenerationState {
110 pub fn as_str(self) -> &'static str {
111 match self {
112 Self::Inactive => "inactive",
113 Self::Current => "current",
114 Self::Previous => "previous",
115 Self::Invalid => "invalid",
116 }
117 }
118
119 pub fn parse(value: &str) -> Result<Self, GenerationValueError> {
120 match value {
121 "inactive" => Ok(Self::Inactive),
122 "current" => Ok(Self::Current),
123 "previous" => Ok(Self::Previous),
124 "invalid" => Ok(Self::Invalid),
125 _ => Err(GenerationValueError::InvalidLifecycleState),
126 }
127 }
128}
129
130#[derive(Debug, Clone, PartialEq, Eq)]
131pub struct ManagedGenerationRecord {
132 pub generation_id: ManagedGenerationId,
133 pub manifest_id: String,
134 pub state: ManagedGenerationState,
135 pub activation_epoch: Option<StartupEpoch>,
136 pub validated_startup_epoch: Option<StartupEpoch>,
137}
138
139#[derive(Debug, Clone, PartialEq, Eq)]
142pub struct StartupValidationEvidence {
143 generation_id: ManagedGenerationId,
144 activation_epoch: StartupEpoch,
145}
146
147impl StartupValidationEvidence {
148 pub fn generation_id(&self) -> &ManagedGenerationId {
149 &self.generation_id
150 }
151
152 pub fn activation_epoch(&self) -> StartupEpoch {
153 self.activation_epoch
154 }
155}
156
157#[derive(Debug, Clone, PartialEq, Eq)]
158pub struct ManagedProfileState {
159 pub profile_id: ModelStoreProfileId,
160 pub startup_epoch: StartupEpoch,
161 pub current_generation_id: Option<ManagedGenerationId>,
162 pub previous_generation_id: Option<ManagedGenerationId>,
163 pub state_revision: u64,
164}
165
166#[derive(Debug, Clone, PartialEq, Eq)]
167pub struct ManagedGenerationSnapshot {
168 pub profile: ManagedProfileState,
169 pub generations: BTreeMap<ManagedGenerationId, ManagedGenerationRecord>,
170}
171
172impl ManagedGenerationSnapshot {
173 pub fn empty(profile_id: ModelStoreProfileId) -> Self {
174 Self {
175 profile: ManagedProfileState {
176 profile_id,
177 startup_epoch: StartupEpoch::ZERO,
178 current_generation_id: None,
179 previous_generation_id: None,
180 state_revision: 0,
181 },
182 generations: BTreeMap::new(),
183 }
184 }
185
186 pub fn validate(&self) -> Result<(), GenerationTransitionError> {
187 if self.profile.current_generation_id == self.profile.previous_generation_id
188 && self.profile.current_generation_id.is_some()
189 {
190 return Err(GenerationTransitionError::EqualCurrentAndPrevious);
191 }
192 if self.profile.current_generation_id.is_none()
193 && self.profile.previous_generation_id.is_some()
194 {
195 return Err(GenerationTransitionError::PreviousWithoutCurrent);
196 }
197
198 let mut current_count = 0;
199 let mut previous_count = 0;
200 for (id, record) in &self.generations {
201 if id != &record.generation_id {
202 return Err(GenerationTransitionError::RecordIdentityMismatch);
203 }
204 if let Some(validated) = record.validated_startup_epoch {
205 let activation = record
206 .activation_epoch
207 .ok_or(GenerationTransitionError::InvalidEpochPair)?;
208 if validated <= activation {
209 return Err(GenerationTransitionError::InvalidEpochPair);
210 }
211 }
212 match record.state {
213 ManagedGenerationState::Current => {
214 current_count += 1;
215 if self.profile.current_generation_id.as_ref() != Some(id)
216 || record.activation_epoch.is_none()
217 {
218 return Err(GenerationTransitionError::PointerStateMismatch);
219 }
220 }
221 ManagedGenerationState::Previous => {
222 previous_count += 1;
223 if self.profile.previous_generation_id.as_ref() != Some(id)
224 || record.activation_epoch.is_none()
225 {
226 return Err(GenerationTransitionError::PointerStateMismatch);
227 }
228 }
229 ManagedGenerationState::Inactive | ManagedGenerationState::Invalid => {
230 if self.profile.current_generation_id.as_ref() == Some(id)
231 || self.profile.previous_generation_id.as_ref() == Some(id)
232 {
233 return Err(GenerationTransitionError::PointerStateMismatch);
234 }
235 }
236 }
237 }
238 if current_count != usize::from(self.profile.current_generation_id.is_some())
239 || previous_count != usize::from(self.profile.previous_generation_id.is_some())
240 {
241 return Err(GenerationTransitionError::PointerStateMismatch);
242 }
243 Ok(())
244 }
245
246 pub fn register_inactive(
247 &self,
248 generation_id: ManagedGenerationId,
249 manifest_id: impl Into<String>,
250 ) -> Result<Self, GenerationTransitionError> {
251 if self.generations.contains_key(&generation_id) {
252 return Err(GenerationTransitionError::GenerationAlreadyExists);
253 }
254 let manifest_id = manifest_id.into();
255 if manifest_id.is_empty() {
256 return Err(GenerationTransitionError::EmptyManifestId);
257 }
258 let mut next = self.clone();
259 next.generations.insert(
260 generation_id.clone(),
261 ManagedGenerationRecord {
262 generation_id,
263 manifest_id,
264 state: ManagedGenerationState::Inactive,
265 activation_epoch: None,
266 validated_startup_epoch: None,
267 },
268 );
269 next.bump_revision()?;
270 next.validate()?;
271 Ok(next)
272 }
273
274 pub fn advance_startup_epoch(&self) -> Result<Self, GenerationTransitionError> {
275 let mut next = self.clone();
276 next.profile.startup_epoch = next.profile.startup_epoch.next()?;
277 next.bump_revision()?;
278 next.validate()?;
279 Ok(next)
280 }
281
282 pub fn activate(
283 &self,
284 generation_id: &ManagedGenerationId,
285 ) -> Result<Self, GenerationTransitionError> {
286 self.validate()?;
287 let candidate = self
288 .generations
289 .get(generation_id)
290 .ok_or(GenerationTransitionError::GenerationNotFound)?;
291 if candidate.state != ManagedGenerationState::Inactive {
292 return Err(GenerationTransitionError::GenerationNotInactive);
293 }
294 if let Some(current_id) = &self.profile.current_generation_id {
295 let current = self
296 .generations
297 .get(current_id)
298 .ok_or(GenerationTransitionError::PointerStateMismatch)?;
299 if current.validated_startup_epoch.is_none() {
300 return Err(GenerationTransitionError::CurrentNotStartupValidated);
301 }
302 }
303
304 let mut next = self.clone();
305 if let Some(old_previous) = next.profile.previous_generation_id.take() {
306 next.generations
307 .get_mut(&old_previous)
308 .ok_or(GenerationTransitionError::PointerStateMismatch)?
309 .state = ManagedGenerationState::Inactive;
310 }
311 if let Some(old_current) = next
312 .profile
313 .current_generation_id
314 .replace(generation_id.clone())
315 {
316 next.generations
317 .get_mut(&old_current)
318 .ok_or(GenerationTransitionError::PointerStateMismatch)?
319 .state = ManagedGenerationState::Previous;
320 next.profile.previous_generation_id = Some(old_current);
321 }
322 let new_current = next
323 .generations
324 .get_mut(generation_id)
325 .ok_or(GenerationTransitionError::GenerationNotFound)?;
326 new_current.state = ManagedGenerationState::Current;
327 new_current.activation_epoch = Some(next.profile.startup_epoch);
328 new_current.validated_startup_epoch = None;
329 next.bump_revision()?;
330 next.validate()?;
331 Ok(next)
332 }
333
334 pub fn observe_current_for_startup_validation(
335 &self,
336 loaded_generation_id: &ManagedGenerationId,
337 ) -> Result<StartupValidationEvidence, GenerationTransitionError> {
338 self.validate()?;
339 if self.profile.current_generation_id.as_ref() != Some(loaded_generation_id) {
340 return Err(GenerationTransitionError::StaleValidationResult);
341 }
342 let current = self
343 .generations
344 .get(loaded_generation_id)
345 .ok_or(GenerationTransitionError::PointerStateMismatch)?;
346 let activation_epoch = current
347 .activation_epoch
348 .ok_or(GenerationTransitionError::InvalidEpochPair)?;
349 Ok(StartupValidationEvidence {
350 generation_id: loaded_generation_id.clone(),
351 activation_epoch,
352 })
353 }
354
355 pub fn validate_current_after_startup(
356 &self,
357 evidence: &StartupValidationEvidence,
358 ) -> Result<Self, GenerationTransitionError> {
359 self.validate()?;
360 if self.profile.current_generation_id.as_ref() != Some(&evidence.generation_id) {
361 return Err(GenerationTransitionError::StaleValidationResult);
362 }
363 let mut next = self.clone();
364 let current = next
365 .generations
366 .get_mut(&evidence.generation_id)
367 .ok_or(GenerationTransitionError::PointerStateMismatch)?;
368 let activation = current
369 .activation_epoch
370 .ok_or(GenerationTransitionError::InvalidEpochPair)?;
371 if activation != evidence.activation_epoch {
372 return Err(GenerationTransitionError::StaleValidationResult);
373 }
374 if next.profile.startup_epoch <= activation {
375 return Err(GenerationTransitionError::ValidationNotFromLaterStartup);
376 }
377 current.validated_startup_epoch = Some(next.profile.startup_epoch);
378 next.bump_revision()?;
379 next.validate()?;
380 Ok(next)
381 }
382
383 pub fn rollback_invalid_current(
384 &self,
385 previous_verified: bool,
386 ) -> Result<Self, GenerationTransitionError> {
387 self.validate()?;
388 let current_id = self
389 .profile
390 .current_generation_id
391 .as_ref()
392 .ok_or(GenerationTransitionError::NoCurrentGeneration)?
393 .clone();
394 let mut next = self.clone();
395 next.generations
396 .get_mut(¤t_id)
397 .ok_or(GenerationTransitionError::PointerStateMismatch)?
398 .state = ManagedGenerationState::Invalid;
399 next.profile.current_generation_id = None;
400
401 if let Some(previous_id) = next.profile.previous_generation_id.take() {
402 let previous = next
403 .generations
404 .get_mut(&previous_id)
405 .ok_or(GenerationTransitionError::PointerStateMismatch)?;
406 if previous_verified {
407 previous.state = ManagedGenerationState::Current;
408 next.profile.current_generation_id = Some(previous_id);
409 } else {
410 previous.state = ManagedGenerationState::Invalid;
411 }
412 }
413 next.bump_revision()?;
414 next.validate()?;
415 Ok(next)
416 }
417
418 pub fn release_previous_after_startup_validation(
421 &self,
422 ) -> Result<Self, GenerationTransitionError> {
423 self.validate()?;
424 let current_id = self
425 .profile
426 .current_generation_id
427 .as_ref()
428 .ok_or(GenerationTransitionError::NoCurrentGeneration)?;
429 let current = self
430 .generations
431 .get(current_id)
432 .ok_or(GenerationTransitionError::PointerStateMismatch)?;
433 if current.validated_startup_epoch.is_none() {
434 return Err(GenerationTransitionError::CurrentNotStartupValidated);
435 }
436 let previous_id = self
437 .profile
438 .previous_generation_id
439 .as_ref()
440 .ok_or(GenerationTransitionError::NoPreviousGeneration)?
441 .clone();
442
443 let mut next = self.clone();
444 next.profile.previous_generation_id = None;
445 next.generations
446 .get_mut(&previous_id)
447 .ok_or(GenerationTransitionError::PointerStateMismatch)?
448 .state = ManagedGenerationState::Inactive;
449 next.bump_revision()?;
450 next.validate()?;
451 Ok(next)
452 }
453
454 fn bump_revision(&mut self) -> Result<(), GenerationTransitionError> {
455 self.profile.state_revision = self
456 .profile
457 .state_revision
458 .checked_add(1)
459 .ok_or(GenerationTransitionError::RevisionExhausted)?;
460 if self.profile.state_revision > i64::MAX as u64 {
461 return Err(GenerationTransitionError::RevisionExhausted);
462 }
463 Ok(())
464 }
465}
466
467#[derive(Debug, Clone, Copy, PartialEq, Eq)]
468pub enum GenerationValueError {
469 InvalidGenerationId,
470 InvalidProfileId,
471 InvalidLifecycleState,
472 EpochOutOfRange,
473}
474
475impl fmt::Display for GenerationValueError {
476 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
477 write!(formatter, "invalid managed-generation value: {self:?}")
478 }
479}
480
481impl std::error::Error for GenerationValueError {}
482
483#[derive(Debug, Clone, Copy, PartialEq, Eq)]
484pub enum GenerationTransitionError {
485 EqualCurrentAndPrevious,
486 PreviousWithoutCurrent,
487 RecordIdentityMismatch,
488 InvalidEpochPair,
489 PointerStateMismatch,
490 GenerationAlreadyExists,
491 GenerationNotFound,
492 GenerationNotInactive,
493 CurrentNotStartupValidated,
494 StaleValidationResult,
495 ValidationNotFromLaterStartup,
496 NoCurrentGeneration,
497 NoPreviousGeneration,
498 EmptyManifestId,
499 EpochExhausted,
500 RevisionExhausted,
501}
502
503impl fmt::Display for GenerationTransitionError {
504 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
505 write!(formatter, "invalid managed-generation transition: {self:?}")
506 }
507}
508
509impl std::error::Error for GenerationTransitionError {}
510
511#[cfg(test)]
512mod tests {
513 use super::*;
514
515 fn new_id() -> ManagedGenerationId {
516 ManagedGenerationId::generate()
517 }
518
519 fn validate_after_restart(
520 state: ManagedGenerationSnapshot,
521 generation_id: &ManagedGenerationId,
522 ) -> ManagedGenerationSnapshot {
523 let later = state.advance_startup_epoch().unwrap();
524 let evidence = later
525 .observe_current_for_startup_validation(generation_id)
526 .unwrap();
527 later.validate_current_after_startup(&evidence).unwrap()
528 }
529
530 #[test]
531 fn identifiers_are_path_safe_and_strict() {
532 let generated = ManagedGenerationId::generate();
533 assert_eq!(
534 ManagedGenerationId::parse(generated.as_str()),
535 Ok(generated)
536 );
537 assert!(ManagedGenerationId::parse("gen_../../escape").is_err());
538 assert!(ManagedGenerationId::parse("gen_550e8400-e29b-41d4-a716-446655440000").is_err());
539 assert!(ModelStoreProfileId::parse("default-embedding").is_ok());
540 assert!(ModelStoreProfileId::parse("../profile").is_err());
541 }
542
543 #[test]
544 fn second_activation_requires_later_startup_validation() {
545 let a = new_id();
546 let b = new_id();
547 let state = ManagedGenerationSnapshot::empty(ModelStoreProfileId::default_embedding())
548 .register_inactive(a.clone(), "manifest")
549 .unwrap()
550 .register_inactive(b.clone(), "manifest")
551 .unwrap()
552 .activate(&a)
553 .unwrap();
554 assert_eq!(
555 state.activate(&b),
556 Err(GenerationTransitionError::CurrentNotStartupValidated)
557 );
558 let state = validate_after_restart(state, &a).activate(&b).unwrap();
559 assert_eq!(state.profile.current_generation_id, Some(b));
560 assert_eq!(state.profile.previous_generation_id, Some(a));
561 }
562
563 #[test]
564 fn stale_or_same_startup_validation_is_rejected() {
565 let a = new_id();
566 let other = new_id();
567 let state = ManagedGenerationSnapshot::empty(ModelStoreProfileId::default_embedding())
568 .register_inactive(a.clone(), "manifest")
569 .unwrap()
570 .activate(&a)
571 .unwrap();
572 let evidence = state.observe_current_for_startup_validation(&a).unwrap();
573 assert_eq!(
574 state.validate_current_after_startup(&evidence),
575 Err(GenerationTransitionError::ValidationNotFromLaterStartup)
576 );
577 let later = state.advance_startup_epoch().unwrap();
578 assert_eq!(
579 later.observe_current_for_startup_validation(&other),
580 Err(GenerationTransitionError::StaleValidationResult)
581 );
582 }
583
584 #[test]
585 fn stale_same_generation_activation_evidence_is_rejected() {
586 let a = new_id();
587 let b = new_id();
588 let c = new_id();
589 let active_a = ManagedGenerationSnapshot::empty(ModelStoreProfileId::default_embedding())
590 .register_inactive(a.clone(), "manifest")
591 .unwrap()
592 .register_inactive(b.clone(), "manifest")
593 .unwrap()
594 .register_inactive(c.clone(), "manifest")
595 .unwrap()
596 .activate(&a)
597 .unwrap();
598 let stale_evidence = active_a.observe_current_for_startup_validation(&a).unwrap();
599 let active_b = validate_after_restart(active_a, &a).activate(&b).unwrap();
600 let active_c = validate_after_restart(active_b, &b).activate(&c).unwrap();
601 let active_a_again = validate_after_restart(active_c, &c).activate(&a).unwrap();
602 let later = active_a_again.advance_startup_epoch().unwrap();
603
604 assert_eq!(
605 later.validate_current_after_startup(&stale_evidence),
606 Err(GenerationTransitionError::StaleValidationResult)
607 );
608 }
609
610 #[test]
611 fn rollback_never_keeps_invalid_current_as_previous() {
612 let a = new_id();
613 let b = new_id();
614 let active_a = ManagedGenerationSnapshot::empty(ModelStoreProfileId::default_embedding())
615 .register_inactive(a.clone(), "manifest")
616 .unwrap()
617 .register_inactive(b.clone(), "manifest")
618 .unwrap()
619 .activate(&a)
620 .unwrap();
621 let active_b = validate_after_restart(active_a, &a).activate(&b).unwrap();
622 let rolled_back = active_b.rollback_invalid_current(true).unwrap();
623 assert_eq!(rolled_back.profile.current_generation_id, Some(a.clone()));
624 assert_eq!(rolled_back.profile.previous_generation_id, None);
625 assert_eq!(
626 rolled_back.generations[&b].state,
627 ManagedGenerationState::Invalid
628 );
629 assert_eq!(
630 rolled_back.generations[&a].state,
631 ManagedGenerationState::Current
632 );
633 }
634
635 #[test]
636 fn rollback_with_two_invalid_generations_clears_both_pointers() {
637 let a = new_id();
638 let b = new_id();
639 let active_a = ManagedGenerationSnapshot::empty(ModelStoreProfileId::default_embedding())
640 .register_inactive(a.clone(), "manifest")
641 .unwrap()
642 .register_inactive(b.clone(), "manifest")
643 .unwrap()
644 .activate(&a)
645 .unwrap();
646 let active_b = validate_after_restart(active_a, &a).activate(&b).unwrap();
647 let rolled_back = active_b.rollback_invalid_current(false).unwrap();
648 assert_eq!(rolled_back.profile.current_generation_id, None);
649 assert_eq!(rolled_back.profile.previous_generation_id, None);
650 assert_eq!(
651 rolled_back.generations[&a].state,
652 ManagedGenerationState::Invalid
653 );
654 assert_eq!(
655 rolled_back.generations[&b].state,
656 ManagedGenerationState::Invalid
657 );
658 }
659
660 #[test]
661 fn previous_is_released_only_after_current_survives_later_startup() {
662 let a = new_id();
663 let b = new_id();
664 let active_a = ManagedGenerationSnapshot::empty(ModelStoreProfileId::default_embedding())
665 .register_inactive(a.clone(), "manifest")
666 .unwrap()
667 .register_inactive(b.clone(), "manifest")
668 .unwrap()
669 .activate(&a)
670 .unwrap();
671 let active_b = validate_after_restart(active_a, &a).activate(&b).unwrap();
672 assert_eq!(
673 active_b.release_previous_after_startup_validation(),
674 Err(GenerationTransitionError::CurrentNotStartupValidated)
675 );
676
677 let validated_b = validate_after_restart(active_b, &b);
678 let released = validated_b
679 .release_previous_after_startup_validation()
680 .unwrap();
681 assert_eq!(released.profile.current_generation_id, Some(b));
682 assert_eq!(released.profile.previous_generation_id, None);
683 assert_eq!(
684 released.generations[&a].state,
685 ManagedGenerationState::Inactive
686 );
687 }
688}