Commit graph

28 commits

Author SHA1 Message Date
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
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
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
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
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
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
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
a6b3e7f1f6 Stagger periodic poll sweeps to avoid thundering herd
All pollers sharing the same poll_interval_s previously woke up and
swept at exactly the same moment after startup. With 10+ sources
this causes a coordinated spike in listing traffic (S3 LIST, WebDAV
PROPFIND, HTTP HEAD) every interval.

Add a random initial delay of 0-25% of the poll interval after the
first sweep, applied to both PeriodicPoller and FSPoller's sweep
loop. Subsequent sweeps run on the normal fixed interval, now
staggered across sources.
2026-06-01 07:02:42 -04:00
Chris McDonough
07c5a97929 Reuse httpx.AsyncClient across requests in HTTP and WebDAV sources
HTTPSource and WebDAVSource previously created a new AsyncClient for
every head(), fetch(), and discover() call — no connection reuse, TLS
renegotiation on every request, and connection pool churn at scale.

Create the client once in __init__ and reuse it for the lifetime of
the source. Add aclose() to both sources, called by PollerManager on
shutdown to cleanly close the connection pool.
2026-06-01 07:00:15 -04:00
Yiorgis Gozadinos
5a23e4eda6
Surface failed discovery sweeps in run-batch 2026-06-01 10:27:31 +03:00
Yiorgis Gozadinos
5ba7838b71
Add haiku-ingester run-batch, remove run-once 2026-05-29 17:00:59 +03:00
Yiorgis Gozadinos
1e6dc34871
Skip FS DELETE enqueue when the file is already back 2026-05-27 15:18:46 +03:00
Yiorgis Gozadinos
b0d0ac588d
Split snapshot APIs and resolve_fetcher by intent 2026-05-27 14:39:54 +03:00
Yiorgis Gozadinos
fdb73fc41f
Drop defensive branches 2026-05-26 16:56:32 +03:00
Yiorgis Gozadinos
f89cc998eb
add sources_provider to WorkerPool. workers now resolve extra info through these sources 2026-05-26 11:45:35 +03:00
Yiorgis Gozadinos
73f8349a00
Use haiku.rag as telemtry scope 2026-05-26 11:44:47 +03:00
Yiorgis Gozadinos
ef9cacf981
Harden FS source against symlink escape; surface pool/poller liveness in /health, additional auth tests 2026-05-26 11:44:47 +03:00
Yiorgis Gozadinos
4aee18dcbe
Operator dashboard at GET /; tighten Logfire span shape
Self-contained HTML status page served from the ingester's FastAPI app.
Polls /health, /sources, /stats, /jobs?status={claimed,dead,succeeded}
every 3s from the browser and renders queue chips, sources with
last-poll/skip-reason/circuit state, active jobs with cancel, recent
failures with retry, and recently-completed feed with op badges so
DELETE rows are visually distinct from UPSERTs. Zero external deps —
single static HTML, no CDN, no fonts, no images. Works offline.

To support the dashboard:
- New /stats endpoint exposing rolling throughput (5m/30m/1h), worker
  occupancy, oldest-queued age, and per-source DLQ + queue-depth
  breakdowns. Each field is a single SQL aggregation against the queue.
- JobRepo gains count_succeeded_since, oldest_queued_age_seconds,
  counts_by_source.
- SourceSummary gains last_skip_reason. BasePoller now records the
  reason the most recent sweep attempt was skipped ("pending_work" /
  "circuit_open"), cleared on the next successful poll. Closes the
  gap where operators couldn't tell from /sources alone why a source
  wasn't picking up new work.

Auth: dashboard route is unauthenticated (markup only). The JS attaches
the bearer to its own JSON fetches; on 401 it prompts once and stashes
the token in localStorage.

Two Logfire fixes that landed alongside:

- Drop logfire.instrument_fastapi() and the [fastapi] extra. The control
  plane is polled frequently (dashboard + docker healthcheck), so every
  endpoint became a span and drowned the useful traces. logfire itself
  stays — pulled in transitively via pydantic-ai-slim[logfire] — so
  ingester.poller.* / ingester.job / document.* spans keep emitting.

- Wrap FSPoller._handle_watch_change in an ingester.poller.watch_event
  span and pass _enqueue_extra. Without this, the watchfiles callback
  ran with no active context, the _otel carrier in job.extra was empty,
  and the worker's ingester.job span surfaced as an orphan trace root
  instead of nesting under the FS event that caused it.
2026-05-26 11:44:47 +03:00
Yiorgis Gozadinos
c2f681dd78
constant-time auth, migration short-circuit 2026-05-26 11:44:46 +03:00
Yiorgis Gozadinos
ba623b0862
mark event-loop glue + defensive guards as no-cover 2026-05-26 11:44:46 +03:00
Yiorgis Gozadinos
5ccbadde0a
Handle shutdown more gracefully, by stopping pollers and cancelling jobs after timeout. Skip periodic poll if a source has pending jobs 2026-05-26 11:44:03 +03:00
Yiorgis Gozadinos
65c309e12b
Handle WebDAV as source 2026-05-26 11:43:30 +03:00
Yiorgis Gozadinos
fa672c298a
logfire spans for the ingester, nested poller/job/document traces 2026-05-26 11:42:42 +03:00
Yiorgis Gozadinos
1ca3c25a83
logging, sqlite cursors 2026-05-26 11:41:54 +03:00
Yiorgis Gozadinos
de3b3fa1c9
HTTP control plane 2026-05-26 11:41:54 +03:00
Yiorgis Gozadinos
75c3896588
additional config, pollers, serve 2026-05-26 11:41:54 +03:00