feat(scanner): quiet scans, enqueue coalescing, watcher probe, sibling cache
Part 2 of the faceted plan, Phase 5 (steps 5-6): the scan that used to print ~4 lines per unchanged file now prints ONE summary line, and a poll tick landing mid-scan no longer stacks a second full scan. - Quiet scans (§4): the walk loop is silent — inserts, changes, and errors log inside processMediaFile; the duplicate 'Successfully processed' branch (both branches printed it) and the per-file 'processing / already exists / same size, skipping' chatter are gone. Summary carries the trigger and a changed counter: 'Scan of <folders> (trigger: poll): N files, N changed, N new, Xs, N errors'. Triggers threaded through every enqueue site: poll (startBackupScan), watcher (dirty-dirs flush), admin (scan API), initial (startup scan). - Enqueue coalescing (§5): Worker.HasActiveScanForLibrary (JobResult gains additive job_type so only SCAN jobs coalesce — an import sharing the library id never blocks one). enqueueLibraryScan skips silently; the admin scan API answers 409 with the active job id. Enforces the one-scan-per-library invariant the bulk scan-state map leans on. - Watcher capability probe (§4): ONE Add on a root folder; failure disables the watcher with a single startup line (replaces the per-directory warning spam on inotify-broken deployments). The watcher stays an optional latency reducer over scheduled scans. - Per-directory sibling cache (§6): hasSiblingBookFileCached caches the ReadDir per directory for one scan — loose-image manga re-stats thousands of images sharing a handful of directories. Tests: TestHasActiveScanForLibrary (unit — running/pending coalesce, terminal jobs don't, non-scan jobs don't, empty library never, nil worker safe). Full scanner suite green in the container.
This commit is contained in:
1 parent
6750e9e0dc
commit
5245f292ca
5 files changed
+225
-49
No files matched your search
@@ -72,6 +72,7 @@ func (w *Worker) recordPending(job *Job) {
|
||||
JobID: job.ID,
|
||||
Status: JobStatusPending,
|
||||
LibraryID: libID,
|
||||
JobType: job.Type,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,7 +105,11 @@ type JobResult struct {
|
||||
// LibraryID carries the scan job's library (from Params) so clients
|
||||
// can label progress rows after a page refresh, when the original
|
||||
// start-request context is gone.
|
||||
LibraryID string `json:"library_id,omitempty"`
|
||||
LibraryID string `json:"library_id,omitempty"`
|
||||
// JobType distinguishes scan jobs in the active set — the enqueue
|
||||
// coalescing check (HasActiveScanForLibrary) must only coalesce
|
||||
// scans, never imports/converts sharing a library id.
|
||||
JobType JobType `json:"job_type,omitempty"`
|
||||
Error string `json:"error"`
|
||||
Result interface{} `json:"result"`
|
||||
Progress float64 `json:"progress"`
|
||||
@@ -286,6 +291,7 @@ func (w *Worker) processJob(job *Job) {
|
||||
JobID: job.ID,
|
||||
Status: JobStatusRunning,
|
||||
LibraryID: libID,
|
||||
JobType: job.Type,
|
||||
}
|
||||
|
||||
// Derive a per-job context so cancelling this job (CancelJob) stops
|
||||
@@ -486,6 +492,12 @@ func (w *Worker) processScanJob(job *Job) (interface{}, error) {
|
||||
scanner.SetAdminID(adminUUID)
|
||||
scanner.SetForce(force)
|
||||
|
||||
// Trigger label (plan Part 2 §4): poll / watcher / admin / initial —
|
||||
// surfaces in the per-scan summary line.
|
||||
if trigger, ok := job.Params["trigger"].(string); ok {
|
||||
scanner.SetTrigger(trigger)
|
||||
}
|
||||
|
||||
var libraryUUID pgtype.UUID
|
||||
if err := libraryUUID.Scan(libraryID); err != nil {
|
||||
return nil, err
|
||||
@@ -1073,6 +1085,28 @@ func (w *Worker) GetActiveScanJobs() []JobResult {
|
||||
return active
|
||||
}
|
||||
|
||||
// HasActiveScanForLibrary reports whether a pending or running SCAN job
|
||||
// already targets the library (faceted plan Part 2 §5): coalescing
|
||||
// stops poll ticks from stacking full scans when a scan outlives its
|
||||
// interval, and enforces the one-scan-per-library invariant the bulk
|
||||
// scan-state map leans on.
|
||||
func (w *Worker) HasActiveScanForLibrary(libraryID string) bool {
|
||||
if w == nil || libraryID == "" {
|
||||
return false
|
||||
}
|
||||
w.mu.RLock()
|
||||
defer w.mu.RUnlock()
|
||||
for _, result := range w.results {
|
||||
if result.JobType != JobTypeScan || result.LibraryID != libraryID {
|
||||
continue
|
||||
}
|
||||
if result.Status == JobStatusRunning || result.Status == JobStatusPending {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// CancelJob cancels a pending or running job by id. Pending jobs are
|
||||
// marked cancelled so processJob skips them at dequeue (no context
|
||||
// exists yet); running jobs have their per-job context cancelled — the
|
||||
|
||||
Reference in new issue
Block a user