1use std::fmt;
4
5use riffdb_proto::v1;
6use riffdb_types::{
7 BackupNameV1, ContractMigrationOperationId, MigrationBundleHash, OfflineMaintenanceOperationId,
8 OfflineMaintenanceReplacementConfirmation, RequestId,
9};
10
11const MAX_CANDIDATE_BUNDLE_BYTES: usize = 15 * 1_024 * 1_024;
12const MAX_MIGRATION_BUNDLE_BYTES: usize = 16 * 1_024 * 1_024;
13
14#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16pub enum ContractMigrationSubmissionError {
17 InvalidCandidateBundleSize,
19 InvalidMigrationBundleSize,
21}
22
23impl fmt::Display for ContractMigrationSubmissionError {
24 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
25 formatter.write_str(match self {
26 Self::InvalidCandidateBundleSize => "candidate bundle size is outside public limits",
27 Self::InvalidMigrationBundleSize => "migration bundle size is outside public limits",
28 })
29 }
30}
31
32impl std::error::Error for ContractMigrationSubmissionError {}
33
34#[derive(Clone)]
35struct ContractMigrationArtifacts {
36 candidate_bundle: Vec<u8>,
37 migration_bundle: Vec<u8>,
38}
39
40impl ContractMigrationArtifacts {
41 fn new(
42 candidate_bundle: Vec<u8>,
43 migration_bundle: Vec<u8>,
44 ) -> Result<Self, ContractMigrationSubmissionError> {
45 if candidate_bundle.is_empty() || candidate_bundle.len() > MAX_CANDIDATE_BUNDLE_BYTES {
46 return Err(ContractMigrationSubmissionError::InvalidCandidateBundleSize);
47 }
48 if migration_bundle.is_empty() || migration_bundle.len() > MAX_MIGRATION_BUNDLE_BYTES {
49 return Err(ContractMigrationSubmissionError::InvalidMigrationBundleSize);
50 }
51 Ok(Self {
52 candidate_bundle,
53 migration_bundle,
54 })
55 }
56}
57
58#[derive(Clone)]
60pub struct CheckContractMigration {
61 operation_id: ContractMigrationOperationId,
62 artifacts: ContractMigrationArtifacts,
63}
64
65impl CheckContractMigration {
66 pub fn new(
68 operation_id: ContractMigrationOperationId,
69 candidate_bundle: Vec<u8>,
70 migration_bundle: Vec<u8>,
71 ) -> Result<Self, ContractMigrationSubmissionError> {
72 Ok(Self {
73 operation_id,
74 artifacts: ContractMigrationArtifacts::new(candidate_bundle, migration_bundle)?,
75 })
76 }
77
78 #[must_use]
80 pub const fn operation_id(&self) -> ContractMigrationOperationId {
81 self.operation_id
82 }
83
84 pub(crate) fn request(&self, request_id: RequestId) -> v1::CheckContractMigrationRequest {
85 v1::CheckContractMigrationRequest {
86 request_id: request_id.into_bytes().to_vec(),
87 operation_id: self.operation_id.into_bytes().to_vec(),
88 candidate_bundle: self.artifacts.candidate_bundle.clone(),
89 migration_bundle: self.artifacts.migration_bundle.clone(),
90 }
91 }
92}
93
94impl fmt::Debug for CheckContractMigration {
95 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
96 formatter.write_str("CheckContractMigration([REDACTED_ARTIFACTS])")
97 }
98}
99
100#[derive(Clone)]
102pub struct ApplyContractMigration {
103 operation_id: ContractMigrationOperationId,
104 artifacts: ContractMigrationArtifacts,
105 confirmed_migration_hash: MigrationBundleHash,
106}
107
108impl ApplyContractMigration {
109 pub fn new(
111 operation_id: ContractMigrationOperationId,
112 candidate_bundle: Vec<u8>,
113 migration_bundle: Vec<u8>,
114 confirmed_migration_hash: MigrationBundleHash,
115 ) -> Result<Self, ContractMigrationSubmissionError> {
116 Ok(Self {
117 operation_id,
118 artifacts: ContractMigrationArtifacts::new(candidate_bundle, migration_bundle)?,
119 confirmed_migration_hash,
120 })
121 }
122
123 #[must_use]
125 pub const fn operation_id(&self) -> ContractMigrationOperationId {
126 self.operation_id
127 }
128
129 #[must_use]
131 pub const fn confirmed_migration_hash(&self) -> MigrationBundleHash {
132 self.confirmed_migration_hash
133 }
134
135 pub(crate) fn request(&self, request_id: RequestId) -> v1::ApplyContractMigrationRequest {
136 v1::ApplyContractMigrationRequest {
137 request_id: request_id.into_bytes().to_vec(),
138 operation_id: self.operation_id.into_bytes().to_vec(),
139 candidate_bundle: self.artifacts.candidate_bundle.clone(),
140 migration_bundle: self.artifacts.migration_bundle.clone(),
141 confirmation: v1::ContractMigrationApplyConfirmation::AllowApplyContractMigration
142 as i32,
143 confirmed_migration_hash: self.confirmed_migration_hash.into_bytes().to_vec(),
144 }
145 }
146}
147
148impl fmt::Debug for ApplyContractMigration {
149 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
150 formatter.write_str("ApplyContractMigration([REDACTED_ARTIFACTS])")
151 }
152}
153
154pub struct CreateOfflineBackup {
156 operation_id: OfflineMaintenanceOperationId,
157 backup_name: BackupNameV1,
158}
159
160impl CreateOfflineBackup {
161 #[must_use]
163 pub const fn new(
164 operation_id: OfflineMaintenanceOperationId,
165 backup_name: BackupNameV1,
166 ) -> Self {
167 Self {
168 operation_id,
169 backup_name,
170 }
171 }
172
173 #[must_use]
175 pub const fn operation_id(&self) -> OfflineMaintenanceOperationId {
176 self.operation_id
177 }
178
179 #[must_use]
181 pub const fn backup_name(&self) -> &BackupNameV1 {
182 &self.backup_name
183 }
184
185 pub(crate) fn request(&self, request_id: RequestId) -> v1::CreateOfflineBackupRequest {
186 v1::CreateOfflineBackupRequest {
187 request_id: request_id.into_bytes().to_vec(),
188 operation_id: self.operation_id.into_bytes().to_vec(),
189 backup_name: self.backup_name.as_str().to_owned(),
190 }
191 }
192}
193
194impl fmt::Debug for CreateOfflineBackup {
195 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
196 formatter
197 .debug_struct("CreateOfflineBackup")
198 .field("operation_id", &self.operation_id)
199 .field("backup_name", &self.backup_name)
200 .finish()
201 }
202}
203
204pub struct RetireOfflineBackup {
206 operation_id: OfflineMaintenanceOperationId,
207 backup_name: BackupNameV1,
208}
209
210impl RetireOfflineBackup {
211 #[must_use]
213 pub const fn new(
214 operation_id: OfflineMaintenanceOperationId,
215 backup_name: BackupNameV1,
216 ) -> Self {
217 Self {
218 operation_id,
219 backup_name,
220 }
221 }
222
223 #[must_use]
225 pub const fn operation_id(&self) -> OfflineMaintenanceOperationId {
226 self.operation_id
227 }
228
229 #[must_use]
231 pub const fn backup_name(&self) -> &BackupNameV1 {
232 &self.backup_name
233 }
234
235 pub(crate) fn request(&self, request_id: RequestId) -> v1::RetireOfflineBackupRequest {
236 v1::RetireOfflineBackupRequest {
237 request_id: request_id.into_bytes().to_vec(),
238 operation_id: self.operation_id.into_bytes().to_vec(),
239 backup_name: self.backup_name.as_str().to_owned(),
240 }
241 }
242}
243
244impl fmt::Debug for RetireOfflineBackup {
245 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
246 formatter
247 .debug_struct("RetireOfflineBackup")
248 .field("operation_id", &self.operation_id)
249 .field("backup_name", &self.backup_name)
250 .finish()
251 }
252}
253
254pub struct StartOfflineStorageScrub {
257 operation_id: OfflineMaintenanceOperationId,
258}
259
260impl StartOfflineStorageScrub {
261 #[must_use]
263 pub const fn new(operation_id: OfflineMaintenanceOperationId) -> Self {
264 Self { operation_id }
265 }
266
267 #[must_use]
269 pub const fn operation_id(&self) -> OfflineMaintenanceOperationId {
270 self.operation_id
271 }
272
273 pub(crate) fn request(&self, request_id: RequestId) -> v1::StartOfflineStorageScrubRequest {
274 v1::StartOfflineStorageScrubRequest {
275 request_id: request_id.into_bytes().to_vec(),
276 operation_id: self.operation_id.into_bytes().to_vec(),
277 }
278 }
279}
280
281impl fmt::Debug for StartOfflineStorageScrub {
282 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
283 formatter
284 .debug_struct("StartOfflineStorageScrub")
285 .field("operation_id", &self.operation_id)
286 .finish()
287 }
288}
289
290pub struct RestoreOfflineBackup {
292 operation_id: OfflineMaintenanceOperationId,
293 backup_name: BackupNameV1,
294 confirmation: OfflineMaintenanceReplacementConfirmation,
295}
296
297impl RestoreOfflineBackup {
298 #[must_use]
300 pub const fn new(
301 operation_id: OfflineMaintenanceOperationId,
302 backup_name: BackupNameV1,
303 confirmation: OfflineMaintenanceReplacementConfirmation,
304 ) -> Self {
305 Self {
306 operation_id,
307 backup_name,
308 confirmation,
309 }
310 }
311
312 #[must_use]
314 pub const fn operation_id(&self) -> OfflineMaintenanceOperationId {
315 self.operation_id
316 }
317
318 #[must_use]
320 pub const fn backup_name(&self) -> &BackupNameV1 {
321 &self.backup_name
322 }
323
324 #[must_use]
326 pub const fn confirmation(&self) -> OfflineMaintenanceReplacementConfirmation {
327 self.confirmation
328 }
329
330 pub(crate) fn request(&self, request_id: RequestId) -> v1::RestoreOfflineBackupRequest {
331 let replacement_confirmation = match self.confirmation {
332 OfflineMaintenanceReplacementConfirmation::NotProvided => {
333 v1::OfflineMaintenanceReplacementConfirmation::Unspecified
334 }
335 OfflineMaintenanceReplacementConfirmation::AllowReplaceNonemptyTarget => {
336 v1::OfflineMaintenanceReplacementConfirmation::AllowReplaceNonemptyTarget
337 }
338 };
339 v1::RestoreOfflineBackupRequest {
340 request_id: request_id.into_bytes().to_vec(),
341 operation_id: self.operation_id.into_bytes().to_vec(),
342 backup_name: self.backup_name.as_str().to_owned(),
343 replacement_confirmation: replacement_confirmation as i32,
344 }
345 }
346}
347
348impl fmt::Debug for RestoreOfflineBackup {
349 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
350 formatter
351 .debug_struct("RestoreOfflineBackup")
352 .field("operation_id", &self.operation_id)
353 .field("backup_name", &self.backup_name)
354 .field("confirmation", &self.confirmation)
355 .finish()
356 }
357}
358
359pub struct RestoreArchivedBackup {
361 operation_id: OfflineMaintenanceOperationId,
362 backup_name: BackupNameV1,
363 archive_name: riffdb_types::ArchiveNameV1,
364 stop: riffdb_types::ArchiveRestoreStopV1,
365 confirmation: OfflineMaintenanceReplacementConfirmation,
366}
367
368impl RestoreArchivedBackup {
369 #[must_use]
371 pub const fn new(
372 operation_id: OfflineMaintenanceOperationId,
373 backup_name: BackupNameV1,
374 archive_name: riffdb_types::ArchiveNameV1,
375 stop: riffdb_types::ArchiveRestoreStopV1,
376 confirmation: OfflineMaintenanceReplacementConfirmation,
377 ) -> Self {
378 Self {
379 operation_id,
380 backup_name,
381 archive_name,
382 stop,
383 confirmation,
384 }
385 }
386
387 #[must_use]
389 pub const fn operation_id(&self) -> OfflineMaintenanceOperationId {
390 self.operation_id
391 }
392
393 #[must_use]
395 pub const fn backup_name(&self) -> &BackupNameV1 {
396 &self.backup_name
397 }
398
399 #[must_use]
401 pub const fn archive_name(&self) -> &riffdb_types::ArchiveNameV1 {
402 &self.archive_name
403 }
404
405 #[must_use]
407 pub const fn stop(&self) -> riffdb_types::ArchiveRestoreStopV1 {
408 self.stop
409 }
410
411 #[must_use]
413 pub const fn confirmation(&self) -> OfflineMaintenanceReplacementConfirmation {
414 self.confirmation
415 }
416
417 pub(crate) fn request(&self, request_id: RequestId) -> v1::RestoreArchivedBackupRequest {
418 v1::RestoreArchivedBackupRequest {
419 request_id: request_id.into_bytes().to_vec(),
420 operation_id: self.operation_id.into_bytes().to_vec(),
421 backup_name: self.backup_name.as_str().to_owned(),
422 archive_name: self.archive_name.as_str().to_owned(),
423 stop_at_sequence: match self.stop {
424 riffdb_types::ArchiveRestoreStopV1::LastArchived => None,
425 riffdb_types::ArchiveRestoreStopV1::AtApplicationSequence(sequence) => {
426 Some(sequence.get())
427 }
428 },
429 replacement_confirmation: match self.confirmation {
430 OfflineMaintenanceReplacementConfirmation::NotProvided => {
431 v1::OfflineMaintenanceReplacementConfirmation::Unspecified as i32
432 }
433 OfflineMaintenanceReplacementConfirmation::AllowReplaceNonemptyTarget => {
434 v1::OfflineMaintenanceReplacementConfirmation::AllowReplaceNonemptyTarget as i32
435 }
436 },
437 }
438 }
439}
440
441impl fmt::Debug for RestoreArchivedBackup {
442 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
443 formatter.write_str("RestoreArchivedBackup([REDACTED])")
444 }
445}
446
447#[cfg(test)]
448mod tests {
449 use super::*;
450
451 fn request_id(timestamp: u64) -> RequestId {
452 RequestId::from_unix_milliseconds_and_random(timestamp, [1; 10]).expect("request ID")
453 }
454
455 fn operation_id() -> OfflineMaintenanceOperationId {
456 OfflineMaintenanceOperationId::from_unix_milliseconds_and_random(1, [2; 10])
457 .expect("operation ID")
458 }
459
460 #[test]
461 fn archive_restore_retry_keeps_every_semantic_input_and_redacts_debug() {
463 let restore = RestoreArchivedBackup::new(
464 operation_id(),
465 BackupNameV1::new("backup").unwrap(),
466 riffdb_types::ArchiveNameV1::new("private-archive").unwrap(),
467 riffdb_types::ArchiveRestoreStopV1::AtApplicationSequence(
468 riffdb_types::CommitSequence::new(7).unwrap(),
469 ),
470 OfflineMaintenanceReplacementConfirmation::AllowReplaceNonemptyTarget,
471 );
472 let first = restore.request(request_id(1));
473 let mut second = restore.request(request_id(2));
474 assert_ne!(first.request_id, second.request_id);
475 second.request_id = first.request_id.clone();
476 assert_eq!(first, second);
477 assert_eq!(first.stop_at_sequence, Some(7));
478 assert!(!format!("{restore:?}").contains("private-archive"));
479 let last = RestoreArchivedBackup::new(
480 operation_id(),
481 BackupNameV1::new("backup").unwrap(),
482 riffdb_types::ArchiveNameV1::new("private-archive").unwrap(),
483 riffdb_types::ArchiveRestoreStopV1::LastArchived,
484 OfflineMaintenanceReplacementConfirmation::NotProvided,
485 );
486 assert_eq!(last.request(request_id(3)).stop_at_sequence, None);
487 }
488
489 fn migration_operation_id() -> ContractMigrationOperationId {
490 ContractMigrationOperationId::from_unix_milliseconds_and_random(1, [3; 10])
491 .expect("operation ID")
492 }
493
494 #[test]
495 fn retries_change_only_the_transport_request_id() {
496 let create =
497 CreateOfflineBackup::new(operation_id(), BackupNameV1::new("stable").expect("name"));
498 let first = create.request(request_id(2));
499 let second = create.request(request_id(3));
500 assert_ne!(first.request_id, second.request_id);
501 assert_eq!(first.operation_id, second.operation_id);
502 assert_eq!(first.backup_name, second.backup_name);
503
504 let retire =
505 RetireOfflineBackup::new(operation_id(), BackupNameV1::new("stable").expect("name"));
506 let first = retire.request(request_id(6));
507 let second = retire.request(request_id(7));
508 assert_ne!(first.request_id, second.request_id);
509 assert_eq!(first.operation_id, second.operation_id);
510 assert_eq!(first.backup_name, second.backup_name);
511
512 let restore = RestoreOfflineBackup::new(
513 operation_id(),
514 BackupNameV1::new("stable").expect("name"),
515 OfflineMaintenanceReplacementConfirmation::AllowReplaceNonemptyTarget,
516 );
517 let first = restore.request(request_id(4));
518 let second = restore.request(request_id(5));
519 assert_ne!(first.request_id, second.request_id);
520 assert_eq!(first.operation_id, second.operation_id);
521 assert_eq!(first.backup_name, second.backup_name);
522 assert_eq!(
523 first.replacement_confirmation,
524 second.replacement_confirmation
525 );
526 }
527
528 #[test]
529 fn migration_retries_change_only_transport_identity() {
530 let check =
531 CheckContractMigration::new(migration_operation_id(), vec![1], vec![2]).expect("check");
532 let first = check.request(request_id(2));
533 let second = check.request(request_id(3));
534 assert_ne!(first.request_id, second.request_id);
535 assert_eq!(first.operation_id, second.operation_id);
536 assert_eq!(first.candidate_bundle, second.candidate_bundle);
537 assert_eq!(first.migration_bundle, second.migration_bundle);
538
539 let apply = ApplyContractMigration::new(
540 migration_operation_id(),
541 vec![1],
542 vec![2],
543 MigrationBundleHash::from_bytes([9; 32]),
544 )
545 .expect("apply");
546 let first = apply.request(request_id(4));
547 let second = apply.request(request_id(5));
548 assert_ne!(first.request_id, second.request_id);
549 assert_eq!(first.operation_id, second.operation_id);
550 assert_eq!(first.candidate_bundle, second.candidate_bundle);
551 assert_eq!(first.migration_bundle, second.migration_bundle);
552 assert_eq!(
553 first.confirmed_migration_hash,
554 second.confirmed_migration_hash
555 );
556 }
557}