Commit graph

1853 commits

Author SHA1 Message Date
Yiorgis Gozadinos
4eae86225e
vb 2026-06-03 14:26:58 +03:00
Yiorgis Gozadinos
0b58e6dc14
Merge pull request #417 from ggozad/fix/atomic-rename-race
skip spurious ingester DELETE when the resource is back on its source
2026-06-03 14:17:51 +03:00
Yiorgis Gozadinos
8afdd46176
skip spurious ingester DELETE when the resource is back on its source 2026-06-03 14:05:53 +03:00
Yiorgis Gozadinos
5b879aa6ed
Merge pull request #416 from ggozad/fix/rebuild-embed-images
Fix rebuild --embed-only corrupting picture embeddings
2026-06-03 13:49:04 +03:00
Yiorgis Gozadinos
a65757807e
Use cached HuggingFace models offline in test job
The Qwen tokenizer and cross-encoder pre-downloads call the HF metadata
API to revalidate even on a cache hit; a 429 there propagates instead of
falling back to the cached files, failing CI on HF throttling.

Skip the pre-download steps when the cache is restored and run pytest
with HF_HUB_OFFLINE/TRANSFORMERS_OFFLINE on a hit, so cached models are
used without any network revalidation; allow online on a miss so a fresh
cache key still populates. Rename the cache key so the snapshot
re-populates with every test model (the old key predated the
cross-encoder step and never cached it).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 12:11:33 +03:00
Yiorgis Gozadinos
651b22ddcf
Fix rebuild --embed-only corrupting picture embeddings 2026-06-03 11:46:43 +03:00
Yiorgis Gozadinos
6d95fbe74a
Merge pull request #414 from ggozad/feat/prune-ingestor-pool
Add retention window to ingester queue
2026-06-03 11:06:44 +03:00
Yiorgis Gozadinos
a423a6dd9c
authenticate HuggingFace downloads in test job 2026-06-03 10:57:45 +03:00
Yiorgis Gozadinos
46747d369a
Add retention window to ingester queue, prune terminal job rows past retention window 2026-06-03 10:38:58 +03:00
Yiorgis Gozadinos
83b7e37855
Merge pull request #413 from ggozad/feat/analysis-citation-improvement
Improve citation rate for analysis skill in lesser models (gemma4)
2026-06-03 10:03:54 +03:00
Yiorgis Gozadinos
4ff17a84bb
Update benchmarks 2026-06-02 15:20:17 +03:00
Yiorgis Gozadinos
eb74525084
Lift analysis-skill citation rate via SKILL.md tightening 2026-06-01 18:58:52 +03:00
Yiorgis Gozadinos
8dbcd4bd7a
vb, changelog 2026-06-01 18:54:22 +03:00
Yiorgis Gozadinos
0550e363d4
Merge pull request #394 from mcdonc/perf/max-file-size-validation
perf: add configurable max_file_size to prevent OOM on large files
2026-06-01 18:49:18 +03:00
Chris McDonough
7e4aa8c71f
Add configurable max_file_size to reject oversized files before ingestion
Large files buffered entirely in RAM can OOM workers. Add
max_file_size to source config (default None = no limit).

FS checks stat().st_size before read_bytes(). HTTP and WebDAV issue
a HEAD request before GET when a limit is configured. S3 checks the
size from the existing head_async() call before get_async().

FileTooLargeError is classified as PermanentError so oversized files
go straight to the DLQ instead of retrying.
2026-06-01 18:40:46 +03:00
Yiorgis Gozadinos
774ac7c350
Merge pull request #393 from mcdonc/perf/batch-sync-state-writes
perf: batch sync_state writes during poller sweeps
2026-06-01 18:33:40 +03:00
Yiorgis Gozadinos
249d6c85c5
Merge pull request #401 from mcdonc/fix/revisionless-server-re-ingestion
fix: stop constant re-ingestion when server provides no revision header
2026-06-01 18:25:51 +03:00
Yiorgis Gozadinos
2cd97880fd
Replace sync_state batch 5-tuple with a SyncRow NamedTuple 2026-06-01 18:25:15 +03:00
Chris McDonough
a0a247d18a
Batch sync_state writes during poller sweeps
Each discovered file previously triggered a separate sync.upsert()
call with its own lock acquire + SQLite commit (fsync). On a sweep
finding 1,000 files this meant 1,000 individual commits.

Collect sync_state rows into a list during the sweep and flush them
in a single SyncStateRepo.batch_upsert() call at the end — one lock
acquisition, one commit, one fsync.
2026-06-01 18:21:09 +03:00
Chris McDonough
8deac2fee8
Fix constant re-ingestion when server provides no revision header
HTTP, S3, and WebDAV sources all check `revision is not None and
snapshot.get(uri) == revision` to decide UPSERT vs UNCHANGED.  When
a server returns no ETag or Last-Modified, revision is None and the
condition always fails — every sweep emits UPSERT even though the
content hasn't changed.

Now emit UNCHANGED when revision is None and the URI is already
known (has been ingested before).  A first-time discovery with no
revision still correctly emits UPSERT.
2026-06-01 18:14:27 +03:00
Yiorgis Gozadinos
20634376a3
Merge pull request #411 from mcdonc/chore/coverage-gaps
chore: close coverage gaps in cli, filter, registry, and migrations
2026-06-01 18:12:22 +03:00
Yiorgis Gozadinos
c0faf5ecf3
Merge pull request #408 from mcdonc/fix/directory-errors-permanent
fix: classify IsADirectoryError and NotADirectoryError as PermanentError
2026-06-01 18:07:24 +03:00
Yiorgis Gozadinos
734a1d8f0e
Merge pull request #407 from mcdonc/fix/http-discover-silent-exception
fix: narrow HTTP discover() exception catch to TransportError
2026-06-01 18:06:46 +03:00
Yiorgis Gozadinos
c1932e22e5
Strengthen config-load assertions 2026-06-01 18:03:09 +03:00
Chris McDonough
1bf2505093
Improve test coverage for cli, filter, registry, and migrations
These files were not touched by the recent performance and
correctness PRs but had coverage gaps. Adds tests for:

- CLI: serve, queue init/migrate, config loading, cli() entry point
  including MigrationRequiredError exit path
- filter: _default_supported_extensions, __call__ watchfiles callback,
  FileFilter with supported_extensions=None
- registry: resolve_adhoc_fetcher with bucket-less S3 URI
- migrations: pragma no-cover on unreachable schema upgrade path
  (no diff migrations exist until SCHEMA_VERSION > 1)
2026-06-01 18:00:47 +03:00
Chris McDonough
adf03284ff
Classify IsADirectoryError and NotADirectoryError as PermanentError
Both are OSError subclasses caught by the broad timeout/io handler
and classified as transient. Pointing at a directory instead of a
file or a broken path component will never succeed on retry.
2026-06-01 17:59:23 +03:00
Chris McDonough
61ea22527a
Narrow HTTP discover() exception catch to TransportError only
The bare `except Exception` in HTTPSource.discover() silently
swallowed all errors from HEAD requests — including configuration
errors (bad auth, invalid headers) and programming errors (TypeError,
AttributeError) — treating them identically to network failures by
emitting UPSERT with no revision.

Narrow the catch to httpx.TransportError (the umbrella for
ConnectError, TimeoutException, etc.) and add a debug log. Other
exceptions now propagate to the poller's circuit breaker where they
surface as failures instead of being silently retried forever.
2026-06-01 17:57:51 +03:00
Yiorgis Gozadinos
64f2b7b7d2
Merge pull request #405 from mcdonc/fix/run-batch-hang-on-dead-workers
fix: run_batch hangs forever when all workers die
2026-06-01 17:51:49 +03:00
Yiorgis Gozadinos
49cb6e7591
Merge pull request #409 from mcdonc/fix/sync-state-write-crash
fix: sync_state write failure after mark_succeeded should not crash worker
2026-06-01 17:46:46 +03:00
Yiorgis Gozadinos
e9875fc842
Merge pull request #395 from mcdonc/perf/reuse-httpx-clients
perf: reuse httpx.AsyncClient in HTTP and WebDAV sources
2026-06-01 17:38:05 +03:00
Yiorgis Gozadinos
c5814b31bf
Test source clients close after the worker pool stops
Workers share the pollers' Source instances for fetch(), so the httpx
clients must be closed only after the pool has stopped. Guards both
run_batch() and serve() against reintroducing the shutdown-order bug.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 17:29:53 +03:00
Yiorgis Gozadinos
e3ea207358
Merge pull request #396 from mcdonc/perf/stagger-periodic-polls
perf: stagger periodic poll sweeps to avoid thundering herd
2026-06-01 17:23:22 +03:00
Yiorgis Gozadinos
b79bbcaed4
Merge pull request #399 from mcdonc/fix/discover-stat-race
fix: handle file deleted during discover() stat() call
2026-06-01 17:18:47 +03:00
Yiorgis Gozadinos
fda798a746
Merge pull request #391 from mcdonc/perf/event-driven-worker-wakeup
perf: event-driven worker wakeup via asyncio.Condition
2026-06-01 17:06:40 +03:00
Yiorgis Gozadinos
6544501123
Merge pull request #392 from mcdonc/perf/add-queue-indexes
perf: add partial indexes for has_pending() and dashboard queries
2026-06-01 17:03:57 +03:00
Chris McDonough
22ab79c492 Fix shutdown-order bug: close source clients after workers stop
Workers share the same Source instances as pollers and use them for
fetch(). PollerManager.stop() was closing httpx clients before the
worker pool drained, so in-flight fetches during the shutdown grace
hit a closed client.

- Move source closing out of stop() into a separate close_sources()
- Call close_sources() after _stop_pool() in both serve() and
  run_batch()
- Promote aclose() to the Source protocol with no-op defaults for
  FS and S3, removing the hasattr duck-typing
2026-06-01 09:55:39 -04:00
Chris McDonough
b144e620de Fix dead-worker condition and test for run_batch abort
The condition only checked claimed jobs, but queued jobs with no
live workers also hang forever. Check live_workers == 0 regardless
of whether outstanding work is queued or claimed.

Rewrite the test to actually crash workers: patch _process to raise
a bare Exception (which _worker_loop doesn't catch), use
worker_count=1 so the single crash leaves live_workers == 0, and
assert the abort log message fires.
2026-06-01 09:47:10 -04:00
Chris McDonough
004d59563c Extract _stagger_start helper into BasePoller, add tests
The jitter-before-first-sleep block was duplicated verbatim in
PeriodicPoller.run() and FSPoller._sweep_loop(). Move it to
BasePoller._stagger_start() with a named _STAGGER_FRACTION constant.

This also gives a testable seam outside the pragma-no-cover
event-loop glue methods.
2026-06-01 09:32:18 -04:00
Chris McDonough
2a06421e7a Improve discover stat race test to actually exercise the try/except
The previous test deleted a file mid-iteration, but is_file()
caught it before stat() ran — so the new try/except never executed.
Monkeypatch Path.stat to raise FileNotFoundError on the third call
for the victim path (after is_symlink and is_file pass), simulating
the exact TOCTOU window between is_file() and stat().
2026-06-01 09:28:26 -04:00
Chris McDonough
5010069474 Drop redundant idx_jobs_pending_by_source index
has_pending() already uses the leading column of uq_jobs_live
(source_id) with the same WHERE clause. The extra index just adds
write amplification on every insert/claim/complete without improving
reads.
2026-06-01 09:24:11 -04:00
Chris McDonough
720ff23357 Fix shutdown regression: notify idle workers on stop()
Workers parked on job_available.wait() were not woken by stop(),
causing them to sleep out the full poll_idle_interval_s before
noticing _stop. With the default 1.0s interval, stop() took ~0.8s
instead of ~0.007s.

Notify all waiters on the condition in stop() so idle workers exit
immediately. Add tests for fast job pickup via notification and
fast shutdown with idle workers.
2026-06-01 09:21:24 -04:00
Yiorgis Gozadinos
8bf1b6be1a
Merge pull request #403 from mcdonc/fix/watch-change-stat-race
fix: watch loop crash when file deleted before stat() in _handle_watch_change
2026-06-01 15:59:50 +03:00
Yiorgis Gozadinos
bf45c4efa8
Merge pull request #406 from mcdonc/fix/permissionerror-permanent
fix: classify PermissionError as PermanentError
2026-06-01 15:49:47 +03:00
Chris McDonough
3c7dbf0966
Classify PermissionError as PermanentError instead of TransientError
PermissionError is a subclass of OSError, so it was caught by the
broad timeout/io handler and classified as transient. An unreadable
file would retry 5 times then DLQ — permissions don't fix themselves
without operator intervention.

Add an explicit PermissionError check before the OSError catch so
unreadable files go straight to the DLQ.
2026-06-01 15:36:37 +03:00
Yiorgis Gozadinos
4372b0546d
Merge pull request #400 from mcdonc/fix/filenotfound-permanent-error
fix: classify FileNotFoundError as PermanentError
2026-06-01 15:35:03 +03:00
Chris McDonough
f33a4629e1 Handle sync_state write failure after mark_succeeded without crashing
If sync.upsert() or sync.delete() raises after a job is already
marked succeeded (e.g. disk full, DB locked), the unhandled
exception crashes the worker. The job stays succeeded but sync_state
is stale, and the crashed worker stops processing other jobs.

Wrap the post-success sync_state writes in a try/except. On failure,
log the error and continue. The worst case is a redundant re-ingest
on the next sweep — better than killing the worker.
2026-06-01 08:11:02 -04:00
Chris McDonough
da8e7dc568 Fix run_batch hanging forever when all workers die
The drain loop in run_batch() polls counts_by_status() waiting for
queued and claimed counts to reach zero. If all worker tasks crash
(unhandled exception, OOM), claimed jobs stay claimed forever and
the loop never exits — the CLI command hangs.

Check live_workers during the drain loop. If claimed jobs exist but
no workers are alive to process them, log an error and break out.
The stranded jobs will be reaped on the next start.
2026-06-01 07:52:17 -04:00
Chris McDonough
42922cd2bd Fix watch loop crash when file is deleted before stat() in _handle_watch_change
The expression `str(path.stat().st_mtime_ns) if path.exists() else None`
has a TOCTOU race: the file can be deleted between exists() and stat().
The resulting FileNotFoundError propagates up to _watch_loop's except
handler, which records a breaker failure and terminates the loop — no
more push events are processed until restart.

Replace with a try/except around stat() and return early on
FileNotFoundError. The deletion event from watchfiles will handle
cleanup.
2026-06-01 07:46:35 -04:00
Chris McDonough
c9c48bc814 Classify FileNotFoundError as PermanentError instead of TransientError
FileNotFoundError is a subclass of OSError, so it was caught by the
broad timeout/io handler and classified as transient. A file deleted
between discovery and fetch would retry 5 times on a file that's
permanently gone, then DLQ with a confusing error message.

Add an explicit FileNotFoundError check before the OSError catch so
deleted files go straight to the DLQ.
2026-06-01 07:32:09 -04:00
Chris McDonough
7d80eb4f83 Fix discover() crash when file is deleted during stat()
A file deleted between os.walk() and path.stat() raises
FileNotFoundError, which propagated uncaught and failed the entire
discover() sweep. With enough failures this trips the circuit
breaker, silencing the poller.

Catch FileNotFoundError around the stat() call and skip the file.
The next sweep (or watchfiles) will emit the DELETE event.
2026-06-01 07:29:51 -04:00