Ein verpasster Bildabruf war endgültig

Auf einem großen Fenster blieb die zweite Reihe fast vollständig grau,
während die erste geladen hatte. Es lag weder am Server noch an
fehlenden Bildern: sechzig sichtbare Kacheln teilen sich zwölf
Verbindungen, und die hinteren stehen so lange in der Schlange, dass sie
in den Zeitablauf laufen. Oben fragt niemand ein zweites Mal — task(id:)
läuft einmal je Adresse.

Jetzt wird bis zu dreimal gefragt, mit wachsendem Abstand. Aber nur, wo
es sich lohnt: ein 404 heißt, dass es das Bild nicht gibt, und dreimal
zu fragen macht es nicht vorhanden. Wiederholt werden Zeitabläufe,
abgerissene Verbindungen, 5xx und 429.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-14 16:18:30 +02:00
co-authored by Claude Opus 5
parent cdef325dcd
commit 018eaa3308
@@ -70,12 +70,7 @@ public actor ArtworkLoader {
if let running = inFlight[url] { return await running.value } if let running = inFlight[url] { return await running.value }
let task = Task<PlatformImage?, Never> { [session] in let task = Task<PlatformImage?, Never> { [session] in
guard let (data, response) = try? await session.data(from: url), await Self.fetch(url, using: session)
let http = response as? HTTPURLResponse,
(200..<300).contains(http.statusCode),
let image = PlatformImage(data: data)
else { return nil }
return image
} }
inFlight[url] = task inFlight[url] = task
@@ -85,6 +80,54 @@ public actor ArtworkLoader {
return image return image
} }
/// Holt ein Bild — und gibt nicht beim ersten Nein auf.
///
/// Der Grund ist ein Fehlerbild, das lange rätselhaft aussah: auf einem
/// großen Fenster blieb die **zweite** Reihe fast vollständig grau,
/// während die erste geladen hatte. Es lag nicht am Server und nicht an
/// fehlenden Bildern, sondern an der Gleichzeitigkeit — sechzig sichtbare
/// Kacheln teilen sich zwölf Verbindungen, die hinteren stehen so lange
/// in der Schlange, dass sie in den Zeitablauf laufen.
///
/// Oben fragt niemand ein zweites Mal: `task(id:)` läuft einmal je
/// Adresse. Ein einziger verpasster Versuch war damit endgültig.
///
/// **Nur wiederholt, was sich zu wiederholen lohnt.** Ein 404 heißt, dass
/// es das Bild nicht gibt — dreimal zu fragen macht es nicht vorhanden
/// und kostet den Server drei Antworten. Ein Zeitablauf oder ein
/// abgerissener Strang dagegen sagt nur, dass es *gerade* nicht ging.
private static func fetch(
_ url: URL,
using session: URLSession,
attempts: Int = 3
) async -> PlatformImage? {
for attempt in 1...attempts {
if Task.isCancelled { return nil }
do {
let (data, response) = try await session.data(from: url)
guard let http = response as? HTTPURLResponse else { return nil }
if (200..<300).contains(http.statusCode) {
return PlatformImage(data: data)
}
// Der Server hat geantwortet und nein gesagt. Bei 5xx und 429
// lohnt ein zweiter Versuch, sonst nicht.
guard http.statusCode >= 500 || http.statusCode == 429 else { return nil }
} catch is CancellationError {
return nil
} catch {
// Zeitablauf, abgerissene Verbindung: der Fall, für den das
// alles hier gebaut ist.
}
guard attempt < attempts else { return nil }
// Wachsender Abstand: ist die Leitung gerade verstopft, hilft es
// nicht, sofort noch einmal zu drängeln.
try? await Task.sleep(for: .milliseconds(300 * attempt * attempt))
}
return nil
}
} }
/// Ein Bildspeicher, der sich von überall lesen lässt. /// Ein Bildspeicher, der sich von überall lesen lässt.