1use crate::{
10 files::file_failure::upload_subject, files::FilesInner, files::TrailingHold,
11 retry_waits::Ending, NetworkOwner, PendingRecord, Retry, SyncError,
12};
13use coven_database::{DbError, FileUpload, RetrySubject};
14use coven_foundation::locks::{lock_unpoisoned, read_unpoisoned};
15use coven_storage::StorageFailure;
16use futures_util::{future::BoxFuture, stream::FuturesUnordered, StreamExt};
17use std::{
18 collections::{BTreeMap, BTreeSet, VecDeque},
19 sync::atomic::Ordering,
20 time::Duration,
21};
22use tokio::sync::oneshot;
23
24#[derive(Debug, PartialEq, Eq)]
27pub enum DrainOutcome {
28 Drained {
31 uploaded: usize,
35 },
36 QueueEmpty,
38 Paused,
41 Metered,
45 NotSyncing,
47}
48
49pub(super) struct AppRetry {
54 covers: Option<BTreeSet<i64>>,
57 uploaded: usize,
59 asked: u64,
62 reply: oneshot::Sender<Result<DrainOutcome, SyncError>>,
63}
64
65pub(super) struct UploadRetryAnswer(oneshot::Receiver<Result<DrainOutcome, SyncError>>);
68
69impl UploadRetryAnswer {
70 pub(super) async fn answered(self) -> Result<DrainOutcome, SyncError> {
72 self.0.await.unwrap_or_else(|_| Err(DbError::StoreClosed.into()))
73 }
74
75 #[cfg(test)]
77 pub(super) fn try_answered(&mut self) -> Option<Result<DrainOutcome, SyncError>> {
78 self.0.try_recv().ok()
79 }
80}
81
82#[derive(Debug)]
84pub(super) enum WorkerEnd {
85 Panicked(coven_foundation::task::Bug),
88 Closed,
90 Failed(crate::LocalFailure, std::sync::Arc<dyn std::error::Error + Send + Sync>),
93}
94
95impl WorkerEnd {
96 pub(super) fn of(error: SyncError) -> Self {
99 match error {
100 SyncError::Database(DbError::StoreClosed) => Self::Closed,
101 error => {
102 let (failure, source) = error.into_shared();
103 Self::Failed(failure, source)
104 }
105 }
106 }
107
108 fn error(&self) -> SyncError {
109 match self {
110 Self::Panicked(bug) => SyncError::WorkerFailed(bug.clone()),
111 Self::Closed => DbError::StoreClosed.into(),
112 Self::Failed(failure, source) => {
113 SyncError::Failed { failure: *failure, source: source.clone() }
114 }
115 }
116 }
117}
118
119#[derive(Clone, Copy, Debug, PartialEq, Eq)]
122pub(super) enum Lane {
123 Bound,
124 Trailing,
125}
126
127impl Lane {
128 pub(super) fn of(upload: &FileUpload) -> Self {
129 match upload.holds {
130 Some(_) => Self::Bound,
131 None => Self::Trailing,
132 }
133 }
134 pub(super) fn index(self) -> usize {
135 match self {
136 Self::Bound => 0,
137 Self::Trailing => 1,
138 }
139 }
140}
141
142#[derive(Clone, Copy, Debug)]
144enum Blocked {
145 Closed,
146 NotSyncing,
148 NoStorage,
149 Held(TrailingHold),
152 CatchUp,
154 Cooling(Duration),
156 Changing,
158}
159
160impl Blocked {
161 fn retry_answer(self) -> Option<Result<DrainOutcome, SyncError>> {
164 Some(match self {
165 Self::Closed => Err(DbError::StoreClosed.into()),
166 Self::NotSyncing => Ok(DrainOutcome::NotSyncing),
167 Self::NoStorage => Err(SyncError::NoStorage),
168 Self::Held(TrailingHold::Paused) => Ok(DrainOutcome::Paused),
169 Self::Held(TrailingHold::Metered) => Ok(DrainOutcome::Metered),
170 Self::Held(TrailingHold::MemberKeysChanging) => Err(DbError::MemberKeysChanging.into()),
172 Self::CatchUp => Err(StorageFailure::CatchUpRequired.into()),
173 Self::Cooling(remaining) => Err(StorageFailure::RateLimited
175 .with_source(coven_storage::CoolingDown { remaining })
176 .into()),
177 Self::Changing => return None,
178 })
179 }
180
181 fn run_outcome(self, uploaded: usize) -> Result<DrainOutcome, SyncError> {
185 match self {
186 Self::Cooling(_) | Self::Changing | Self::Held(TrailingHold::MemberKeysChanging) => {
187 Ok(DrainOutcome::Drained { uploaded })
188 }
189 blocked => blocked.retry_answer().unwrap_or(Ok(DrainOutcome::Drained { uploaded })),
190 }
191 }
192}
193
194type Attempt<'a> = BoxFuture<'a, (i64, Result<super::file_upload::Attempted, SyncError>)>;
195
196#[derive(Default)]
198struct Run<'a> {
199 attempts: FuturesUnordered<Attempt<'a>>,
201 started: BTreeMap<i64, u64>,
203 lanes: BTreeMap<i64, Lane>,
205 bound: BTreeSet<i64>,
208 committed: bool,
210 ready: VecDeque<FileUpload>,
213 stale: bool,
215 crossed: BTreeMap<RetrySubject, Ending>,
219 uploaded: usize,
221}
222
223impl Run<'_> {
224 fn in_flight(&self, lane: Lane) -> usize {
225 self.lanes.values().filter(|of| **of == lane).count()
226 }
227
228 fn used(&self, lane: Lane, owner: &FilesInner) -> usize {
231 self.in_flight(lane) + owner.connections_beside(lane)
232 }
233
234 fn room(&self, limit: usize, held: bool, owner: &FilesInner) -> bool {
238 (self.used(Lane::Bound, owner) < limit && (self.committed || !self.bound.is_empty()))
239 || (!held && self.used(Lane::Trailing, owner) < limit)
240 }
241}
242
243impl FilesInner {
244 pub(super) fn ask_retry(&self) -> Result<UploadRetryAnswer, SyncError> {
248 let (reply, answer) = oneshot::channel();
249 {
250 let mut retries = lock_unpoisoned(&self.app_retries);
254 self.check_open()?;
255 if let Some(ended) = self.worker_ended.get() {
256 return Err(ended.error());
257 }
258 let asked = self.retry_asked();
259 retries.push(AppRetry { covers: None, uploaded: 0, asked, reply });
260 }
261 self.upload_wake.notify_one();
262 Ok(UploadRetryAnswer(answer))
263 }
264
265 pub(super) fn worker_ended(&self, end: WorkerEnd) {
269 let mut retries = lock_unpoisoned(&self.app_retries);
270 let ended = self.worker_ended.get_or_init(|| end);
271 for retry in retries.drain(..) {
272 #[expect(
273 clippy::let_underscore_must_use,
274 reason = "a retry whose caller stopped waiting needs no answer"
275 )]
276 let _ = retry.reply.send(Err(ended.error()));
277 }
278 }
279
280 fn answer_retries(&self, answer: impl Fn() -> Option<Result<DrainOutcome, SyncError>>) {
283 let mut retries = lock_unpoisoned(&self.app_retries);
284 if answer().is_none() {
285 return;
286 }
287 for retry in retries.drain(..) {
288 if let Some(result) = answer() {
289 #[expect(
291 clippy::let_underscore_must_use,
292 reason = "a retry whose caller stopped waiting needs no answer"
293 )]
294 let _ = retry.reply.send(result);
295 }
296 }
297 }
298
299 #[cfg(test)]
302 pub(crate) async fn drain_uploads_retrying(&self) -> Result<DrainOutcome, SyncError> {
303 let mut own = self.ask_retry()?;
304 let run = self.drain_uploads().await;
305 own.try_answered().unwrap_or(run)
308 }
309
310 pub(crate) async fn drain_uploads(&self) -> Result<DrainOutcome, SyncError> {
316 use coven_foundation::task::CarriesBug;
317 let _run = self.drain.lock().await;
318 let result = self.serve_uploads().await;
319 match result {
320 Ok(outcome) => Ok(outcome),
321 Err(error) if lock_unpoisoned(&self.app_retries).is_empty() => Err(error),
322 Err(error) => {
323 if let Some(bug) = error.bug().cloned() {
324 self.answer_retries(|| Some(Err(SyncError::WorkerFailed(bug.clone()))));
325 return Err(error);
326 }
327 let (failure, source) = error.into_shared();
329 self.answer_retries(|| {
330 Some(Err(SyncError::Failed { failure, source: source.clone() }))
331 });
332 Err(SyncError::Failed { failure, source })
333 }
334 }
335 }
336
337 fn blocked(&self) -> Option<Blocked> {
339 if self.check_open().is_err() {
340 Some(Blocked::Closed)
341 } else if !self.waits.started() {
342 Some(Blocked::NotSyncing)
343 } else if read_unpoisoned(&self.storage).is_none() {
344 Some(Blocked::NoStorage)
345 } else if self.changing.load(Ordering::Acquire) > 0 {
346 Some(Blocked::Changing)
347 } else if !self.send.files_allowed() {
348 Some(Blocked::CatchUp)
349 } else {
350 self.waits.cooldown().map(Blocked::Cooling)
351 }
352 }
353
354 fn end_blocked(&self, blocked: Blocked, uploaded: usize) -> Result<DrainOutcome, SyncError> {
355 self.answer_retries(|| blocked.retry_answer());
356 blocked.run_outcome(uploaded)
357 }
358
359 fn end_held(
366 &self,
367 hold: TrailingHold,
368 uploaded: usize,
369 covered: impl Fn(&AppRetry) -> bool,
370 ) -> Result<DrainOutcome, SyncError> {
371 let blocked = Blocked::Held(hold);
372 let mut retries = lock_unpoisoned(&self.app_retries);
373 let (answered, asked_since): (Vec<_>, Vec<_>) =
374 retries.drain(..).partition(|retry| covered(retry));
375 *retries = asked_since;
376 let waiting = !retries.is_empty();
377 drop(retries);
378 for retry in answered {
379 if let Some(answer) = blocked.retry_answer() {
380 #[expect(
381 clippy::let_underscore_must_use,
382 reason = "a retry whose caller stopped waiting needs no answer"
383 )]
384 let _ = retry.reply.send(answer);
385 }
386 }
387 if waiting {
388 self.upload_wake.notify_one();
389 }
390 blocked.run_outcome(uploaded)
391 }
392
393 #[cfg(test)]
394 fn pass_blocked_end_gate(&self) {
395 let gate = lock_unpoisoned(&self.blocked_end_gate).clone();
396 if let Some(gate) = gate {
397 gate();
398 }
399 }
400
401 async fn serve_uploads(&self) -> Result<DrainOutcome, SyncError> {
402 let mut changes = self.database.queued_work_changes();
405 let mut sending = self.send.watch();
406 let mut reports = self.waits.network_reports();
407 let mut rearmed = self.waits.rearmed();
408 let mut syncing = self.waits.syncing();
409 let mut network = self.network.clone();
410 if let Some(blocked @ Blocked::Closed) = self.blocked() {
411 return self.end_blocked(blocked, 0);
412 }
413 self.end_abandoned_sessions().await?;
418 for id in self
419 .database
420 .retire_unlinked(self.database.uploads_in_flight().current(), upload_subject)
421 .await?
422 {
423 self.waits.forget(RetrySubject::Upload(id));
424 self.forget_connection_wait(RetrySubject::Upload(id));
425 }
426 if let Some(blocked @ (Blocked::NotSyncing | Blocked::NoStorage)) = self.blocked() {
427 return self.end_blocked(blocked, 0);
428 }
429 let change = self.lock_hold_change().await;
434 let read_after = self.retries_asked();
436 let paused = self.database.uploads_paused().await?;
437 self.set_paused(paused);
438 let unmetered_only = self.database.unmetered_only().await?;
439 self.unmetered_only.store(unmetered_only, Ordering::Release);
440 #[cfg(test)]
441 {
442 let gate = self.take_settings_read_gate();
443 if let Some((read, resume)) = gate {
444 read.send(()).expect("the test waits for the read");
445 resume.await.expect("the test resumes the run");
446 }
447 }
448 self.read_writes_paused().await?;
449 let work = self.database.upload_work::<PendingRecord>(upload_subject).await?;
450 if let Some(hold) = self.trailing_hold() {
453 if work.iter().all(|(item, _)| Lane::of(item) == Lane::Trailing) {
454 #[cfg(test)]
455 self.pass_blocked_end_gate();
456 return self.end_held(hold, 0, |retry| retry.asked < read_after);
457 }
458 }
459 drop(change);
460 if work.is_empty() {
461 self.answer_retries(|| Some(Ok(DrainOutcome::QueueEmpty)));
462 return Ok(DrainOutcome::QueueEmpty);
463 }
464 let mut run = Run::default();
465 let mut work = Some(work);
466 loop {
467 if let Some(blocked) = self.blocked() {
468 if run.attempts.is_empty() {
469 #[cfg(test)]
470 self.pass_blocked_end_gate();
471 return self.end_blocked(blocked, run.uploaded);
472 }
473 } else {
474 self.act_on_reports(&mut run).await;
475 let limit = self.transfers.limits().uploads.get();
476 if work.is_some() || (run.stale && run.room(limit, self.trailing_held(), self)) {
477 let work = match work.take() {
479 Some(work) if !run.stale => work,
480 _ => {
481 self.read_writes_paused().await?;
484 self.database.upload_work::<PendingRecord>(upload_subject).await?
485 }
486 };
487 self.read_ready(&mut run, work);
488 }
489 self.start_ready(&mut run, limit);
490 if run.attempts.is_empty() && run.ready.is_empty() {
491 let held = self.trailing_held();
494 if held && (!run.stale || !run.room(limit, held, self)) {
495 #[cfg(test)]
496 self.pass_blocked_end_gate();
497 let _change = self.lock_hold_change().await;
502 self.read_writes_paused().await?;
503 if let Some(hold) = self.trailing_hold() {
504 return self
505 .end_held(hold, run.uploaded, |retry| retry.covers.is_some());
506 }
507 run.stale = true;
508 continue;
509 }
510 if !run.stale
511 && lock_unpoisoned(&self.app_retries)
512 .iter()
513 .all(|retry| retry.covers.is_some())
514 {
515 for retry in lock_unpoisoned(&self.app_retries).drain(..) {
517 #[expect(
518 clippy::let_underscore_must_use,
519 reason = "a retry whose caller stopped waiting needs no answer"
520 )]
521 let _ = retry
522 .reply
523 .send(Ok(DrainOutcome::Drained { uploaded: retry.uploaded }));
524 }
525 return Ok(DrainOutcome::Drained { uploaded: run.uploaded });
526 }
527 run.stale = true;
528 continue;
529 }
530 }
531 let open = self.blocked().is_none()
534 && run.room(self.transfers.limits().uploads.get(), self.trailing_held(), self);
535 let retry = if open { self.next_upload_retry(&run.bound) } else { None };
536 tokio::select! {
537 Some((id, result)) = run.attempts.next(), if !run.attempts.is_empty() => {
538 self.finished(&mut run, id, result?);
539 }
540 () = self.upload_wake.notified() => run.stale = true,
541 () = self.connection_freed() => {}
544 change = changes.next() => {
545 change?;
546 run.stale = true;
547 run.committed = true;
548 }
549 _ = sending.changed() => run.stale = true,
550 _ = syncing.changed() => run.stale = true,
551 _ = reports.changed() => {}
552 () = network.changed() => run.stale = true,
553 _ = rearmed.changed() => {}
554 () = async {
555 match retry {
556 Some(retry) => retry.await,
557 None => std::future::pending().await,
558 }
559 } => run.stale = true,
560 }
561 }
562 }
563
564 fn failed_on_its_own(&self, what: &'static str, error: &SyncError) {
567 if !coven_foundation::task::report_carried(self.database.bugs().as_ref(), error) {
568 tracing::error!(?error, "{what}");
569 }
570 }
571
572 async fn act_on_reports(&self, run: &mut Run<'_>) {
575 if !run.crossed.is_empty() {
576 let crossed = std::mem::take(&mut run.crossed);
577 match self.waits.end_waits_after_report(NetworkOwner::Uploads, &crossed).await {
578 Ok(()) => run.stale = true,
579 Err(error) => {
580 self.failed_on_its_own("ending reported upload waits failed", &error);
581 run.crossed = crossed;
582 }
583 }
584 }
585 match self.waits.end_reported_waits(NetworkOwner::Uploads).await {
586 Ok(true) => run.stale = true,
587 Ok(false) => {}
588 Err(error) => self.failed_on_its_own("ending reported upload waits failed", &error),
589 }
590 }
591
592 fn read_ready(&self, run: &mut Run<'_>, work: Vec<(FileUpload, Option<PendingRecord>)>) {
597 run.stale = false;
598 run.committed = false;
599 run.bound = work
600 .iter()
601 .filter(|(item, _)| Lane::of(item) == Lane::Bound)
602 .map(|(item, _)| item.id)
603 .collect();
604 let held = self.trailing_held();
605 let retryable: BTreeSet<i64> = work
606 .iter()
607 .filter(|(_, record)| {
608 record.as_ref().is_none_or(|record| record.retry() != Retry::Never)
609 })
610 .map(|(item, _)| item.id)
611 .collect();
612 let forced: BTreeSet<i64> = {
613 let mut retries = lock_unpoisoned(&self.app_retries);
614 retries.retain(|retry| !retry.reply.is_closed());
616 for retry in retries.iter_mut() {
617 retry.covers.get_or_insert_with(|| retryable.clone());
618 }
619 retries.iter().flat_map(|retry| retry.covers.iter().flatten().copied()).collect()
620 };
621 self.answer_covered();
622 run.ready.clear();
623 for (item, record) in work {
624 let subject = RetrySubject::Upload(item.id);
625 if !retryable.contains(&item.id) || (held && Lane::of(&item) == Lane::Trailing) {
626 continue;
627 }
628 if run.started.contains_key(&item.id) {
631 self.waits.seen(subject);
632 } else if forced.contains(&item.id) {
633 self.waits.seen(subject);
634 run.ready.push_back(item);
635 } else if self.retry_due(&item, record.as_ref()) {
636 run.ready.push_back(item);
637 }
638 }
639 }
640
641 fn start_ready<'a>(&'a self, run: &mut Run<'a>, limit: usize) {
645 let held = self.trailing_held();
646 let mut waiting = VecDeque::new();
647 while let Some(item) = run.ready.pop_front() {
648 let lane = Lane::of(&item);
649 if lane == Lane::Trailing && held {
650 continue;
651 }
652 if run.used(lane, self) >= limit {
653 waiting.push_back(item);
654 continue;
655 }
656 let id = item.id;
657 run.lanes.insert(id, lane);
658 self.set_lane_attempts(lane, run.in_flight(lane));
659 run.started.insert(id, self.waits.network_reported());
660 let progress = coven_database::TransferProgress {
662 bytes_transferred: item.progress.map_or(0, |progress| progress.bytes_confirmed),
663 bytes_total: item.bytes_total,
664 };
665 let sending = self.database.uploads_in_flight().start(
666 item.location.uploader(),
667 item.location.object(),
668 item.files.clone(),
669 progress,
670 );
671 run.attempts.push(Box::pin(async move {
672 let attempted = self.attempt_upload(item, sending.meter()).await;
673 sending.end(match &attempted {
674 Ok(attempted) => attempted.transfer_end(),
675 Err(_) => coven_database::TransferEnd::Failed,
677 });
678 (id, attempted)
679 }));
680 }
681 for lane in [Lane::Bound, Lane::Trailing] {
684 let waits = waiting.iter().any(|item| Lane::of(item) == lane);
685 self.set_lane_waiting(lane, waits);
686 }
687 run.ready = waiting;
688 }
689
690 fn finished(&self, run: &mut Run<'_>, id: i64, attempted: super::file_upload::Attempted) {
694 use super::file_upload::Attempted;
695 if let Some(lane) = run.lanes.remove(&id) {
696 self.set_lane_attempts(lane, run.in_flight(lane));
697 }
698 let started = run.started.remove(&id);
699 let stored = attempted == Attempted::Stored;
700 match attempted {
701 Attempted::Stored => run.uploaded += 1,
702 Attempted::Retired => {}
703 Attempted::Failed | Attempted::Held => {
704 if let Some(ending) = started.and_then(|seen| self.waits.reports_since(seen)) {
705 run.crossed.insert(RetrySubject::Upload(id), ending);
706 }
707 }
708 }
709 for retry in lock_unpoisoned(&self.app_retries).iter_mut() {
710 if let Some(covers) = &mut retry.covers {
711 if covers.remove(&id) && stored {
712 retry.uploaded += 1;
713 }
714 }
715 }
716 self.answer_covered();
717 }
718
719 fn answer_covered(&self) {
721 let mut retries = lock_unpoisoned(&self.app_retries);
722 let mut kept = Vec::new();
723 for retry in retries.drain(..) {
724 match &retry.covers {
725 Some(covers) if covers.is_empty() => {
726 #[expect(
727 clippy::let_underscore_must_use,
728 reason = "a retry whose caller stopped waiting needs no answer"
729 )]
730 let _ =
731 retry.reply.send(Ok(DrainOutcome::Drained { uploaded: retry.uploaded }));
732 }
733 _ => kept.push(retry),
734 }
735 }
736 *retries = kept;
737 }
738}