* feat(metadata): expand provider image cache queue * fix(metadata): harden provider image cache queue Addresses bug-review feedback from Codex/CodeRabbit on the metadata image cache pipeline. All findings validated against the code before fixing; false positives (rows/connection deadlock, PhotoSourcePath merge coupling) were confirmed non-issues and left unchanged. - Honor metadata.cache_images for the background processor. The cache_metadata_images task was registered whenever S3 was configured, so merely enabling object storage downloaded the entire provider-artwork catalog even with caching disabled. Add ImageCacheProcessor.SetEnabled, gate RunOnce/RunUntilIdle on it, and wire it (with hot reload) from cfg.Metadata.CacheImages in main.go. - Guard terminal job updates with lease ownership. EnqueueBatch can repurpose a running row with a new source; MarkSucceeded/MarkFailed keyed on id alone let a stale worker finalize the replacement job and drop the new artwork. Thread locked_by through and add status='running' AND locked_by=$n guards. - Avoid uploading stale jobs onto the live artwork key. Verify the target still references the job's source (CurrentTargetSourcePath) before CacheImage, so a job whose source an admin/refresh already replaced cannot overwrite the deterministic storage object. - COALESCE nullable external IDs in EnqueueExistingProviderArtwork. A NULL tmdb_id/tvdb_id/imdb_id on any candidate failed the scan and aborted the whole cache run; matches the existing item_repo pattern. - Stop re-downloading the catalog every 30 days. Discovery now skips targets whose *_path is already a cached relative path, making the cached row the durable dedup marker instead of the prunable job row. - Decouple catalog sweeps from queue draining. RunOnce no longer runs discovery per batch; RunUntilIdle sweeps only when the queue drains and throttles full sweeps to every 15m, so idle installs stop full-scanning every entity table each minute. - Requeue claimed-but-unstarted jobs on cancellation. Acquire the semaphore before spawning workers and RequeueClaimed any jobs not yet started, instead of leaving them locked until the 15m lease expires. - Skip the backoff sleep after the final upload attempt in putObjectWithRetry (saves ~1.5s on permanent failures). - Add the s3/file/local/upload/generated exclusion to the seasons and episodes backfill in migration 20260617184537 for consistency with the later migration (the bad backfill was inert downstream, but the asymmetry is removed). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
78 lines
2.3 KiB
Go
78 lines
2.3 KiB
Go
package tasks
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/Silo-Server/silo-server/internal/metadata"
|
|
"github.com/Silo-Server/silo-server/internal/taskmanager"
|
|
)
|
|
|
|
const (
|
|
cacheMetadataImagesIntervalMs = int64(60 * 1000)
|
|
cacheMetadataImagesBatchSize = 1000
|
|
cacheMetadataImagesWorkers = 12
|
|
cacheMetadataImagesMaxRuntime = 10 * time.Minute
|
|
)
|
|
|
|
type MetadataImageCacheRunner interface {
|
|
RunUntilIdle(ctx context.Context, workerID string, claimLimit int, concurrency int, maxRuntime time.Duration) (metadata.ImageCacheRunStats, error)
|
|
}
|
|
|
|
type CacheMetadataImagesTask struct {
|
|
runner MetadataImageCacheRunner
|
|
}
|
|
|
|
func NewCacheMetadataImagesTask(runner MetadataImageCacheRunner) *CacheMetadataImagesTask {
|
|
return &CacheMetadataImagesTask{runner: runner}
|
|
}
|
|
|
|
func (t *CacheMetadataImagesTask) Key() string { return "cache_metadata_images" }
|
|
func (t *CacheMetadataImagesTask) Name() string { return "Cache Metadata Images" }
|
|
func (t *CacheMetadataImagesTask) Description() string {
|
|
return "Caches provider metadata artwork into object storage"
|
|
}
|
|
func (t *CacheMetadataImagesTask) Category() taskmanager.TaskCategory {
|
|
return taskmanager.TaskCategoryMetadata
|
|
}
|
|
func (t *CacheMetadataImagesTask) IsHidden() bool { return false }
|
|
|
|
func (t *CacheMetadataImagesTask) DefaultTriggers() []taskmanager.TriggerConfig {
|
|
return []taskmanager.TriggerConfig{
|
|
{Type: taskmanager.TriggerTypeStartup},
|
|
{Type: taskmanager.TriggerTypeInterval, IntervalMs: cacheMetadataImagesIntervalMs},
|
|
}
|
|
}
|
|
|
|
func (t *CacheMetadataImagesTask) Execute(ctx context.Context, progress taskmanager.ProgressReporter) error {
|
|
if t.runner == nil {
|
|
progress.Report(100, "Metadata image cache is not configured")
|
|
return nil
|
|
}
|
|
hostname, _ := os.Hostname()
|
|
if hostname == "" {
|
|
hostname = "silo"
|
|
}
|
|
stats, err := t.runner.RunUntilIdle(ctx, hostname, cacheMetadataImagesBatchSize, cacheMetadataImagesWorkers, cacheMetadataImagesMaxRuntime)
|
|
if err != nil {
|
|
return fmt.Errorf("caching metadata images: %w", err)
|
|
}
|
|
message := fmt.Sprintf(
|
|
"Batches %d, enqueued %d existing, claimed %d, cached %d, failed %d, skipped %d, deleted %d old successes",
|
|
stats.Batches,
|
|
stats.EnqueuedExisting,
|
|
stats.Claimed,
|
|
stats.Succeeded,
|
|
stats.Failed,
|
|
stats.Skipped,
|
|
stats.DeletedSucceeded,
|
|
)
|
|
if stats.RuntimeLimited {
|
|
message += ", runtime budget reached"
|
|
}
|
|
progress.Report(100, message)
|
|
return nil
|
|
}
|