Remote data too large for one request¶
Which component shows it, how it is paged, and why. This is the reasoning behind CLAUDE.md rule 34. The decisions and their rejected alternatives live on the wayfinder map "Map: choosing components for large remote data" (#70), and the build is specified in #81.
Status. Everything here exists:
pkg/eventlog,source.Anchored,source.Model'sMaxHeld/Follow/Find,pkg/table's span mode (Options.Anchored),pkg/resume, andlogview.Prepend/AppendMarker. Each shape has an example:examples/patterns/eventlog(Seekable — an AWX client, against demoapi's AWX-shaped endpoints over HTTP),examples/patterns/anchored(Anchored — asearch_afterclient against demoapi's Elasticsearch-shaped_search) andexamples/patterns/podlogs(Streamed, pod logs).
Classify the data first¶
Two questions decide everything: what an item is, and how an item can be
reached. The terms are in CONTEXT.md.
What an item is
| Meaning | Examples | |
|---|---|---|
| Record | fields the user compares across items | Prefect flow runs, Argo apps, most REST lists |
| Event | fields in a timeline read in order | AWX job events, Elasticsearch log documents |
| Line | raw text read in order | pod logs, Prefect log messages |
How it is reached
| Meaning | Examples | |
|---|---|---|
| Seekable | item N can be fetched directly; the total is known or grows | offset/limit endpoints, AWX counter ranges |
| Anchored | only reachable by walking forwards or backwards from an anchor; no reliable total | Elasticsearch search_after, Prefect logs/filter by timestamp |
| Streamed | only the tail, then whatever arrives | Kubernetes pod logs |
Plus one flag, Growing: items keep arriving at the end while the user watches.
Then pick the component¶
| Seekable | Anchored | Streamed | |
|---|---|---|---|
| Record | table + SetWindow + source.Model |
table span + source.Anchored |
table + SetKeyedRows from a watch |
| Event | eventlog + inspector on enter |
eventlog + source.Anchored + inspector |
logview + inspector |
| Line | eventlog |
eventlog + source.Anchored |
logview + pkg/resume |
Three rules sit on top:
- If it fits in one request, don't page it. A finished AWX job whose
output is under 1 MiB goes in a
textview; a short list goes throughSetRows. Paging machinery earns its keep only when the data doesn't fit. - Events never go in a table. A timeline is read in order. Sorting and comparing across rows are what a table is for, and neither applies. The fields are one enter away, in an inspector.
- Search jumps, filter narrows. A filter changes the query, so fewer items come back. Search finds the next match and moves the view there, with its surrounding context intact. Over remote data this matters more, not less: a filtered result loses the lines around each hit, which is usually what you were looking for.
The sources¶
Prefect: flow runs and logs¶
- Flow runs are Record / Seekable.
POST /flow_runs/filtertakes offset and limit, with/flow_runs/countand/flow_runs/paginatefor totals. Usetable+source.Model+pkg/poll. - Keep
PageSizeat or belowPREFECT_SERVER_API_DEFAULT_LIMIT(200). A larger limit is refused with a 422 rather than trimmed. - Logs are Line (or Event) / Anchored / Growing, in an
eventlogoversource.Anchored. POST /logs/filterreturns a bare array with no total, sorted by timestamp only, with no tiebreaker.- Offsets are unstable while a run is live: workers send logs in batches about every 2s, stamped when they were created, so a new batch can land in the middle of timestamp order and shift every later offset.
- Anchor at Newest (sort descending, then reverse). Follow by polling
timestamp.after_with a few seconds of overlap, and drop duplicates by log id. - Keep polling briefly after the run finishes: late batches still arrive.
- The logs websocket is not a data channel.
WS /logs/outis off by default, never replays history, and silently drops logs once a subscriber's 256-entry queue is full. At most it is a hint to poll sooner (Poke).
AWX: job events¶
- Job events are Event / Seekable / Growing, in an
eventlogoversource.ModelwithMaxHeld. - ansible-runner numbers events 1, 2, 3… with no gaps at the source, so
order_by=counter&counter__gte=a&counter__lte=bfetches any range. - The total is the highest counter (
order_by=-counter&limit=1), and it grows while the job runs. - Holes appear mid-run because several workers save events out of order.
- A short reply means "not saved yet", not "the end". Draw holes as placeholders and refetch them while Growing.
- The final count only settles once
event_processing_finishedis true. - Always send
order_by=counter. The default order (start_line) has ties. - Events carry 0–n output lines. The item is the event, so windows count events and positions stay stable as output expands.
- The stdout endpoint can't page a large job. Past 1 MiB, every format
except
*_downloadreturns a "too large" message. It suits only a finished, small job, in atextview. - The websocket is a hint.
- It carries
counterin ajob_eventsgroup, but it is lossy (about 30 messages a second at most), drops events with no output, and can't resume after a disconnect. - Use it to
Poke;jobs-summarycarriesfinal_counter.
Elasticsearch (ECK): log documents¶
- Log documents are Event / Anchored / Growing, in an
eventlogoversource.Anchored. from/sizestops atindex.max_result_window(10k). Beyond that the only way in issearch_afterwith the full sort values.- Always sort with a unique tiebreaker. Under a point in time (PIT),
_shard_docis added automatically. - To page backwards, reverse every sort, pass the first row's sort values, and reverse the hits client-side.
- The cursor is the sort values encoded as JSON, and it is opaque to the library.
- Totals are an exact count or "at least N".
track_total_hitscounts to 10k by default. Most log views turn totals off. - A PIT stabilises scroll-back but cannot tail.
- It is a snapshot, so extend
keep_aliveper page andDELETEit when done. - Tailing is polling
search_afterforward without a PIT, rewinding for documents that arrive late (refresh is about 1s) and dropping duplicates by_id. - The gutter shows
@timestamp, the server's time for each document, asItem.Mark— the same column Kibana's Discover leads with, and stable as pages arrive above it (there are no positions to show). It is never the time the TUI fetched anything. If your messages carry the application's own timestamp, leaveMarkempty rather than show two times for one event; and note@timestampmay be ingestion time unless the pipeline parses the app's time into it. - ECK changes only the connection.
- The service is
<name>-es-http:9200, with TLS by default. - The CA is in
<name>-es-http-certs-public(tls.crt), and the elastic user's password is in<name>-es-elastic-user.
Kubernetes: pod logs¶
- Pod logs are Line / Streamed / Growing, in a
logviewwithpkg/resume. - There is no pagination. The endpoint streams the newest log file, about 10 MiB with the kubelet defaults.
- "Load older" means asking again with a larger
tailLines, which re-downloads every newer line. So it is an explicit action, never triggered by scrolling. - Resume is by
sinceTime, truncated to the second, so a naive reconnect repeats lines or loses them. - stern resumes exactly by always requesting timestamps, remembering the last second and how many lines it held, and skipping that many.
pkg/resumeis that algorithm, generic and without I/O.- Rotation loss is undetectable. A resume that lands after the file rotated returns nothing for what went into the rotated file, and a quiet container looks the same. Mark every reconnect in the log instead of claiming there was or wasn't loss.
- Containers and runs.
- Follow ends when the container stops; watch the pod to pick up the next run.
previous=truereaches back one run, shown as its own view, never interleaved.- Several containers means one stream each, tagged with pod and container.
Generic REST¶
- Most list endpoints are Record / Seekable:
table+source.Model, as in rule 29. - Cursor-paginated lists that can only go forward are Record /
Seekable-by-cursor (
ByCursor, which accumulates a growing window). - Lists that page both ways from a token are Record / Anchored: a
tablespan oversource.Anchored.
What carries over from tables¶
Everything built for slow remote tables (#69, rule 29) applies to the
eventlog and to anchored spans, because they share a windowing core with
pkg/table:
- Waiting: stale rows dimmed under a
showing X · ⠙ loading Ysuffix while a query is in flight, and Loading only before the first answer. - Staying cheap: scroll requests wait for the viewport to settle, a
superseded request is cancelled through
Query.Ctx, and nothing is paged while the committed query is unanswered. - Reporting: the query history (answered, failed, cancelled) goes to the
output console, and the border shows
⠙ loading rows …orloading older… / newer…while data is missing.
What is new for timelines:
- Follow: pinned at the newest item. New items are counted, not shown,
once the user moves away (
↓ 42 new). The source polls; when not following, a poll is a probe that counts without fetching. - Late items: merged by key where the page puts them, without moving the view.
- Remote find:
n/Nsearch resident items first, then ask the server for one match past the edge and jump or re-anchor there. - Spans: Anchored data is held as a span grown at either edge, trimmed at the end furthest from the viewport past 5,000 items.
Research¶
Each source's findings are summarised in the resolution comment of its research ticket, with primary-source links:
- AWX job events and stdout (#71)
- Elasticsearch paging and tailing (#72)
- Prefect logs and flow runs (#73)
- Kubernetes pod logs (#74)