Update actor split idea file

This commit is contained in:
Hugh Kaznowski
2026-09-07 13:14:19 +01:00
parent c7173cf9dc
commit 5fee6909f2
3 changed files with 99 additions and 76 deletions
+2 -2
View File
@@ -5,7 +5,7 @@
<!-- REPOWISE_AGENTS:START — Do not edit below this line. Auto-generated by Repowise. -->
## Codebase Intelligence for ottyl (Repowise)
Indexed by [Repowise](https://repowise.dev). Last indexed: 2026-09-07 (commit dbe2ea9). Confidence: 100%.
Indexed by [Repowise](https://repowise.dev). Last indexed: 2026-09-07 (commit c7173cf). Confidence: 100%.
### How to work in this repo
- **Trust the index.** `verified: true` and `_meta.complete` mean the bytes were checked against the live tree, so never re-read them. Re-read only what `bounds: "approximate"` or `_meta.stale_warning` names. `confidence` rates the prose, not the evidence: on `low` read the `fallback_targets` or `best_guesses` the reply names, and run `repowise update` and ask again if `_meta.hint` says the index is behind HEAD. `index_behind: true` alone is informational.
@@ -70,7 +70,7 @@ Covers the 3 source files in 2 directories under…
- `webapp/src/components/Docs.tsx` — 1 bug fix, last fix today; 17 commits/90d
### Code health
Three co-equal signals: defect risk 5.95/10 avg, hotspot health 4.94/10 (stable), worst `src/engine/snapshot/mod.rs` at 2.0/10 · maintainability 7.48/10 · performance risk 16 open static I/O-in-loop / N+1 findings. Detail: `get_health()`.
Three co-equal signals: defect risk 5.95/10 avg, hotspot health 4.94/10 (declining), worst `src/engine/snapshot/mod.rs` at 2.0/10 · maintainability 7.48/10 · performance risk 16 open static I/O-in-loop / N+1 findings. Detail: `get_health()`.
Critical files:
- `webapp/src/components/LogPreview.tsx` — untested hotspot — impact −2.0
+2
View File
@@ -43,3 +43,5 @@ Frontend structure: `webapp/src/App.tsx` is the shell; `webapp/src/components/`
- **`tickets/`** holds scoped, written-up work items (problem statement, pointers to exact call sites with `file.rs:line` references, and an explicit scope section) — check here for planned/in-flight work before starting something that might overlap. Tickets are removed from this directory once implemented (see recent commit history — e.g. "Remove ticket 047 (implemented: ...)").
- Declaration-related features (params, metrics) consistently need three coordinated changes: an `EngineMessage` variant + handler in `engine.rs`, a persistence method in `settings.rs`/`profile.rs`, and an `api.rs` route wiring them together (plus a webapp UI affordance). Grep for an existing similar declaration (e.g. how metric declaration is wired) as the template before adding a new one.
- Doc comments in this codebase tend to explain *why* a non-obvious choice was made (e.g. ordering constraints, a workaround, a past ticket's rationale) rather than restating what the code does — match that style rather than adding descriptive comments.
- All markdown files (`*.md`) in this repo should follow one-sentence-per-line: each sentence starts on its own line, even within the same paragraph.
+95 -74
View File
@@ -26,6 +26,54 @@
> Values in the primary index should be arcs, to prevent cloning
> allocations.
## Resolved decisions (2026-09-07)
Clarified interactively; supersedes the open questions raised below where
they conflict.
- **No profiling gate.** Proceeding on the code-reading evidence already in
this doc (double/triple string clones per line, full-`EngineSnapshot`
deep-clone on every `left_right` reconciliation) rather than waiting on a
`samply` profile first.
- **Three actors, not two.** The split is: **reader actor** (tails
stdin/file, splits into lines, timestamps, wraps each line in an `Arc`,
sends downstream) → **primary actor** (ingests timestamped `Arc` lines,
extracts declared + auto params only, maintains the `param=value` index —
no metrics, no profile logic) → **profile actor** (holds its own
`left_right` reader over the primary's index; builds metrics, custom
params, timeseries, and PromQL evaluation for the one active profile).
This resolves the "per-profile message format" question below: there is
no per-profile parsing fork on the primary — format/shape parsing of an
already-classified line is entirely the profile actor's concern, on top
of the shared raw text.
- **Still exactly one active profile at a time.** No concurrent
multi-profile indexing; the split changes *how* the one active profile's
data is computed relative to the primary, not the user-facing
single-active-profile model.
- **Alerts move to the profile actor.** Alert rules are profile-scoped and
read metric buckets, so they move with metric computation rather than
staying on the primary.
- **Primary→profile signaling:** the primary actor notifies subscribers
(e.g. a `tokio::sync::watch` carrying the latest processed
position/index) right after each `left_right` sync completes. The
profile actor wakes on that signal and re-reads whatever's newly visible
through its own `left_right` reader — no busy-polling, no second copy of
the data in transit.
- **Eviction wins over profile-actor lag.** If the profile actor is
behind (e.g. mid-backfill on a newly declared metric) when the primary
evicts old lines (`SetMaxLogs`), the primary evicts on schedule
regardless — the profile actor silently misses data for the evicted
range. Simplest option, matches today's single-actor retention model.
- **System param suppressions: dropped from scope.** Not needed right now;
revisit as a separate idea if it becomes a real requirement.
- **Migration path: incremental**, in this order:
1. Arc-ify stored values (`Arc<str>`/`Arc<String>` for log text and
metric-captured strings) — independent win, and the load-bearing
precondition for cheap cross-actor reads in steps 2–3.
2. Extract metric computation into a separately-callable stage, still
within the single actor.
3. Split into the reader/primary/profile actors described above.
## Current architecture (confirmed in code)
- **One process, one actor, today.** `spawn_engine`
@@ -188,12 +236,16 @@
`left_right` pair. Splitting introduces: a defined "primary
perspective" (raw logs + derived/auto params only, no
profile-specific metrics) as its own bounded module, a
"profile perspective" abstraction that can be instantiated per
active profile, a notification/subscription mechanism from primary
to profile workers (the note's "profiles need to be informed of new
messages to index" — doesn't exist in any form today; the closest
analogue, `ingest.rs`'s producer tasks feeding one consumer, runs the
opposite direction), and a query-fan-out/merge layer for any request
"profile perspective" abstraction for the one active profile, a
notification/subscription mechanism from primary to the profile actor
(the note's "profiles need to be informed of new messages to index" —
doesn't exist in any form today; the closest analogue, `ingest.rs`'s
producer tasks feeding one consumer, runs the opposite direction).
**Resolved** (see "Resolved decisions"): the primary signals via e.g. a
`tokio::sync::watch` after each `left_right` sync completes, and the
profile actor re-reads through its own `left_right` reader rather than
the primary pushing data. A query-fan-out/merge layer is still needed
for any request
that needs both raw logs and profile-derived data in one response
(today `Logs`/`Timeline` queries read both from a single
`EngineSnapshot` in one pass, e.g. `query.rs:154`'s
@@ -202,10 +254,9 @@
across two snapshots/workers instead of one borrow).
- Alert evaluation (`handle.rs:199-211`) currently depends on running
immediately after metric bucketing in the same message, against the
same snapshot. If metrics move to per-profile workers, alerts need to
either move with them (probably correct, since alert rules are
profile-scoped conceptually) or gain a defined way to read
cross-worker metric state that doesn't exist today.
same snapshot. **Resolved** (see "Resolved decisions"): alerts move
with metric computation into the profile actor, since alert rules are
profile-scoped and read the metric buckets computed there.
- `EngineHandle`'s current API (`handle.rs:49-73` for reads,
the mpsc `EngineMessage` send for writes) would need to become two
APIs, or one handle type parameterized by "primary" vs. "profile" —
@@ -222,75 +273,45 @@
### Functionality
- **Per-profile message format** (the note's "What if message needs to
be different format? Thats per-profile.") — today, log-stream header
detection and logfmt/JSON auto-param extraction happen once, in the
primary, against the raw line (`derive_log_stream_params_from_row`,
`auto_declare_params`, both in the current `Ingest` arm). If
different profiles can expect genuinely different message shapes
from the *same* piped process (unclear from the note whether this
means "different profiles parse the same stream differently" or
"each profile only ever sees its own stream"), that's a real design
fork: either the primary's parsing must become profile-aware (which
undercuts "primary only extracts params" as a clean, profile-agnostic
boundary), or each profile worker does its own message-format
parsing independently on top of the shared raw text — the latter fits
the stated architecture better but means format detection isn't a
primary-side concern at all, just param extraction of an
already-classified line is.
- **System param suppressions** (the note's fragment) is undeveloped —
unclear whether this means "some auto-derived params should never be
promoted to profile-visible params" (a filtering rule between primary
and profile perspectives) or "a way to suppress a param a profile
inherited but doesn't want" (a per-profile override). This needs a
sentence or two from you before it can be scoped at all; flagging
here rather than guessing.
- Multiple simultaneously-active profile workers (implied by "a
different worker" being per-profile, plural) raises a question the
current single-profile-at-a-time model never had to answer: can two
profiles be "active" and indexing concurrently, or is it still
strictly one active profile with the split only changing *how* that
one profile's data is computed relative to the primary? The
perf/functionality tradeoffs are very different depending on the
answer (concurrent multi-profile indexing multiplies the backfill
and per-line cost discussed above by the number of concurrently
active profiles).
- **Per-profile message format** — **resolved**, see "Resolved decisions"
above: there's no per-profile parsing fork on the primary. Log-stream
header detection and auto-param extraction stay a one-time, primary-side,
profile-agnostic concern (`derive_log_stream_params_from_row`,
`auto_declare_params`); any further message-shape interpretation the
profile actor needs happens independently, on top of the shared raw
`Arc` text, in the profile actor itself.
- **System param suppressions** — **dropped from scope** (see "Resolved
decisions"). Revisit as a separate idea if it becomes a real need.
- **Concurrency** — **resolved**: still strictly one active profile at a
time. The split changes *how* that one profile's data is computed
relative to the primary, not the user-facing model — no multi-profile
concurrent indexing.
### Not yet considered
- **Failure/restart semantics**: if a profile worker crashes or lags
behind the primary, what's the recovery story? Today there's one
actor with one failure mode (the whole engine). A split needs an
answer for "profile worker is behind — does it catch up from the
primary's retained history (bounded by the same retention limit the
primary enforces), or is data permanently lost for the gap?"
- **Retention/eviction coordination**: the primary already evicts old
logs (`evict_excess_logs`, `handle.rs:195`, `SetMaxLogs` message).
If a profile worker is reading "the primary log sequence" lazily or
asynchronously, eviction on the primary could remove a line before a
lagging profile worker has processed it — there's no coordination
primitive today (like a per-consumer low-water-mark) to prevent this
silently dropping data for a slow profile worker.
- **Failure/restart semantics** — partially resolved: the profile actor
falling behind the primary during a backfill is expected and accepted
(see "Resolved decisions" — eviction wins over lag, silent gaps for the
evicted range). Still open: what happens if the profile actor *crashes*
outright (not just lags) — does it restart and replay from whatever the
primary currently retains, or is there a persisted checkpoint? Not yet
decided; likely fine to punt until the split is actually implemented,
since "restart and rebuild from currently-retained history" is a
reasonable default consistent with the accepted-gaps decision above.
- **Retention/eviction coordination** — **resolved**, see "Resolved
decisions": no low-water-mark. The primary evicts on its own schedule
(`evict_excess_logs`, `handle.rs:195`, `SetMaxLogs`) regardless of the
profile actor's progress; a lagging profile actor silently misses data
for the evicted range rather than blocking primary eviction.
- **Testing story**: `src/engine/tests.rs` (2348 lines) and the
`test_handle` pattern (see ticket 099) are built entirely around one
actor with one channel — a split roughly doubles the kinds of
integration tests needed (primary alone, profile worker alone,
primary+profile interaction) and the existing test harness would
need real rework, not just extension.
- **Migration path**: this is a rewrite of the highest-risk, largest,
most-churned file in the codebase (per `CLAUDE.md`'s repowise
summary) — no plan yet for whether this ships as one big change or
incrementally (e.g. Arc-ing values first as an independent,
lower-risk step; then extracting metric computation into a
separately-callable-but-still-single-actor stage; then finally
splitting into separate tasks) — given the size, an incremental path
is very likely necessary rather than optional.
- **Whether this is worth it at all without profiling evidence**: per
this project's own established norm (tickets 079/080 were removed
for "no profiling evidence to justify the fix"), this is a large
structural bet motivated by the note's stated concern ("primary
worker is doing too much") rather than a measured bottleneck — worth
a `samply` profile of realistic ingest load *before* committing to
this, to confirm metric-bucketing-on-the-primary is actually a
measurable cost relative to param extraction, storage, and alert
evaluation, rather than assuming it from code-reading alone.
need real rework, not just extension. Still open — no resolution yet.
- **Migration path** — **resolved**, see "Resolved decisions": incremental,
in three steps (Arc-ify stored values → extract metric computation into
a still-single-actor stage → split into reader/primary/profile actors).
- **Whether this is worth it at all without profiling evidence** —
**resolved**, see "Resolved decisions": proceeding without a `samply`
profile, on the code-reading evidence already documented in this file.