using DataLayer; using System; using System.Collections.Generic; namespace ApplicationServices; /// /// How long to leave a title alone after a download attempt Audible refused, so a scheduled run stops asking /// for the same license every time. /// /// The wait doubles with each failure in a row, up to a cap, and every kind of failure has a finite cap: a /// title held back is always attempted again eventually. Audible never distinguishes "you will never have /// rights to this" from "not right now", so nothing here may be permanent. /// /// public static class DownloadRetryBackoff { private static readonly Dictionary schedule = new() { // An eligibility refusal changes only when the account or the catalog changes. A day matches the // advice Libation already gives for a Plus title ("try again in 1 to 2 days"), which is the most // common refusal that clears by itself. [DownloadFailureKind.LicenseDenied] = (TimeSpan.FromDays(1), TimeSpan.FromDays(30)), // A preorder becomes downloadable on its release date, which nobody can predict from the error, so // keep checking within a week. [DownloadFailureKind.AssetUnavailable] = (TimeSpan.FromHours(6), TimeSpan.FromDays(7)), // Short: an outage that has passed should not delay a title any longer than it has to. [DownloadFailureKind.ServiceInterruption] = (TimeSpan.FromHours(1), TimeSpan.FromHours(12)), }; /// How long to wait after the th failure in a row. public static TimeSpan GetWait(DownloadFailureKind kind, int consecutiveFailures) { var (first, max) = schedule.TryGetValue(kind, out var found) ? found : schedule[DownloadFailureKind.ServiceInterruption]; // Doubled one step at a time and stopped at the cap: first * 2^n overflows a TimeSpan long before a // plausible failure count, let alone the absurd ones a corrupt row could hold. var doublings = Math.Max(0, consecutiveFailures - 1); var wait = first; for (var i = 0; i < doublings && wait < max; i++) wait += wait; return wait > max ? max : wait; } /// When a title becomes eligible for another automatic attempt. public static DateTimeOffset GetRetryAfter(DownloadFailureKind kind, int consecutiveFailures, DateTimeOffset failedAt) => failedAt + GetWait(kind, consecutiveFailures); }