Add stage-3 content triage: sort unknown files into binary/ and text/
Unknown files are no longer terminal. Stage 1 now routes :unknown onto a dedicated queue (with the same blocking backpressure as the known queue), and a third worker pool sorts each file into data/binary/ or data/text/ using a NUL-byte sniff of the first 8000 bytes. - content.jl: is_binary content sniff (stage 3) - worker.jl: handle_unknown_job; stage-1 routes unknown with backpressure; KNOWN_ENQUEUE_RETRY_SECONDS -> ROUTE_ENQUEUE_RETRY_SECONDS (serves both) - config.jl: unknown_worker_count/queue_capacity, binary_dir, text_dir + env - FileServer.jl: unknown queue, pool, stage-aware recovery, drain ordering - tests for is_binary and handle_unknown_job; tmp_config isolates new dirs - README: three-stage pipeline
This commit is contained in:
@@ -1,48 +1,54 @@
|
||||
# Worker tasks: pull jobs off a queue and process them. The loop scaffolding
|
||||
# (dequeue-until-drained, try/catch, quarantine-on-throw) is identical for every
|
||||
# stage, so `worker_loop` is parametrized with a `handler` and reused. Today
|
||||
# there are two stages:
|
||||
# there are three stages:
|
||||
#
|
||||
# stage 1 handle_classify_job spool/ → classify → known/ (+known queue) | unknown/
|
||||
# stage 1 handle_classify_job spool/ → classify → known/ (+known queue) | unknown/ (+unknown queue)
|
||||
# stage 2 handle_known_job known/ → exiftool enrich → done/ (+ .meta.json)
|
||||
# stage 3 handle_unknown_job unknown/ → binary-vs-text sniff → binary/ | text/
|
||||
#
|
||||
# Adding a stage later (e.g. an unknown-file pool consuming unknown/) is just
|
||||
# another queue + pool + handler; the loop below doesn't change.
|
||||
# Adding a stage later is just another queue + pool + handler; the loop below
|
||||
# doesn't change.
|
||||
|
||||
# How long a stage-1 worker backs off before retrying an enqueue onto a full
|
||||
# known queue. Blocking backpressure: a classified file is never dropped, so the
|
||||
# stage-1 worker parks until stage 2 makes room. Keeps intake decoupled — the
|
||||
# HTTP path's `enqueue!` stays non-blocking; only this worker-to-worker handoff
|
||||
# blocks.
|
||||
const KNOWN_ENQUEUE_RETRY_SECONDS = 0.05
|
||||
# downstream queue (known or unknown). Blocking backpressure: a classified file
|
||||
# is never dropped, so the stage-1 worker parks until the next stage makes room.
|
||||
# Keeps intake decoupled — the HTTP path's `enqueue!` stays non-blocking; only
|
||||
# this worker-to-worker handoff blocks.
|
||||
const ROUTE_ENQUEUE_RETRY_SECONDS = 0.05
|
||||
|
||||
"""
|
||||
handle_classify_job(job, cfg, worker_id, known_queue)
|
||||
handle_classify_job(job, cfg, worker_id, known_queue, unknown_queue)
|
||||
|
||||
Stage 1. Classify the spooled file and route it:
|
||||
* `:unknown` → move to `unknown/` (parked for a future pipeline; terminal here).
|
||||
* `:known` → move to `known/`, then enqueue onto the known queue for stage 2,
|
||||
retrying on a full queue rather than dropping the file.
|
||||
Stage 1. Classify the spooled file and route it to the next stage's queue,
|
||||
retrying on a full queue rather than dropping the file:
|
||||
* `:known` → move to `known/`, enqueue onto the known queue for stage 2.
|
||||
* `:unknown` → move to `unknown/`, enqueue onto the unknown queue for stage 3.
|
||||
|
||||
In both cases move first so the file physically lives in its stage dir before the
|
||||
reference is visible downstream; the moved path becomes the routed job's location.
|
||||
|
||||
Sub-`MIN_FILE_BYTES` files short-circuit to `:unknown` inside `classify`.
|
||||
"""
|
||||
function handle_classify_job(job::Job, cfg::Config, worker_id::Int, known_queue::JobQueue)
|
||||
function handle_classify_job(job::Job, cfg::Config, worker_id::Int,
|
||||
known_queue::JobQueue, unknown_queue::JobQueue)
|
||||
classification = classify(CLASSIFIER[], job.path)
|
||||
@info "classified file" worker=worker_id id=job.id name=job.original_name size=job.size classification=classification
|
||||
|
||||
if classification === :known
|
||||
# Move first so the file physically lives in known/ before the reference
|
||||
# is visible to stage 2; then hand off. The moved path becomes the job's
|
||||
# new location for the known queue.
|
||||
dest = move_to(cfg.known_dir, job)
|
||||
routed = Job(job.id, job.original_name, dest, job.size, job.received_at)
|
||||
while !enqueue!(known_queue, routed)
|
||||
sleep(KNOWN_ENQUEUE_RETRY_SECONDS) # known queue full → back off, don't drop
|
||||
sleep(ROUTE_ENQUEUE_RETRY_SECONDS) # known queue full → back off, don't drop
|
||||
end
|
||||
@info "routed to enrichment" worker=worker_id id=job.id dest=dest
|
||||
else
|
||||
dest = move_to(cfg.unknown_dir, job)
|
||||
@info "parked unknown" worker=worker_id id=job.id dest=dest
|
||||
routed = Job(job.id, job.original_name, dest, job.size, job.received_at)
|
||||
while !enqueue!(unknown_queue, routed)
|
||||
sleep(ROUTE_ENQUEUE_RETRY_SECONDS) # unknown queue full → back off, don't drop
|
||||
end
|
||||
@info "routed to content triage" worker=worker_id id=job.id dest=dest
|
||||
end
|
||||
return nothing
|
||||
end
|
||||
@@ -63,6 +69,21 @@ function handle_known_job(job::Job, cfg::Config, worker_id::Int)
|
||||
return nothing
|
||||
end
|
||||
|
||||
"""
|
||||
handle_unknown_job(job, cfg, worker_id)
|
||||
|
||||
Stage 3. Sort an unrecognized file into a coarse content bucket by sniffing its
|
||||
first bytes: `binary/` if it looks like binary data, `text/` otherwise. Terminal
|
||||
— there is no further stage. Simple by design for now; richer handling can hang
|
||||
off either bucket later.
|
||||
"""
|
||||
function handle_unknown_job(job::Job, cfg::Config, worker_id::Int)
|
||||
binary = is_binary(job.path)
|
||||
dest = move_to(binary ? cfg.binary_dir : cfg.text_dir, job)
|
||||
@info "sorted unknown" worker=worker_id id=job.id name=job.original_name kind=(binary ? :binary : :text) dest=dest
|
||||
return nothing
|
||||
end
|
||||
|
||||
"""
|
||||
worker_loop(worker_id, cfg, queue, handler)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user