1use super::pending_encoding as encoding;
4use crate::{OperationId, OperationKind, Refusal};
5use coven_database::PendingValue;
6use coven_format::{admin::SnapshotId, path::ObjectPath};
7use coven_foundation::id_source::{DeviceId, FileId, KeyId, MemberId, ObjectId};
8use coven_merge::{Audience, Slot, WriteId};
9use coven_storage::{CloudProvider, RetainedAccess, StorageFailure};
10use serde::{Deserialize, Serialize};
11
12pub use coven_format::RequiredUpdate;
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
16pub enum FileSourceFailure {
17 Missing,
19 Changed,
21 Integrity,
23}
24
25#[derive(Clone, Debug, Serialize, Deserialize)]
27pub struct PendingRecord {
28 pub subject: PendingSubject,
30 pub reason: PendingReason,
32 #[serde(skip)]
40 pub cause: Option<String>,
41}
42
43impl PartialEq for PendingRecord {
44 fn eq(&self, other: &Self) -> bool {
45 self.subject == other.subject && self.reason == other.reason
46 }
47}
48impl Eq for PendingRecord {}
49
50impl std::fmt::Display for PendingRecord {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 write!(f, "{:?}: {:?}", self.subject, self.reason)?;
55 match &self.cause {
56 Some(cause) => write!(f, ": {cause}"),
57 None => Ok(()),
58 }
59 }
60}
61
62#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
64pub enum PendingSubject {
65 Write(#[serde(with = "encoding::write")] WriteId),
67 Slot(#[serde(with = "coven_database::slot_json")] Slot),
69 KeyCopy {
71 #[serde(with = "crate::pending_encoding::audience")]
73 audience: Audience,
74 key: KeyId,
76 member: MemberId,
78 #[serde(with = "encoding::sealing")]
80 sealing: coven_crypto::SealingKey,
81 },
82 Snapshot(#[serde(with = "crate::pending_encoding::snapshot_id")] SnapshotId),
84 Upload {
87 device: DeviceId,
89 object: ObjectId,
91 },
92 Download {
95 device: DeviceId,
97 object: ObjectId,
99 },
100 Keep {
104 file: FileId,
106 },
107 Move {
110 id: coven_database::MoveId,
112 },
113 Operation {
115 id: OperationId,
117 kind: OperationKind,
119 },
120 Retention {
122 path: ObjectPath,
124 },
125 Audience(#[serde(with = "crate::pending_encoding::audience")] Audience),
127 Connection,
129}
130
131#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
133pub enum Prerequisite {
134 SequenceCatchUp,
136 Reload(#[serde(with = "crate::pending_encoding::audience")] Audience),
138 Raise {
142 #[serde(with = "crate::pending_encoding::audience")]
144 audience: Audience,
145 version: u32,
148 },
149}
150
151#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
153pub enum LocalFailure {
154 Database,
156 DiskFull,
159 DiskDenied,
162 DiskOther,
166 KeyCustody,
168 Crypto,
170 Operation,
172 Shutdown,
175 Bug,
179}
180
181impl LocalFailure {
182 pub(crate) fn of_disk(error: &(dyn std::error::Error + 'static)) -> Self {
186 let mut current = Some(error);
187 while let Some(cause) = current {
188 if let Some(os) = cause.downcast_ref::<std::io::Error>() {
189 return match os.kind() {
190 std::io::ErrorKind::StorageFull | std::io::ErrorKind::QuotaExceeded => {
191 Self::DiskFull
192 }
193 std::io::ErrorKind::PermissionDenied
194 | std::io::ErrorKind::ReadOnlyFilesystem => Self::DiskDenied,
195 _ => Self::DiskOther,
196 };
197 }
198 current = cause.source();
199 }
200 Self::DiskOther
201 }
202}
203
204#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
206pub enum ProviderAccessAction {
207 Grant,
209 Revoke,
211}
212
213#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
215pub enum FileMissingReason {
216 Lost,
220 Source(FileSourceFailure),
224}
225
226#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
228pub enum PendingReason {
229 Missing {
231 path: ObjectPath,
233 },
234 Waits(Prerequisite),
236 KeyUnavailable {
238 #[serde(with = "crate::pending_encoding::audience")]
240 audience: Audience,
241 key: KeyId,
243 },
244 UpdateRequired(RequiredUpdate),
246 NoStorage,
248 Storage {
250 provider: CloudProvider,
252 failure: StorageFailure,
254 answered: bool,
258 },
259 Refused(Refusal),
261 WritesRefused {
263 #[serde(with = "coven_database::slot_json")]
265 cutoff: Slot,
266 cause: coven_database::WriteCutoff,
268 },
269 ProviderPending {
271 action: ProviderAccessAction,
273 account: String,
275 },
276 AccountInUse {
278 account: String,
280 },
281 AccessRemains {
283 account: String,
285 shares: Vec<RetainedAccess>,
287 },
288 FileUnavailable(FileMissingReason),
290 Original {
293 #[serde(with = "crate::pending_encoding::path")]
295 path: std::path::PathBuf,
296 failure: FileSourceFailure,
298 },
299 Failed(LocalFailure),
301 Rejected(crate::OperationRejection),
303 Cleanup {
305 operation: Box<PendingReason>,
307 cleanup: Box<PendingReason>,
309 },
310 Paused,
312 Removed,
314 NotAdmitted,
316 MemberKeysReplaced {
319 member: MemberId,
321 },
322}
323
324#[derive(Clone, Copy, Debug, PartialEq, Eq)]
326pub enum Retry {
327 Automatic,
329 AfterUpdate,
332 AppAction,
334 Never,
336}
337impl PendingRecord {
338 pub fn new(subject: PendingSubject, reason: PendingReason) -> Self {
340 Self { subject, reason, cause: None }
341 }
342
343 pub fn retry(&self) -> Retry {
345 match (&self.subject, self.reason.retry_cause()) {
346 (
347 PendingSubject::Upload { .. },
348 PendingReason::FileUnavailable(FileMissingReason::Source(_)),
349 ) => Retry::Automatic,
350 (PendingSubject::Download { .. }, reason) => match reason.clears() {
354 Some(Clears::Never) => Retry::Never,
355 Some(Clears::ByItself | Clears::ByPerson) => Retry::AppAction,
356 None => match reason.retry() {
357 Retry::Automatic => Retry::AppAction,
358 retry => retry,
359 },
360 },
361 (subject, reason)
362 if matches!(
363 subject,
364 PendingSubject::Upload { .. } | PendingSubject::Keep { .. }
365 ) || matches!(subject, PendingSubject::Operation { kind, .. } if kind.coven_owned()) =>
366 {
367 match reason.clears() {
368 Some(Clears::ByItself) => Retry::Automatic,
369 Some(Clears::ByPerson) => Retry::AppAction,
370 Some(Clears::Never) => Retry::Never,
371 None => reason.retry(),
372 }
373 }
374 (subject, reason @ PendingReason::Failed(_))
379 if !matches!(subject, PendingSubject::Operation { .. })
380 && reason.clears() == Some(Clears::ByItself) =>
381 {
382 Retry::Automatic
383 }
384 (_, reason) => reason.retry(),
385 }
386 }
387}
388
389#[derive(Clone, Copy, Debug, PartialEq, Eq)]
391pub(crate) enum Clears {
392 ByItself,
394 ByPerson,
396 Never,
398}
399
400pub(crate) fn offline(failure: StorageFailure, answered: bool) -> bool {
404 failure == StorageFailure::Network && !answered
405}
406
407pub(crate) fn automatic_storage(failure: StorageFailure) -> bool {
410 matches!(
411 failure,
412 StorageFailure::Network
413 | StorageFailure::Corrupted
414 | StorageFailure::Conflict
415 | StorageFailure::RateLimited
416 | StorageFailure::NotFound
417 | StorageFailure::SessionExpired
418 )
419}
420
421#[deny(clippy::wildcard_enum_match_arm)]
424pub(crate) fn storage_clears(failure: StorageFailure) -> Clears {
425 use StorageFailure::*;
426 match failure {
427 Network | Corrupted | Conflict | RateLimited | NotFound | SessionExpired
428 | CatchUpRequired | QuotaExceeded | File | Bug => Clears::ByItself,
429 Authentication
430 | PermissionDenied
431 | AccessNotRegistered { .. }
432 | AccessNotGranted { .. }
433 | ContainerNotFound
434 | RegionMismatch
435 | ClockSkew
436 | InvalidConfiguration
437 | NotStoreOwner
438 | AccountIdUnavailable
439 | SharingRefused(_)
440 | ProviderUnavailable
441 | MemberKeysMissing => Clears::ByPerson,
442 DropboxAppFolderNotShareable
443 | AlreadyExists
444 | StoreExists
445 | OwnLocationRequired
446 | Refused
447 | Protocol
448 | InvalidPath
449 | InvalidRange
450 | InviteeIsStoreOwner
451 | InvitationMismatch
452 | SessionMismatch
453 | SessionIdentityUnknown
454 | InvalidPart
455 | SingleRequestTooLarge { .. }
456 | CopyTooLarge { .. }
457 | Encoding => Clears::Never,
458 }
459}
460
461impl PendingReason {
462 pub(crate) fn retry_cause(&self) -> &Self {
464 match self {
465 Self::Cleanup { cleanup, .. } => cleanup.retry_cause(),
466 reason => reason,
467 }
468 }
469
470 #[deny(clippy::wildcard_enum_match_arm)]
473 pub(crate) fn clears(&self) -> Option<Clears> {
474 match self.retry_cause() {
475 Self::Failed(
476 LocalFailure::Database
477 | LocalFailure::DiskFull
478 | LocalFailure::DiskDenied
479 | LocalFailure::DiskOther
480 | LocalFailure::KeyCustody
481 | LocalFailure::Shutdown
482 | LocalFailure::Bug,
483 ) => Some(Clears::ByItself),
484 Self::Failed(LocalFailure::Crypto | LocalFailure::Operation) => Some(Clears::Never),
485 Self::Storage { failure, .. } => Some(storage_clears(*failure)),
486 Self::Missing { .. }
487 | Self::Waits(_)
488 | Self::KeyUnavailable { .. }
489 | Self::UpdateRequired(_)
490 | Self::NoStorage
491 | Self::Refused(_)
492 | Self::WritesRefused { .. }
493 | Self::ProviderPending { .. }
494 | Self::AccountInUse { .. }
495 | Self::AccessRemains { .. }
496 | Self::FileUnavailable(_)
497 | Self::Original { .. }
498 | Self::Rejected(_)
499 | Self::Cleanup { .. }
500 | Self::Paused
501 | Self::Removed
502 | Self::NotAdmitted
503 | Self::MemberKeysReplaced { .. } => None,
504 }
505 }
506
507 pub(crate) fn waits(&self) -> bool {
511 match self {
512 Self::Missing { .. }
513 | Self::Waits(_)
514 | Self::KeyUnavailable { .. }
515 | Self::NoStorage
516 | Self::ProviderPending { .. }
517 | Self::AccountInUse { .. }
518 | Self::Paused => true,
519 Self::Original { failure, .. } => *failure == FileSourceFailure::Missing,
520 Self::UpdateRequired(_)
521 | Self::Storage { .. }
522 | Self::Refused(_)
523 | Self::WritesRefused { .. }
524 | Self::AccessRemains { .. }
525 | Self::FileUnavailable(_)
526 | Self::Failed(_)
527 | Self::Rejected(_)
528 | Self::Removed
529 | Self::NotAdmitted
530 | Self::MemberKeysReplaced { .. } => false,
531 Self::Cleanup { operation, cleanup } => operation.waits() && cleanup.waits(),
532 }
533 }
534
535 pub(crate) fn offline(&self) -> bool {
538 matches!(self, Self::Storage { failure, answered, .. } if offline(*failure, *answered))
539 }
540
541 #[deny(clippy::wildcard_enum_match_arm)]
542 fn retry(&self) -> Retry {
543 match self {
544 Self::Missing { .. }
545 | Self::Waits(_)
546 | Self::KeyUnavailable { .. }
547 | Self::ProviderPending { .. }
548 | Self::AccountInUse { .. } => Retry::Automatic,
549 Self::Original { failure: FileSourceFailure::Missing, .. } => Retry::Automatic,
553 Self::Original {
554 failure: FileSourceFailure::Changed | FileSourceFailure::Integrity,
555 ..
556 } => Retry::AppAction,
557 Self::Storage { failure, .. } if automatic_storage(*failure) => Retry::Automatic,
558 Self::Refused(_) | Self::UpdateRequired(_) => Retry::AfterUpdate,
559 Self::Rejected(_)
560 | Self::WritesRefused { .. }
561 | Self::Removed
562 | Self::NotAdmitted
563 | Self::MemberKeysReplaced { .. } => Retry::Never,
564 Self::NoStorage
565 | Self::Storage { .. }
566 | Self::AccessRemains { .. }
567 | Self::FileUnavailable(_)
568 | Self::Failed(_)
569 | Self::Cleanup { .. }
570 | Self::Paused => Retry::AppAction,
571 }
572 }
573}
574
575impl coven_database::types::FromSql for PendingRecord {
578 fn column_result(
579 value: coven_database::types::ValueRef<'_>,
580 ) -> coven_database::types::FromSqlResult<Self> {
581 serde_json::from_slice(value.as_blob()?)
582 .map_err(|error| coven_database::types::FromSqlError::Other(Box::new(error)))
583 }
584}
585
586impl PendingValue for PendingRecord {
587 fn provider_access(id: OperationId, account: String, grant: bool) -> Self {
588 Self::new(
589 PendingSubject::Operation { id, kind: OperationKind::RevokeAccess },
590 PendingReason::ProviderPending {
591 action: if grant {
592 ProviderAccessAction::Grant
593 } else {
594 ProviderAccessAction::Revoke
595 },
596 account,
597 },
598 )
599 }
600 fn reload(audience: Audience) -> Self {
601 Self::new(
602 PendingSubject::Audience(audience.clone()),
603 PendingReason::Waits(Prerequisite::Reload(audience)),
604 )
605 }
606 fn writes_refused(slot: Slot, cutoff: Slot, cause: coven_database::WriteCutoff) -> Self {
607 Self::new(PendingSubject::Slot(slot), PendingReason::WritesRefused { cutoff, cause })
608 }
609 fn raise_wait(write: coven_merge::WriteId, audience: Audience, version: u32) -> Self {
610 Self::new(
611 PendingSubject::Write(write),
612 PendingReason::Waits(Prerequisite::Raise { audience, version }),
613 )
614 }
615 fn raise_awaited(&self) -> Option<(coven_merge::WriteId, &Audience, u32)> {
616 match (&self.subject, &self.reason) {
617 (
618 PendingSubject::Write(write),
619 PendingReason::Waits(Prerequisite::Raise { audience, version }),
620 ) => Some((*write, audience, *version)),
621 _ => None,
622 }
623 }
624 type Subject = PendingSubject;
625 fn subject(&self) -> &PendingSubject {
626 &self.subject
627 }
628 fn operation(&self) -> Option<OperationId> {
629 match self.subject {
630 PendingSubject::Operation { id, .. } => Some(id),
631 _ => None,
632 }
633 }
634 fn move_of(&self) -> Option<coven_database::MoveId> {
635 match self.subject {
636 PendingSubject::Move { id } => Some(id),
637 _ => None,
638 }
639 }
640 fn pin_of(&self) -> Option<FileId> {
641 match self.subject {
642 PendingSubject::Keep { file } => Some(file),
643 _ => None,
644 }
645 }
646 fn object(&self) -> Option<(coven_database::FileWork, DeviceId, ObjectId)> {
647 match self.subject {
648 PendingSubject::Upload { device, object } => {
649 Some((coven_database::FileWork::Upload, device, object))
650 }
651 PendingSubject::Download { device, object } => {
652 Some((coven_database::FileWork::Download, device, object))
653 }
654 _ => None,
655 }
656 }
657 fn slot(&self) -> Option<Slot> {
658 match self.subject {
659 PendingSubject::Slot(slot) => Some(slot),
660 _ => None,
661 }
662 }
663 fn ends_when_covered(&self) -> bool {
664 !matches!(
665 self.reason.retry_cause(),
666 PendingReason::Refused(_) | PendingReason::WritesRefused { .. }
667 )
668 }
669 const BUILD: u128 = const_random::const_random!(u128);
677 fn can_retry(&self, recorded_build: u128, schema_version: u32) -> bool {
681 match self.reason.retry_cause() {
682 PendingReason::Refused(_) => recorded_build != Self::BUILD,
683 PendingReason::UpdateRequired(RequiredUpdate::AppSchema { version }) => {
684 schema_version >= *version
685 }
686 PendingReason::UpdateRequired(RequiredUpdate::CovenFormat { version }) => {
687 coven_format::FORMAT_VERSION >= *version
688 }
689 _ => self.retry() == Retry::Automatic,
690 }
691 }
692 fn cause(&self) -> Option<&str> {
693 self.cause.as_deref()
694 }
695 fn with_cause(self, cause: Option<String>) -> Self {
696 Self { cause, ..self }
697 }
698}
699
700#[cfg(test)]
701#[path = "pending_tests.rs"]
702mod tests;