How it works
Processing pipeline
Each file goes through the following steps:
flowchart TD
A[File detected] --> B[Wait for file to be fully written]
B --> C[Generate unique key]
C --> D[Read file into memory]
D --> E[Resize to low-resolution]
E --> F[Upload full-res and low-res to MinIO]
F -->|success| G[Move to processed/]
F -->|failure| H[Move to failed/]
File detection
The service uses FileSystemWatcher to detect new .jpg files in the watch directory. In addition, a background sweep runs every 30 seconds to catch any files that may have been missed due to OS-level event buffering or dropped events during high load.
Each file path is tracked in a concurrent dictionary to ensure it is only processed once, even if both the watcher and the sweep detect it at the same time.
Stable file check
Before processing a file, the service waits until it is fully written to disk. It polls the file size every 500 ms and considers the file ready when two consecutive checks return the same non-zero size and the file can be opened exclusively. This prevents reading a partially written file. The check times out after 2 minutes.
Key generation
Each photo is assigned a unique key in the format {StandId}-{UUID}.jpg. The StandId comes from configuration and identifies the physical stand. The UUID prevents collisions between photos taken on the same stand.
Upload
The full-resolution file and the resized version are uploaded to MinIO concurrently. A semaphore limits the total number of concurrent uploads across all files to 4.
Objects are stored at:
{FullPrefix}/{key}for the original{LowPrefix}/{key}for the resized version
Retry and reconnection
All MinIO operations use exponential backoff retry logic. When a failure occurs:
- The first failure emits a
MinIO connection lostwarning - Each subsequent attempt waits twice as long as the previous one, up to
RetryMaxDelayMs - When the operation eventually succeeds, a
MinIO connection restoredlog is emitted
Non-retriable errors (invalid credentials, bad bucket name, access denied) are not retried and cause the operation to fail immediately.
Graceful shutdown
When SIGTERM is received:
- The file watcher stops raising events
- No new files are accepted
- The service waits for all in-progress uploads to complete
- The process exits cleanly
The shutdown timeout is 5 minutes. Files that are queued but not yet started are cancelled and remain in the watch directory for processing on the next startup.
Logging
All log entries are structured and use named parameters, compatible with any log aggregator that supports structured logging (e.g. Seq, Loki).
Key log points:
| Event | Level |
|---|---|
| File detected | Information |
| Resize started / completed | Debug |
| Upload started | Debug |
| Upload completed | Information |
| MinIO connection lost | Warning |
| MinIO connection restored | Information |
| File moved to processed | Information |
| File moved to failed | Warning |
| Shutdown initiated / complete | Information |
| Any processing error | Error |
Set Logging__LogLevel__Default=Debug to see all steps including resize and upload start events.