Files
sift/docs/plans/p6/README.md
T
kyleandClaude Opus 5.5 218eaa8b53 P6 plan, owner review folded in: caller travels with the job, 1–2 round-robin workers, busy retry, summary reads, local-only tests, 8 tasks
Kyle: "Fold them into the plan." What changed and why is in docs/P6.md §14. In short:
- FetchFunc gets the *auth.Caller (the given api test could not compile, and a fetch with no
  caller would bypass per-caller gates, the pool limit, the cache class and redaction);
- store reads never bump the lease (task 01's rule contradicted its own test); the API Touches;
- 1-2 workers round-robin over live jobs, so a batch holds at most 2 of the 8 global slots;
  busy targets retry in the runner (bounded); batch PDFs use their own pool name; 210 s deadline;
- the api tests no longer touch the network or race the cancel; private names via fake DNS;
- failed/cancelled jobs are not reused; GET is a summary plus GET …/{idx} and ?results=1;
- the 256 KiB body cap moves into ServeHTTP; a fixed metrics label for /v1/batch/{id};
- guard.PreCheck (given verbatim) replaces guard.ParseURL at submit;
- 5 tasks became 8 (refactor, wiring, endpoints and serve/config split), each restating the
  APIs it uses; every given test compiles against a panic-only skeleton and fails on it.

P6 starts after P5c: task 04 moves v1fetch, which P5b changes. Recheck the api given tests
against post-P5c master before the run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 17:55:50 -07:00

59 lines
3.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# P6 implementation plan: asynchronous batch fetch
> **For the implementing model:** do not work from this file. The owner gives you one task file
> at a time. This file is the index for the owner and the reviewer.
**Goal:** an agent that has collected a series of targets (URLs) and wants them fetched without
holding a request open per target submits them as one batch, gets a job ID back immediately, and
polls for per-target results later. A batch is **deliberate and slow** — serial, one target at a
time — so it is the tool for research threads, not for interactive fetches.
Decisions (settled with the owner, 2026-09-26; revised after review the same evening, see
`docs/P6.md` §14):
- **Serial per job, few workers.** One worker by default (at most 2) drains all live jobs,
round-robin, one target at a time. The batch therefore holds at most 1–2 of the 8 global fetch
slots, so interactive requests are never starved. A target refused busy (fetch gate or PDF
pool) is retried after a pause, for up to 10 minutes, then failed.
- **The caller travels with the job.** The runner keeps each job's `*auth.Caller` in memory and
hands it to every fetch, so per-caller gates, the PDF pool, the digest cache class and
restricted-caller redaction all apply. Nothing about the caller is persisted beyond its name
and class: jobs never resume after a restart.
- **Caps.** targets/job 32; live jobs/caller 1; live jobs total 8; body 256 KiB. All instrumented
(`sift_batch_*`).
- **Idempotency in v1.** The same (caller, target list) returns the existing job while it is live or
has results (`queued`/`running`/`done`/`partial`) and its lease holds. A failed or cancelled job
is not reused.
- **Storage.** `batch_job` + `batch_target` tables in the existing `dcache` SQLite, with its own
byte budget (64 MiB), outside the content cache's LRU.
- **Retention / TTL.** One sliding lease per job, bumped on submit, on every target write, and on
every owner read (the API calls `Touch`; store reads never write). GC deletes a job when its
lease is older than the TTL (24 h).
- **Reads.** `GET /v1/batch/{id}` is a summary (statuses, errors). A result is read one at a time
(`GET /v1/batch/{id}/{idx}`), or all at once with `?results=1`. 32 inline results can be 6 MB.
- **Restart.** `queued`/`running` jobs are marked `failed` (`restart`) on startup; never resumed.
- **Restricted callers.** Their fetches go through the same `fetchTarget` with the real caller,
so the existing redaction applies. No caller can see another's job (404, never 403). Batch
log lines never carry a target URL or hostname.
| Task | Package | Given |
|---|---|---|
| 01-batchstore | `internal/dcache` | `dcache/batch_test.go` |
| 02-runner | `internal/batch` | `batch/runner_test.go` |
| 03-metrics | `internal/metrics` | `metrics/batch_test.go` |
| 04-fetchtarget | `internal/api` (the one refactor of working code) | `api/fetchtarget_test.go` |
| 05-batch-wiring | `internal/api` (+ `guard.PreCheck`, given verbatim) | `api/batchrun_test.go`, `guard/precheck_test.go` |
| 06-api-endpoints | `internal/api` | `api/batch_test.go` |
| 07-serve-config | `internal/config`, `cmd/sift/serve.go` | `config/batch_test.go` |
| 08-cli-docs | `cmd/sift`, README, skill | `cmd_sift/batch_test.go` |
Given files live under `_files/<package>/` (`cmd_sift` is `cmd/sift`); each task says where to
copy its file. Every given test was compiled against a panic-only skeleton of the whole plan and
fails there on `panic: todo`, not on a compile error.
**How this plan was made:** design worked through with the owner from `docs/PLAN.md` §13 (future
work) on top of the P5 pool (`internal/pdfpool`), cache (`internal/dcache`), and gate
(`global`/`per` in `internal/api`) infrastructure. Acceptance tests first: the given tests are
authored against this spec and compiled against a panic-only skeleton so they fail for the
intended reasons before each task is implemented.