Skip to main content

lattice_embed/migration/
types.rs

1//! Defines migration plans, states, progress snapshots, skip reasons, and errors.
2//!
3//! These serializable values describe a model-version transition; the controller supplies
4//! the lifecycle and accounting rules that give their fields meaning.
5//!
6//! See [docs/migration.md](../../docs/migration.md) for state, format, and coverage details.
7
8use serde::{Deserialize, Serialize};
9
10use crate::model::EmbeddingModel;
11
12/// Reason why an embedding was skipped during migration.
13#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
14#[serde(rename_all = "snake_case")]
15#[non_exhaustive]
16pub enum SkipReason {
17    /// Content exceeds maximum size for embedding.
18    ContentTooLarge {
19        /// Actual content size in bytes.
20        size: usize,
21        /// Maximum allowed size in bytes.
22        max: usize,
23    },
24    /// Content encoding is invalid or unsupported.
25    InvalidEncoding(String),
26    /// Content was deleted during migration.
27    ContentDeleted,
28    /// Embedding API returned a permanent (non-retryable) error.
29    PermanentApiError(String),
30    /// Manually skipped by operator.
31    ManualSkip(String),
32}
33
34impl std::fmt::Display for SkipReason {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        match self {
37            SkipReason::ContentTooLarge { size, max } => {
38                write!(f, "content too large: {size} bytes (max {max})")
39            }
40            SkipReason::InvalidEncoding(enc) => write!(f, "invalid encoding: {enc}"),
41            SkipReason::ContentDeleted => write!(f, "content deleted"),
42            SkipReason::PermanentApiError(msg) => write!(f, "permanent API error: {msg}"),
43            SkipReason::ManualSkip(reason) => write!(f, "manually skipped: {reason}"),
44        }
45    }
46}
47
48/// Migration state machine states.
49#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
50#[serde(rename_all = "snake_case")]
51#[non_exhaustive]
52pub enum MigrationState {
53    /// Migration is planned but has not started.
54    // FP-036: alias allows deserializing data stored before rename_all = "snake_case" was applied.
55    #[serde(alias = "Planned")]
56    Planned,
57    /// Migration is actively processing embeddings.
58    #[serde(alias = "InProgress")]
59    InProgress {
60        /// Number of embeddings processed so far.
61        processed: usize,
62        /// Total number of embeddings to process.
63        total: usize,
64        /// Number of embeddings skipped.
65        #[serde(default)]
66        skipped: usize,
67    },
68    /// Migration is paused (can be resumed).
69    #[serde(alias = "Paused")]
70    Paused {
71        /// Number of embeddings processed before pause.
72        processed: usize,
73        /// Total number of embeddings to process.
74        total: usize,
75        /// Number of embeddings skipped.
76        #[serde(default)]
77        skipped: usize,
78        /// Reason the migration was paused.
79        reason: String,
80    },
81    /// Migration completed successfully.
82    #[serde(alias = "Completed")]
83    Completed {
84        /// Total number of embeddings processed.
85        processed: usize,
86        /// Number of embeddings skipped.
87        #[serde(default)]
88        skipped: usize,
89        /// Wall-clock duration in seconds.
90        duration_secs: f64,
91    },
92    /// Migration failed with an error.
93    #[serde(alias = "Failed")]
94    Failed {
95        /// Number of embeddings processed before failure.
96        processed: usize,
97        /// Total number of embeddings to process.
98        total: usize,
99        /// Number of embeddings skipped.
100        #[serde(default)]
101        skipped: usize,
102        /// Error message describing the failure.
103        error: String,
104    },
105    /// Migration was cancelled by the operator.
106    #[serde(alias = "Cancelled")]
107    Cancelled {
108        /// Number of embeddings processed before cancellation.
109        processed: usize,
110        /// Total number of embeddings to process.
111        total: usize,
112        /// Number of embeddings skipped.
113        #[serde(default)]
114        skipped: usize,
115    },
116}
117
118impl MigrationState {
119    /// Returns `true` if the migration can be resumed (paused or failed).
120    #[inline]
121    pub fn is_resumable(&self) -> bool {
122        matches!(
123            self,
124            MigrationState::Paused { .. } | MigrationState::Failed { .. }
125        )
126    }
127
128    /// Returns `true` if the migration has reached a terminal state (completed or cancelled).
129    #[inline]
130    pub fn is_terminal(&self) -> bool {
131        matches!(
132            self,
133            MigrationState::Completed { .. } | MigrationState::Cancelled { .. }
134        )
135    }
136
137    /// Returns `true` if the migration is currently in progress.
138    #[inline]
139    pub fn is_active(&self) -> bool {
140        matches!(self, MigrationState::InProgress { .. })
141    }
142
143    /// Returns raw processed-to-total progress; a zero total returns 1.0.
144    pub fn progress(&self) -> Option<f64> {
145        match self {
146            MigrationState::Planned => Some(0.0),
147            MigrationState::InProgress {
148                processed, total, ..
149            }
150            | MigrationState::Paused {
151                processed, total, ..
152            }
153            | MigrationState::Failed {
154                processed, total, ..
155            }
156            | MigrationState::Cancelled {
157                processed, total, ..
158            } => {
159                if *total == 0 {
160                    Some(1.0)
161                } else {
162                    Some(*processed as f64 / *total as f64)
163                }
164            }
165            MigrationState::Completed { .. } => Some(1.0),
166        }
167    }
168
169    /// Returns the number of embeddings processed so far.
170    pub fn processed(&self) -> usize {
171        match self {
172            MigrationState::Planned => 0,
173            MigrationState::InProgress { processed, .. }
174            | MigrationState::Paused { processed, .. }
175            | MigrationState::Failed { processed, .. }
176            | MigrationState::Cancelled { processed, .. }
177            | MigrationState::Completed { processed, .. } => *processed,
178        }
179    }
180
181    /// Returns the number of embeddings skipped.
182    pub fn skipped(&self) -> usize {
183        match self {
184            MigrationState::Planned => 0,
185            MigrationState::InProgress { skipped, .. }
186            | MigrationState::Paused { skipped, .. }
187            | MigrationState::Failed { skipped, .. }
188            | MigrationState::Cancelled { skipped, .. }
189            | MigrationState::Completed { skipped, .. } => *skipped,
190        }
191    }
192
193    /// Returns the total number of embeddings to process.
194    pub fn total(&self) -> usize {
195        match self {
196            MigrationState::Planned | MigrationState::Completed { .. } => 0,
197            MigrationState::InProgress { total, .. }
198            | MigrationState::Paused { total, .. }
199            | MigrationState::Failed { total, .. }
200            | MigrationState::Cancelled { total, .. } => *total,
201        }
202    }
203
204    /// Returns total minus skipped -- the number of embeddings that actually need processing.
205    pub fn effective_total(&self) -> usize {
206        self.total().saturating_sub(self.skipped())
207    }
208
209    /// Returns processed-to-effective-total coverage; a zero effective total returns 1.0.
210    pub fn effective_coverage(&self) -> f64 {
211        let eff = self.effective_total();
212        if eff == 0 {
213            1.0
214        } else {
215            self.processed() as f64 / eff as f64
216        }
217    }
218}
219
220/// Describes an embedding migration operation.
221///
222/// See [docs/migration.md](../../docs/migration.md) for plan fields and transition scope.
223#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct MigrationPlan {
225    /// Unique migration identifier.
226    pub id: String,
227    /// Model to migrate from.
228    pub source_model: EmbeddingModel,
229    /// Model to migrate to.
230    pub target_model: EmbeddingModel,
231    /// Total number of embeddings to migrate.
232    pub total_embeddings: usize,
233    /// Number of embeddings processed per batch.
234    pub batch_size: usize,
235    /// ISO 8601 timestamp when the plan was created.
236    pub created_at: String,
237}
238
239/// Progress report for an active migration.
240#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct MigrationProgress {
242    /// Identifier of the migration this progress belongs to.
243    pub migration_id: String,
244    /// Current state of the migration.
245    pub state: MigrationState,
246    /// Number of embeddings skipped so far.
247    #[serde(default)]
248    pub skipped: usize,
249    /// Total minus skipped -- the number of embeddings that actually need processing.
250    #[serde(default)]
251    pub effective_total: usize,
252    /// Processed-to-effective-total coverage.
253    #[serde(default)]
254    pub effective_coverage: f64,
255    /// Embeddings processed per second.
256    pub throughput: f64,
257    /// Estimated seconds remaining, if calculable.
258    pub eta_secs: Option<f64>,
259    /// Number of errors encountered during processing.
260    pub error_count: usize,
261}
262
263/// Errors from migration operations.
264#[derive(Debug, Clone)]
265#[non_exhaustive]
266pub enum MigrationError {
267    /// Attempted an invalid state transition.
268    InvalidTransition {
269        /// State being transitioned from.
270        from: String,
271        /// State being transitioned to.
272        to: String,
273    },
274}
275
276impl std::fmt::Display for MigrationError {
277    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
278        match self {
279            MigrationError::InvalidTransition { from, to } => {
280                write!(f, "invalid migration transition from {from} to {to}")
281            }
282        }
283    }
284}
285
286impl std::error::Error for MigrationError {}