1use crate::internal_schema::WRITE_DEVICE_SQL;
4use crate::{
5 sqlite::DatabaseConnection,
6 write_encoding::{counter, damaged},
7 DbError, OperationId,
8};
9use coven_format::path::ObjectPath;
10use coven_foundation::id_source::DeviceId;
11use rusqlite::params;
12use std::io::Read;
13
14#[derive(Clone, Debug, PartialEq, Eq)]
16pub enum ClaimWork {
17 Writes(u64),
19 Operation(OperationId),
21}
22
23pub struct ClaimAttempt<R> {
25 pub path: ObjectPath,
27 pub bytes: R,
29}
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub enum ClaimSettlement {
34 Unknown,
36 Retry,
38 NotLanded,
40 Landed,
42 DeviceIdInUse,
44}
45
46#[derive(Clone, Debug, PartialEq, Eq)]
48pub struct Claim {
49 pub device: DeviceId,
51 pub number: u64,
53 pub work: ClaimWork,
55 pub attempts: Vec<ObjectPath>,
57 pub settlement: ClaimSettlement,
59}
60
61pub enum ClaimRead {
63 Created,
66 Kept(Option<Box<dyn Read + Send>>),
68 Compacted {
70 first: u64,
72 covers: u64,
74 last_claim: u64,
76 digest: Option<[u8; 32]>,
78 },
79}
80
81fn settlement(text: String) -> rusqlite::Result<ClaimSettlement> {
82 match text.as_str() {
83 "unknown" => Ok(ClaimSettlement::Unknown),
84 "retry" => Ok(ClaimSettlement::Retry),
85 "not-landed" => Ok(ClaimSettlement::NotLanded),
86 "landed" => Ok(ClaimSettlement::Landed),
87 "device-id-in-use" => Ok(ClaimSettlement::DeviceIdInUse),
88 _ => Err(damaged(4, rusqlite::types::Type::Text)),
89 }
90}
91
92fn metadata(db: &DatabaseConnection) -> Result<Option<Claim>, DbError> {
93 db.query("SELECT device,number,through,operation,settlement FROM _coven_claim_journal WHERE singleton=1", [], |r| {
94 let work = match (r.get::<_,Option<Vec<u8>>>(2)?, r.get::<_,Option<i64>>(3)?) {
95 (Some(n),None) => ClaimWork::Writes(counter(n)?),
96 (None,Some(id)) => ClaimWork::Operation(OperationId(id)),
97 _ => return Err(damaged(2, rusqlite::types::Type::Blob)),
98 };
99 Ok(Claim { device:r.get::<_, DeviceId>(0)?, number:counter(r.get(1)?)?, work,
100 attempts:Vec::new(), settlement:settlement(r.get(4)?)? })
101 }).map(|mut rows| rows.pop())
102}
103
104pub(crate) fn current(db: &DatabaseConnection) -> Result<Option<Claim>, DbError> {
105 metadata(db)?.map(|mut claim| {
106 claim.attempts = db.query("SELECT path FROM _coven_claim_attempts WHERE device=?1 AND number=?2 ORDER BY attempt",
107 params![claim.device,claim.number.to_be_bytes().as_slice()], |r| {
108 let path:String=r.get(0)?;
109 ObjectPath::parse(&path).map_err(|e|crate::write_encoding::decode_failure(0,rusqlite::types::Type::Text,e))
110 })?;
111 Ok(claim)
112 }).transpose()
113}
114
115pub(crate) fn reserved(db: &DatabaseConnection, device: DeviceId) -> Result<u64, DbError> {
116 let previous: Option<Vec<u8>> = db.query_row(
117 "SELECT (SELECT number FROM _coven_claim_counters WHERE device=?1)",
118 [device],
119 |r| r.get(0),
120 )?;
121 Ok(previous.map(counter).transpose()?.unwrap_or(0))
122}
123
124pub(crate) fn begin<R: Read>(
125 db: &DatabaseConnection,
126 device: DeviceId,
127 work: ClaimWork,
128 number: u64,
129 prepared: crate::claim_preparation::PreparedClaim<R>,
130) -> Result<Claim, DbError> {
131 if metadata(db)?.is_some() {
132 return Err(DbError::ClaimPending);
133 }
134 if reserved(db, device)?.checked_add(1) != Some(number) {
136 return Err(DbError::ClaimPending);
137 }
138 let (through, operation) = match work {
139 ClaimWork::Writes(through) => (Some(through.to_be_bytes().to_vec()), None),
140 ClaimWork::Operation(operation) => (None, Some(operation.0)),
141 };
142 db.internal_execute("INSERT INTO _coven_claim_counters(device,number) VALUES(?1,?2) ON CONFLICT(device) DO UPDATE SET number=excluded.number",
143 params![device,number.to_be_bytes().as_slice()])?;
144 db.internal_execute("INSERT INTO _coven_claim_journal(device,number,through,operation,settlement,singleton) VALUES(?1,?2,?3,?4,'unknown',1)",
145 params![device,number.to_be_bytes().as_slice(),through,operation])?;
146 let path = prepared.attempt.path.clone();
147 append(db, device, number, prepared)?;
148 if let ClaimWork::Writes(through) = work {
149 db.internal_execute(
150 "INSERT INTO _coven_upload_claims(device,number,claim)
151 SELECT device,number,?1 FROM _coven_uploads WHERE device=?2 AND number<=?3
152 ON CONFLICT(device,number) DO UPDATE SET claim=excluded.claim",
153 params![number.to_be_bytes().as_slice(), device, through.to_be_bytes().as_slice()],
154 )?;
155 }
156 Ok(Claim { device, number, work, attempts: vec![path], settlement: ClaimSettlement::Unknown })
157}
158
159fn append(
160 db: &DatabaseConnection,
161 device: DeviceId,
162 number: u64,
163 prepared: crate::claim_preparation::PreparedClaim<impl Read>,
164) -> Result<(), DbError> {
165 let crate::claim_preparation::PreparedClaim { mut attempt, digest, size } = prepared;
166 let ordinal: i64 = db.query_row("SELECT coalesce(max(attempt),0)+1 FROM _coven_claim_attempts WHERE device=?1 AND number=?2",
167 params![device,number.to_be_bytes()], |r|r.get(0))?;
168 db.internal_execute(
169 "INSERT INTO _coven_claim_attempts(device,number,attempt,path) VALUES(?1,?2,?3,?4)",
170 params![device, number.to_be_bytes(), ordinal, attempt.path.as_str()],
171 )?;
172 for chunk in 0i64.. {
173 let bytes = read_chunk(&mut attempt.bytes)?;
174 if bytes.is_empty() {
175 break;
176 }
177 db.internal_execute("INSERT INTO _coven_claim_chunks(device,number,attempt,chunk,bytes) VALUES(?1,?2,?3,?4,?5)",
178 params![device,number.to_be_bytes(),ordinal,chunk,bytes])?;
179 }
180 db.internal_execute(
181 "UPDATE _coven_claim_attempts SET digest=?4,size=?5 WHERE device=?1 AND number=?2 AND attempt=?3",
182 params![
183 device,
184 number.to_be_bytes(),
185 ordinal,
186 digest.as_bytes(),
187 size.to_be_bytes()
188 ],
189 )?;
190 Ok(())
191}
192
193pub(crate) fn read_chunk(bytes: &mut dyn Read) -> Result<Vec<u8>, DbError> {
196 let mut chunk = Vec::new();
197 bytes
198 .take(coven_format::chunks::CHUNK_SIZE as u64)
199 .read_to_end(&mut chunk)
200 .map_err(DbError::ClaimRead)?;
201 Ok(chunk)
202}
203
204pub struct ClaimBytes<'a> {
206 db: &'a DatabaseConnection,
207 device: DeviceId,
208 number: u64,
209 attempt: i64,
210 chunk: i64,
211 ended: bool,
212}
213
214impl Iterator for ClaimBytes<'_> {
215 type Item = Result<Vec<u8>, DbError>;
216
217 fn next(&mut self) -> Option<Self::Item> {
218 if self.ended {
219 return None;
220 }
221 let bytes = self.db.query_row("SELECT (SELECT bytes FROM _coven_claim_chunks WHERE device=?1 AND number=?2 AND attempt=?3 AND chunk=?4)",
222 params![self.device,self.number.to_be_bytes(),self.attempt,self.chunk], |r|r.get::<_,Option<Vec<u8>>>(0));
223 match bytes {
224 Ok(Some(bytes)) => {
225 self.chunk += 1;
226 Some(Ok(bytes))
227 }
228 Ok(None) => {
229 self.ended = true;
230 None
231 }
232 Err(error) => {
233 self.ended = true;
234 Some(Err(error))
235 }
236 }
237 }
238}
239
240pub(crate) fn read_attempt<T, E: From<DbError>>(
241 db: &DatabaseConnection,
242 consume: impl FnOnce(ClaimBytes<'_>) -> Result<T, E>,
243) -> Result<T, E> {
244 let claim = metadata(db)?.ok_or(DbError::NoClaim)?;
245 let attempt = db.query_row(
246 "SELECT max(attempt) FROM _coven_claim_attempts WHERE device=?1 AND number=?2",
247 params![claim.device, claim.number.to_be_bytes()],
248 |r| r.get(0),
249 )?;
250 consume(ClaimBytes {
251 db,
252 device: claim.device,
253 number: claim.number,
254 attempt,
255 chunk: 0,
256 ended: false,
257 })
258}
259
260fn equal(db: &DatabaseConnection, mut received: Box<dyn Read + Send>) -> Result<bool, DbError> {
261 read_attempt(db, |mut saved| {
262 let mut equal = true;
263 loop {
264 let incoming = read_chunk(&mut received)?;
265 let stored = saved.next().transpose()?;
266 if incoming.is_empty() && stored.is_none() {
267 return Ok(equal);
268 }
269 equal &= stored.as_deref() == Some(incoming.as_slice());
270 }
271 })
272}
273
274pub(crate) fn retry(
275 db: &DatabaseConnection,
276 attempt: crate::claim_preparation::PreparedClaim<impl Read>,
277) -> Result<(), DbError> {
278 let claim = metadata(db)?.ok_or(DbError::NoClaim)?;
279 if claim.settlement != ClaimSettlement::NotLanded {
280 return Err(DbError::ClaimUnsettled);
281 }
282 append(db, claim.device, claim.number, attempt)?;
283 db.internal_execute(
284 "UPDATE _coven_claim_journal SET settlement='unknown' WHERE singleton=1",
285 [],
286 )?;
287 Ok(())
288}
289
290pub(crate) fn settle(db: &DatabaseConnection, read: ClaimRead) -> Result<ClaimSettlement, DbError> {
291 let claim = current(db)?.ok_or(DbError::NoClaim)?;
292 if matches!(
293 claim.settlement,
294 ClaimSettlement::Landed | ClaimSettlement::NotLanded | ClaimSettlement::DeviceIdInUse
295 ) {
296 return Ok(claim.settlement);
297 }
298 let attempt = claim.attempts.last().expect("a journal starts with its first attempt");
299 let (result, name) = match read {
300 ClaimRead::Created => (ClaimSettlement::Landed, "landed"),
301 ClaimRead::Kept(Some(bytes)) => {
302 if equal(db, bytes)? {
303 (ClaimSettlement::Landed, "landed")
304 } else {
305 (ClaimSettlement::NotLanded, "not-landed")
306 }
307 }
308 ClaimRead::Kept(None) => (ClaimSettlement::Retry, "retry"),
309 ClaimRead::Compacted { first, covers, last_claim, digest } => {
310 let slot = attempt.slot_number().ok_or(DbError::InvalidClaimCoverage)?;
311 if first <= slot.get() || covers < first - 1 {
312 return Err(DbError::InvalidClaimCoverage);
313 }
314 if last_claim < claim.number {
315 (ClaimSettlement::NotLanded, "not-landed")
316 } else if last_claim > claim.number {
317 (ClaimSettlement::DeviceIdInUse, "device-id-in-use")
318 } else {
319 let attempted_digest: Vec<u8> = db.query_row(
320 "SELECT digest FROM _coven_claim_attempts WHERE device=?1 AND number=?2 ORDER BY attempt DESC LIMIT 1",
321 params![claim.device, claim.number.to_be_bytes()],
322 |r| r.get(0),
323 )?;
324 if digest.as_ref().map(|value| value.as_slice())
325 == Some(attempted_digest.as_slice())
326 {
327 (ClaimSettlement::Landed, "landed")
328 } else {
329 (ClaimSettlement::DeviceIdInUse, "device-id-in-use")
330 }
331 }
332 }
333 };
334 db.internal_execute("UPDATE _coven_claim_journal SET settlement=?1 WHERE singleton=1", [name])?;
335 Ok(result)
336}
337
338pub(crate) fn landed_operation(
344 db: &DatabaseConnection,
345) -> Result<Option<(OperationId, coven_merge::Slot)>, DbError> {
346 let Some(claim) = metadata(db)? else {
347 return Ok(None);
348 };
349 let ClaimWork::Operation(id) = claim.work else {
350 return Ok(None);
351 };
352 let found = db.query(
353 "SELECT a.path FROM _coven_claim_attempts AS a
354 JOIN _coven_last_claims AS c ON c.device=a.device AND c.number=a.number
355 AND c.digest=a.digest
356 WHERE a.device=?1 AND a.number=?2
357 AND a.attempt=(SELECT max(attempt) FROM _coven_claim_attempts
358 WHERE device=?1 AND number=?2)",
359 params![claim.device, claim.number.to_be_bytes()],
360 |r| r.get::<_, String>(0),
361 )?;
362 let Some(path) = found.into_iter().next() else {
363 return Ok(None);
364 };
365 let path = ObjectPath::parse(&path)
366 .map_err(|e| crate::write_encoding::decode_failure(0, rusqlite::types::Type::Text, e))?;
367 Ok(Some((id, path.slot_number().ok_or(DbError::InvalidClaimCoverage)?)))
368}
369
370pub(crate) fn held_by(db: &DatabaseConnection, id: OperationId) -> Result<bool, DbError> {
372 Ok(metadata(db)?.is_some_and(|claim| claim.work == ClaimWork::Operation(id)))
373}
374
375pub(crate) fn drop_operation(db: &DatabaseConnection, id: OperationId) -> Result<(), DbError> {
378 if held_by(db, id)? {
379 db.internal_execute("DELETE FROM _coven_claim_journal WHERE singleton=1", [])?;
380 }
381 Ok(())
382}
383
384pub(crate) fn finish(db: &DatabaseConnection) -> Result<(), DbError> {
387 let claim = metadata(db)?.ok_or(DbError::NoClaim)?;
388 let mut writes_landed = false;
389 match claim.settlement {
390 ClaimSettlement::Unknown | ClaimSettlement::Retry => return Err(DbError::ClaimUnsettled),
391 ClaimSettlement::Landed if matches!(claim.work, ClaimWork::Writes(_)) => {
392 let unconsumed: bool = db.query_row(
395 &format!("SELECT EXISTS(SELECT 1 FROM _coven_upload_claims u JOIN _coven_writes w ON {WRITE_DEVICE_SQL}=u.device AND w.number=u.number WHERE u.device=?1 AND u.claim=?2 AND w.slot IS NULL)"),
396 params![claim.device, claim.number.to_be_bytes()],
397 |r| r.get(0),
398 )?;
399 if unconsumed {
400 return Err(DbError::ClaimUnsettled);
401 }
402 crate::upload::forget_claimed(db, claim.device, claim.number)?;
403 writes_landed = true;
404 }
405 _ => (),
406 }
407 db.internal_execute("DELETE FROM _coven_claim_journal WHERE singleton=1", [])?;
408 if writes_landed {
409 db.give_back_free_pages()?;
410 }
411 Ok(())
412}
413
414#[cfg(any(test, feature = "test-utils"))]
416pub(crate) fn attempted(db: &DatabaseConnection, path: &ObjectPath) -> Result<bool, DbError> {
417 db.query_row(
418 "SELECT EXISTS(SELECT 1 FROM _coven_claim_attempts WHERE path=?1)",
419 [path.as_str()],
420 |r| r.get(0),
421 )
422}
423
424pub(crate) fn attempted_digest(
425 db: &DatabaseConnection,
426 number: u64,
427 path: &ObjectPath,
428 digest: &[u8; 32],
429) -> Result<bool, DbError> {
430 db.query_row("SELECT EXISTS(SELECT 1 FROM _coven_claim_attempts WHERE number=?1 AND path=?2 AND digest=?3)",
431 params![number.to_be_bytes(), path.as_str(), digest], |r| r.get(0))
432}
433
434pub struct ClaimUpload {
436 pub size: u64,
438 pub session: Option<Vec<u8>>,
440}
441
442pub(crate) fn upload(db: &DatabaseConnection) -> Result<ClaimUpload, DbError> {
443 let claim = metadata(db)?.ok_or(DbError::NoClaim)?;
444 db.query_row("SELECT size,session FROM _coven_claim_attempts WHERE device=?1 AND number=?2 ORDER BY attempt DESC LIMIT 1",
445 params![claim.device, claim.number.to_be_bytes()], |row| Ok(ClaimUpload {size: counter(row.get(0)?)?, session: row.get(1)?}))
446}
447
448pub(crate) fn save_upload(db: &DatabaseConnection, session: Vec<u8>) -> Result<(), DbError> {
449 let claim = metadata(db)?.ok_or(DbError::NoClaim)?;
450 db.internal_execute("UPDATE _coven_claim_attempts SET session=?3 WHERE device=?1 AND number=?2 AND attempt=(SELECT max(attempt) FROM _coven_claim_attempts WHERE device=?1 AND number=?2)",
451 params![claim.device, claim.number.to_be_bytes(), session])?;
452 Ok(())
453}
454
455pub(crate) fn range(
456 db: &DatabaseConnection,
457 offset: u64,
458 length: usize,
459) -> Result<Vec<u8>, DbError> {
460 if length == 0 {
461 return Ok(Vec::new());
462 }
463 let claim = metadata(db)?.ok_or(DbError::NoClaim)?;
464 let chunk_size = coven_format::chunks::CHUNK_SIZE as u64;
465 let end = offset.checked_add(length as u64).ok_or(DbError::DamagedDatabase)?;
466 let pieces = db.query("SELECT chunk,bytes FROM _coven_claim_chunks WHERE device=?1 AND number=?2 AND attempt=(SELECT max(attempt) FROM _coven_claim_attempts WHERE device=?1 AND number=?2) AND chunk>=?3 AND chunk<=?4 ORDER BY chunk",
467 params![claim.device, claim.number.to_be_bytes(), (offset / chunk_size) as i64, ((end-1) / chunk_size) as i64],
468 |row| Ok((row.get::<_,i64>(0)? as u64, row.get::<_,Vec<u8>>(1)?)))?;
469 let mut bytes = Vec::with_capacity(length);
470 for (index, piece) in pieces {
471 let start = offset.saturating_sub(index * chunk_size) as usize;
472 let last = (end - index * chunk_size).min(piece.len() as u64) as usize;
473 bytes.extend_from_slice(piece.get(start..last).ok_or(DbError::DamagedDatabase)?);
474 }
475 if bytes.len() != length {
476 return Err(DbError::DamagedDatabase);
477 }
478 Ok(bytes)
479}
480
481#[cfg(test)]
482#[path = "claim_tests.rs"]
483pub(crate) mod tests;