91 lines
3.4 KiB
Plaintext
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
|