Files
music-agregator/docs/architecture/flows/monitor-album-error-poll-worker.puml
T

91 lines
3.4 KiB
Plaintext

@startuml MonitorAlbum - Async Poll Worker Failures
skinparam sequenceMessageAlign center
skinparam responseMessageBelowArrow true
title MonitorAlbum: Async Download Poll Worker Failures
participant "River Queue" as River
participant "PollDownloadWorker" as Worker
participant "TorrentClient\n(qBittorrent)" as QBit
database "PostgreSQL" as DB
note over Worker: These occur asynchronously\nafter MonitorAlbum returns.\nClient already received response.
== Case 1: qBittorrent unreachable during poll ==
River -> Worker: Work(PollDownloadArgs)
Worker -> QBit: Find(hash)
QBit --> Worker: error (connection refused)
note right #orange: Logged as error.\nJob rescheduled.
Worker -> River: Insert(PollDownloadArgs)
note right: Reschedule after check_interval (30s).\nRetries indefinitely until\nqBit becomes available.
== Case 2: Torrent disappeared from qBittorrent ==
River -> Worker: Work(PollDownloadArgs)
Worker -> QBit: Find(hash)
QBit --> Worker: empty results
note right #salmon: Torrent was removed\nfrom qBit externally.
Worker -> DB: downloads.SetFailed(id, "torrent not found in client")
note right: Download marked as failed.\nNo further polls scheduled.\nNo retry.
== Case 3: Torrent in error state ==
River -> Worker: Work(PollDownloadArgs)
Worker -> QBit: Find(hash)
QBit --> Worker: TorrentInfo{state: "error"}
note right #salmon: qBit reports torrent error.\n(e.g. tracker unreachable,\ncorrupt data, disk full)
Worker -> DB: downloads.SetFailed(id, "torrent error state")
note right: Download marked as failed.\nNo further polls.\nTorrent remains in qBit.
== Case 4: Download completes, but SetCompleted fails ==
River -> Worker: Work(PollDownloadArgs)
Worker -> QBit: Find(hash)
QBit --> Worker: TorrentInfo{progress: 1.0, savePath: "/downloads"}
Worker -> DB: downloads.SetCompleted(id, "/downloads")
DB --> Worker: error (DB connection lost)
note right #salmon: Worker returns error.\nRiver will retry the job\n(built-in retry policy).\nDownload stays in\n"downloading" state.
== Case 5: File scan fails after completion ==
River -> Worker: Work(PollDownloadArgs)
Worker -> QBit: Find(hash)
QBit --> Worker: TorrentInfo{progress: 1.0, path: "/downloads/Album"}
Worker -> DB: downloads.SetCompleted(id, "/downloads")
DB --> Worker: OK
note right #lightgreen: Download marked completed.
Worker -> Worker: scanAndHashFiles("/downloads/Album")
Worker --> Worker: error (permission denied / path not found)
note right #orange: Logged as error.\nDownload IS completed.\nBut download_files NOT populated.\nGetAlbum won't show file info\nfor individual tracks.
== Case 6: File persist fails ==
River -> Worker: Work(PollDownloadArgs)
Worker -> QBit: Find(hash)
QBit --> Worker: TorrentInfo{progress: 1.0}
Worker -> DB: downloads.SetCompleted(id, savePath)
Worker -> Worker: scanAndHashFiles → 12 files
Worker -> DB: download_files.CreateBatch(files)
DB --> Worker: error (duplicate / constraint)
note right #orange: Download is completed.\nFiles not persisted.\nNon-fatal: returns nil.\nNo retry.
== Case 7: App crash during download (startup recovery) ==
note over River, Worker: Application restarts.\nRiver picks up persisted jobs.
River -> Worker: RecoverOrphanedDownloads()
Worker -> DB: downloads.GetActive()
DB --> Worker: [Download{state: downloading, hash: ...}]
Worker -> River: Insert(PollDownloadArgs, UniqueOpts{ByArgs})
note right #lightgreen: Deduplicated insert.\nIf River job already exists\n→ no duplicate.\nIf job was lost → recovered.\nPolling resumes.
@enduml