feat: split celery workers, fix asyncpg-in-fork, add pipeline progress UI
Three overlapping fixes so the ingestion pipeline actually runs and the
user can see what it's doing:
Pipeline recovery
- app/database.py: use NullPool when MULITA_CELERY_WORKER=1 so each
Celery task opens a fresh asyncpg connection on its own event loop.
Fixes "another operation in progress" and "Future attached to a
different loop" errors that were dropping ~every thumbnail +
extract_metadata task on the floor.
- app/tasks/thumbs.py: initialize photo=None before the try and rollback
on error so a transport failure in the initial SELECT doesn't raise
UnboundLocalError in the except block and leak rows stuck in 'pending'.
- app/services/vision/bootstrap_models.py: on missing model files,
invoke export_models automatically instead of just warning. First
boot of a fresh install now self-heals.
- app/services/vision/export_models.py: shutil.move instead of
Path.rename so the YOLO export survives the /app → /data/models
cross-volume hop.
- requirements.txt: add ultralytics so export works in a stock image.
Worker topology
- docker-compose.yml: replace the single worker with worker-light
(default/high/low queues, c=2, IO-bound) and worker-vision (vision
queue, c=5, OMP_NUM_THREADS=1 to avoid oversubscription on 6 cores).
Vision is pinned to ≤5 parallel inferences so ONNX doesn't each
spawn an all-cores intra-op pool.
- .env / .env.example: CELERYD_CONCURRENCY replaced with
CELERY_LIGHT_CONCURRENCY + CELERY_VISION_CONCURRENCY.
- Backfill queries in thumbs / scan / vision now ORDER BY taken_at
DESC NULLS LAST so newest photos finish first — the library fills
in top-down in the UI instead of arbitrary insertion order.
Settings visibility
- routers/library.py: new GET /maintenance/pipeline-stats returning
done/total per stage (thumbnails, exif, gps, phash, embeddings,
tags, ocr, faces, face clusters, duplicate groups). Worker-status
now also reports the `vision` queue depth, which was missing.
- services/api.ts: PipelineStats / PipelineStage / ScanStatus types
and the matching client call.
- components/dialogs/SettingsDialog.tsx:
- new Pipeline Progress card with one progress bar per stage
- inline scan banner (processed/total/current folder) inside the
Library section while a scan is running
- Tasks/min throughput computed by diffing worker processed counters
between polls
- Workers section calls out the vision queue and documents the
CELERY_LIGHT/VISION_CONCURRENCY + docker compose up -d scale path
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -297,6 +297,37 @@ export interface WorkerStatus {
|
||||
scan_errors: string[]
|
||||
}
|
||||
|
||||
export interface PipelineStage {
|
||||
key: string
|
||||
label: string
|
||||
done: number
|
||||
total: number
|
||||
hint: string
|
||||
/** True when the stage legitimately runs on a subset of photos — e.g.
|
||||
* GPS / tags / OCR / faces — so 100% coverage is never expected and
|
||||
* the UI should not frame "missing" as a problem. */
|
||||
partial?: boolean
|
||||
/** True when the stage doesn't have a done/total progress semantic
|
||||
* (e.g. face clusters, duplicate groups — those are output counts,
|
||||
* not ratios). The UI renders a plain count instead of a bar. */
|
||||
standalone?: boolean
|
||||
}
|
||||
|
||||
export interface PipelineStats {
|
||||
total_photos: number
|
||||
total_images: number
|
||||
embedder_model: string
|
||||
stages: PipelineStage[]
|
||||
}
|
||||
|
||||
export interface ScanStatus {
|
||||
is_scanning: boolean
|
||||
current_folder: string | null
|
||||
processed_files: number
|
||||
total_files: number
|
||||
errors: string[]
|
||||
}
|
||||
|
||||
export const library = {
|
||||
scan: async () => {
|
||||
const response = await api.post('/library/scan')
|
||||
@@ -344,6 +375,14 @@ export const library = {
|
||||
return response.data
|
||||
},
|
||||
|
||||
/** Per-stage ingestion progress — thumbnails, EXIF, GPS, phash,
|
||||
* embeddings, object tags, OCR, faces, face clusters, duplicate
|
||||
* groups. Drives the Pipeline Progress card in Settings. */
|
||||
pipelineStats: async (): Promise<PipelineStats> => {
|
||||
const response = await api.get('/library/maintenance/pipeline-stats')
|
||||
return response.data
|
||||
},
|
||||
|
||||
/** Dry-run count of photo rows whose files are no longer on disk
|
||||
* (under a mounted source root). */
|
||||
missingStats: async (): Promise<MissingStats> => {
|
||||
|
||||
Reference in New Issue
Block a user