1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
//! Error types for the shared write path.
use notedthat_core::{Error as CoreError, StorageError};
/// What a write had already done to storage when a later step failed.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WriteEffect {
/// The bytes are stored under the key.
Stored,
/// The key is gone.
Deleted,
}
impl std::fmt::Display for WriteEffect {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Stored => "stored",
Self::Deleted => "deleted",
})
}
}
/// Errors returned by shared write operations.
#[derive(Debug, thiserror::Error)]
pub enum WriteError {
/// Storage-layer failure.
#[error(transparent)]
Storage(StorageError),
/// Upload size exceeded the configured limit.
#[error("payload too large: {size} bytes (limit {limit})")]
TooLarge {
/// Actual byte size.
size: u64,
/// Maximum allowed byte size.
limit: u64,
},
/// Path/domain validation failure.
#[error(transparent)]
Path(CoreError),
/// Indexer queue was full while enqueueing an upsert.
#[error("indexer queue full during upsert")]
IndexerBackpressureUpsert,
/// Indexer queue was full while enqueueing a tombstone.
#[error("indexer queue full during tombstone")]
IndexerBackpressureTombstone,
/// The event log refused the change after storage had already taken it.
///
/// Mirrors the indexer backpressure variants: the caller answers 503 with
/// `Retry-After` and a retried write publishes the event (D38, D55).
#[error("change event not published after the object was {after}")]
EventPublishFailed {
/// What storage had already done by the time publishing failed.
after: WriteEffect,
},
/// Object body exceeds the `NOTEDTHAT_MAX_PATCHABLE_SIZE` limit before or after splice.
#[error("patch payload too large: {size} bytes (limit {limit})")]
PatchTooLarge {
/// Actual byte size.
size: u64,
/// Maximum allowed byte size.
limit: u64,
},
/// Requested line range is beyond the end of the object at server-side splice time.
#[error("line range {first}..{last} out of range (total {total_lines} lines)")]
PatchLineOutOfRange {
/// Requested first line.
first: u64,
/// Requested last line.
last: u64,
/// Total line count in the object.
total_lines: u64,
/// Total byte count in the object (for X-Content-Range-Bytes).
total_bytes: u64,
},
/// Invalid range, mode contradiction, or missing If-Match for PATCH.
#[error("invalid patch request: {message}")]
PatchInvalidRange {
/// Human-readable reason.
message: String,
},
/// A body written to `.notedthat/manifest.json` is not the manifest startup
/// would accept: not a manifest document, naming another knowledge base, or
/// outside the limits `KbManifest::validate` enforces. The message is the
/// one the refused boot would have printed.
#[error("invalid manifest: {message}")]
InvalidManifest {
/// Human-readable reason, as startup would report it.
message: String,
},
/// Replace operation found no occurrence of the requested old string.
#[error("replace: no match found for old_string")]
ReplaceNoMatch,
/// Replace operation found multiple occurrences, making single replace ambiguous.
#[error("replace: found {count} matches; use replace_all to replace them all")]
ReplaceAmbiguous {
/// Number of matches found in the object body.
count: u64,
},
}
impl From<StorageError> for WriteError {
fn from(err: StorageError) -> Self {
Self::Storage(err)
}
}
#[cfg(test)]
mod tests {
use super::WriteError;
#[test]
fn patch_too_large_displays_size_limit_when_constructed() {
let err = WriteError::PatchTooLarge {
size: 200 * 1024 * 1024,
limit: 100 * 1024 * 1024,
};
assert!(err.to_string().contains("too large"));
}
#[test]
fn patch_line_out_of_range_constructs_with_line_and_byte_totals() {
let err = WriteError::PatchLineOutOfRange {
first: 999,
last: 1000,
total_lines: 20,
total_bytes: 100,
};
assert!(err.to_string().contains("out of range"));
}
#[test]
fn patch_invalid_range_displays_reason_when_constructed() {
let err = WriteError::PatchInvalidRange {
message: "test".into(),
};
assert!(err.to_string().contains("invalid patch"));
}
#[test]
fn replace_ambiguous_displays_match_count_when_constructed() {
let err = WriteError::ReplaceAmbiguous { count: 3 };
assert_eq!(
err.to_string(),
"replace: found 3 matches; use replace_all to replace them all"
);
}
}