feat(podcasts): concurrent download pool — fair share, tickets, admin tunables
Replaces the sequential worker with a download-manager-style pool (§0 item 4). Model: the user's connection is the limit; semantics are fair-share ACROSS feeds, not goroutine-per-book (a global FIFO would make Book B wait behind Book A's 100 files). - Atomic claim: GetNextQueuedEpisode + SetPodcastEpisodeDownloading was a read-then-write race under concurrency; ClaimQueuedPodcastEpisode converts queued→downloading in ONE guarded statement (two claimants, one winner — the loser gets ErrNoRows). SetPodcastEpisodeDownloading retired. - Queue tickets: download_queue_pos BIGINT fed by a Postgres SEQUENCE stamped at enqueue — the enqueue loop's ordering IS the claim order (the foundation §0 item 5 builds on; previously all rows in a batch shared one updated_at, so within-feed order was arbitrary). - Pool: up to podcast_download_concurrency (default 4, live range 1–16) claim-loop goroutines per drain round; the setting re-reads every round so admin changes take effect without restart. - Fair share: claim prefers the feed least-recently-served (pickFeedToServe; never-served first), earliest ticket within a feed — books proceed in parallel while chapters land in order (start listening while the rest download). - Bandwidth: shared token bucket across all workers (podcast_download_speed_mbps, default 0 = unlimited); take() reserves bandwidth and callers sleep outside the lock. - Settings: new float setting type end to end (registry defaults + clamping, save validation, admin UI number-with-step input) + the two schema seeds; worker wires the registry via SetSettings (tests run on registered defaults). - Tests: unit coverage for the bucket reservation math + fair-share pick; integration TestPodcastDownloadPool pins ticket order = enqueue order, claim exclusivity, and a full 3-episode pool drain. Dev-DB note: the column/sequence/settings were applied live (idempotent DDL, same statements the schema runs at next boot) since the running container predates them.
This commit is contained in:
1 parent
e3675536aa
commit
0f3c4a8cec
14 files changed
+893
-264
No files matched your search
@@ -178,6 +178,21 @@ func validateSettingValue(def database.SettingDefault, value string) error {
|
||||
if _, err := strconv.ParseBool(value); err != nil {
|
||||
return fmt.Errorf("value must be true or false")
|
||||
}
|
||||
case database.SettingTypeFloat:
|
||||
f, err := strconv.ParseFloat(value, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("value must be a number")
|
||||
}
|
||||
if def.Min != "" {
|
||||
if mn, err := strconv.ParseFloat(def.Min, 64); err == nil && f < mn {
|
||||
return fmt.Errorf("value must be >= %s", def.Min)
|
||||
}
|
||||
}
|
||||
if def.Max != "" {
|
||||
if mx, err := strconv.ParseFloat(def.Max, 64); err == nil && f > mx {
|
||||
return fmt.Errorf("value must be <= %s", def.Max)
|
||||
}
|
||||
}
|
||||
case database.SettingTypeString:
|
||||
if value == "" {
|
||||
return fmt.Errorf("value must not be empty")
|
||||
|
||||
Reference in new issue
Block a user