Repository navigation
Description
Problem
tail.rs is still the clearest example of why we want the full correct-by-construction architecture. It is a large stateful subsystem whose correctness depends on file lifecycle transitions, event ordering, identity rules, bounded reads, and checkpoint semantics, yet much of that logic is still concentrated in one monolithic file.
This issue is not just about splitting a file. It is about turning the tailer into a subsystem with an explicit state model and verifiable invariants.
Target Architecture
crates/logfwd-io/src/tail/
├── mod.rs # Public API: FileTailer and tail-facing event types
├── discovery.rs # Glob expansion, directory watching, rescan, duplicate suppression
├── reader.rs # Per-file IO, truncation, EOF, budgeted reads, eviction
├── identity.rs # FileIdentity, SourceId derivation, fingerprint policy
├── state.rs # Explicit file lifecycle state model / typestate-adjacent core
├── config.rs # TailConfig with validated defaults and fairness controls
└── tests/ # Domain-focused regression/proptest/model tests
Desired end-state
- illegal lifecycle combinations are removed by design, not merely guarded by comments and booleans
- per-file lifecycle is explicit: discovery, activation, draining, eviction, reopen, truncation, deletion
- fairness and bounded-read behavior are deliberate policy, not incidental iteration order
- identity and checkpoint rules are documented and testable
- the downstream tail->framing contract is preserved and made explicit
Sub-tasks
- Modularize the tailer — split the monolith into coherent modules without losing current behavior
- Introduce an explicit state model — typestate where it materially pays off, or an equally rigorous explicit runtime state machine where async/IO edges make compile-time typestate too awkward
- Per-file fairness and bounded reads — make per-file budget/fairness policy explicit and configurable
- Identity policy hardening —
(dev, inode, fingerprint)and empty-file/reopen/eviction semantics are explicit and tested - Truncation / rotation / EOF contract hardening — event ordering is documented and regression-tested end-to-end
- Proof/test coverage — Kani for pure identity/transition helpers, proptest for randomized file-operation sequences, targeted regression tests for event semantics
Design stance
The destination here is ambitious: we do want the tailer to feel "correct by construction" rather than merely "less bad than before."
That does not require pretending every async/filesystem interaction should be encoded in type parameters. The point is to make invalid states and invalid transitions difficult or impossible to represent, while keeping the implementation readable and verifiable.
Related issues fixed or absorbed
Fixes or absorbs the architectural dimension of:
- bug: TailEvent::Truncated never emitted — copytruncate corrupts remainder #796
- bug: shared remainder buffer across files — cross-file data corruption #797
- bug: fingerprint collision causes checkpoint misattribution #798
- bug: glob rescan double-opens files via non-canonical paths #799
- bug: read_new_data allocates unbounded Vec — OOM on large files #800
- enhancement: per-file read fairness budget #801
Relates to:
Activity
github-actions commentedon Apr 6, 2026
tl;dr: Outcome A — ready for implementation.
crates/logfwd-io/src/tail.rsis still a 2326-line monolith, and the remaining work is now a mechanical extraction + typestate pass with explicit per-file state transitions.Recommendation
Proceed with a behavior-preserving refactor in
logfwd-iothat:tail.rsinto focused modules,This is implementable without new design decisions.
Findings
crates/logfwd-io/src/tail.rs:1(2326 lines in tree). Public orchestrator and internal layers are still in one file:FileDiscoveryattail.rs:223FileReaderattail.rs:412FileTailer::pollattail.rs:927eof_emittedinTailedFile(tail.rs:56-66) and transitions attail.rs:501,tail.rs:616,tail.rs:638,tail.rs:659-661.MAX_READ_PER_POLLis 4 MiB (tail.rs:430), while this issue calls for 256 KiB/file/poll.glob::globpath expansion (tail.rs:201) and dependencyglob = "0.3"incrates/logfwd-io/Cargo.toml:17(noglobset/walkdiryet).FramedInput.sourcesper source atcrates/logfwd-io/src/framed.rs:54-60,SourceStateatframed.rs:33-41.tail.rs:1966+,tail.rs:2027+,tail.rs:2110+).Verification
I ran targeted tailer tests:
Detailed Action Plan (mechanical)
Split module layout (move-only first, no behavior change)
crates/logfwd-io/src/tail/mod.rscrates/logfwd-io/src/tail/discovery.rscrates/logfwd-io/src/tail/reader.rscrates/logfwd-io/src/tail/identity.rscrates/logfwd-io/src/tail/config.rsFileIdentity(tail.rs:26-52) ->identity.rsTailConfig(tail.rs:131-163) ->config.rsFileDiscovery(tail.rs:223-402) ->discovery.rsFileReader(tail.rs:412-812) ->reader.rsFileTailer+ public API (tail.rs:819-1025) ->mod.rspub enum TailEventshape unchanged (tail.rs:91-128).Introduce per-file typestate (replace boolean lifecycle flags)
reader.rs, replaceeof_emitted: bool(tail.rs:65) with a small state enum that encodes legal transitions (e.g.,Active,EofReported,Draining,Evicted).tail.rs:496-506)tail.rs:612-646)tail.rs:655-667)TruncatedbeforeData(tail.rs:628-645)tail.rs:655-667)Apply per-file read budget target
FileReader::MAX_READ_PER_POLLfrom 4 MiB (tail.rs:430) to 256 KiB.tail.rs:2027+) to match new cap.Discovery stack follow-up for dependency alignment
glob::globexpansion path (tail.rs:201) with the approvedglobset + walkdirapproach (dependency issue linkage in File tailer refactor — split monolith, typestate, per-file correctness #1310).crates/logfwd-io/Cargo.tomldependencies accordingly.Tests to add/update
tail.rs(or move totail/tests.rs) and add:NoData -> NoData -> Data -> NoData,crates/logfwd-io/tests/it/checkpoint_state_machine.rs.Verification command for implementation PR
cargo test -p logfwd-io tail::tests::cargo test -p logfwd-io --test it checkpoint_state_machinecargo clippy -p logfwd-io --all-targets -- -D warningscargo fmt --all --checkRelated Items
globset/walkdir)logfwd-iotest/contract work in adjacent areacrates/logfwd-io/src/tail.rs:223FileDiscoverysplit anchorcrates/logfwd-io/src/tail.rs:412FileReadersplit anchorcrates/logfwd-io/src/tail.rs:927FileTailer::pollorchestration anchorcrates/logfwd-io/src/framed.rs:54Note
🔒 Integrity filtering filtered 8 items
Integrity filtering activated and filtered the following items during workflow execution.
This happens when a tool call accesses a resource that does not meet the required integrity or secrecy level of the workflow.
issue_read: has lower integrity than agent requires. The agent cannot read data with integrity below "approved".)search_pull_requests: has lower integrity than agent requires. The agent cannot read data with integrity below "approved".)issue_read: has lower integrity than agent requires. The agent cannot read data with integrity below "approved".)issue_read: has lower integrity than agent requires. The agent cannot read data with integrity below "approved".)issue_read: has lower integrity than agent requires. The agent cannot read data with integrity below "approved".)issue_read: has lower integrity than agent requires. The agent cannot read data with integrity below "approved".)issue_read: has lower integrity than agent requires. The agent cannot read data with integrity below "approved".)issue_read: has lower integrity than agent requires. The agent cannot read data with integrity below "approved".)What is this? | From workflow: Issue Triage
Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.
strawgate commentedon Apr 6, 2026
Updated this issue to describe the full tailer architecture target: a real subsystem with an explicit lifecycle/state model, fairness/identity/checkpoint semantics, and proofs/tests, not just a file split.
strawgate commentedon Apr 6, 2026
Two concrete cleanup slices from the 2026-04-06 fan-in map directly under this tailer track:
Execution meta: #1450
The fan-in recommendation was to land these hardening slices before any larger seam movement under #1310.
strawgate commentedon Apr 6, 2026
Repo-wide cleanup pass: I split the unique discovery glob bug out of #1422 into #1453. That keeps this architecture umbrella focused on the broader tailer subsystem work while the concrete cwd-relative glob bug gets a leaf of its own.
strawgate commentedon Apr 11, 2026
Partial-resolution status update (April 11, 2026):
Landed toward this architecture:
Remaining scope to keep this issue open:
Recommendation:
strawgate commentedon Apr 11, 2026
Partial-resolution refresh (April 11, 2026, post-merge wave):
Additional landed work since the prior update:
fix(io): WS06+WS07+WS08) directly hardened tailer correctness seams incrates/logfwd-io/src/tail/{discovery,reader,tailer,tests}.rs.mainkeeps the tailer modularized undercrates/logfwd-io/src/tail/with dedicatedidentity.rs,state.rs, andverification.rs.Assessment:
docs: add architecture research fanout prompts for 5 workstreams
docs: add architecture research synthesis memo
strawgate commentedon Apr 24, 2026
Deep-Dive Research: File Tailer Refactor
Current State (Code-Verified)
7 modules in
crates/logfwd-io/src/tail/:tailer.rsFileTailer: poll loop, glob rescan, error backoff, adaptive pollingdiscovery.rsFileDiscovery: notify watcher, glob expansion, change detectionreader.rsFileReader: open files, read data, LRU eviction, fingerprint matchinglifecycle.rsActive,EvictedClosedCached+ scaffolded but unwired:Untracked,DiscoveredUnopened,DeletedCleanupPending,TerminalRemovedstate.rsidentity.rsFileIdentity(device, inode, fingerprint),ByteOffsetglob.rsKey finding:
lifecycle.rshas#![allow(dead_code)]— the typestate transitions are scaffolded but not wired. OnlyActiveandEvictedClosedCachedare used. TheSession<S>wrapper exists but transitions aren't implemented.Gaps
FileReader/FileDiscoverymethods, not through typed state machine. Invalid transitions not caught at compile time.FileDiscovery::detect_changes()mutatesFileReaderdirectlypoll()does too much — ~60 lines: drain events → backoff check → poll interval → glob rescan → detect changes → read all → cleanup → evict → update backoffhad_erroris a single boolean ORed across all operations. One file's error backoffs entire tailer.EofStateusesInstant::now()directlyRecommended Implementation (All Three Complement)
had_error: boolwithHashMap<PathBuf, ErrorState>. Backoff per file, not per tailer. Immediate correctness win.poll()intoDrainEvents → GlobRescan → DetectChanges → ReadAll → Cleanup → Evict. Each phase is a pure function(&mut TailerState, &Config) → PhaseResult.Session<Active> → Session<EvictedClosedCached>etc. Make illegal transitions compile errors. The scaffolding inlifecycle.rssuggests this was always the intent.Key Files
tailer.rs,lifecycle.rs,reader.rs,discovery.rs,state.rs,tla/TailLifecycle.tlaComplexity: L (each individual option is M)
strawgate commentedon Apr 25, 2026
Research: File Tailer Typestate Patterns (2026-04-24)
Current State
Tailer modularized into 7 modules in `tail/`. Typestate scaffolding exists but is unused — `lifecycle.rs` defines `Session
` with 6 state types, but is `#[allow(dead_code)]`. Only `Active` and `EvictedClosedCached` are used as type aliases. Verification is strong for pure reducers (TLA+, 7 Kani proofs, 2000-case proptest) but does not cover the file lifecycle state machine.External Patterns
Recommended: Hybrid Typestate
Typestate for transitions (compile-time enforcement):
```rust
impl Session {
fn read(self, ...) -> Result<(Session, ReadResult), Session>;
fn evict(self) -> Session;
fn detect_truncation(self) -> Session;
}
```
Runtime enum for storage (HashMap needs homogeneous values):
```rust
enum FileState { Active(Session), Evicted(Session), ... }
```
What current code allows that shouldn't be possible
Implementation Phases
Sources: Cliffle typestate blog, Microsoft Rust Patterns, Hoverbear state machine patterns, Vector file source docs, Filebeat harvester docs.
strawgate commentedon Apr 25, 2026
Mechanical Implementation Spec: File Tailer Typestate Phase 1 — 2026-04-24
Summary
Wire the existing
Session<S>scaffolding inlifecycle.rs(currently#[allow(dead_code)]) with typed transition methods. Use them inevict_lruandopen_file_at. No behavior change — pure type safety improvement. HashMap structure unchanged.Files Changed (2)
crates/logfwd-io/src/tail/lifecycle.rs#![allow(dead_code)]. AddFileStateenum,Session<Active>::evict(),Session<EvictedClosedCached>::restore(), accessor helpers. Add 3 unit tests (evict preserves fields, restore preserves fields, round-trip).crates/logfwd-io/src/tail/reader.rsuse FileState, Session. Rewriteevict_lruto useSession::new(tailed).evict(path, source_id).state. Rewriteopen_file_atevicted-match branch to useSession::new(evicted).restore(file, offset).state. Eliminate 4 mutablestored_*variables.Key Transitions
What is NOT in Phase 1
FileStateenum used in HashMap storage (still two separate maps)discovery.rs,tailer.rs, or test filesTailedFileandEvictedFiletype aliases preservedVerification
Full spec with exact old→new code for
evict_lruandopen_file_at: see research thread above.