Skip to content

fix(observability): bound repeated durable-append failure reports - #97

Open
oxoxDev wants to merge 3 commits into
tinyhumansai:mainfrom
oxoxDev:fix/observability-append-report-bound
Open

fix(observability): bound repeated durable-append failure reports#97
oxoxDev wants to merge 3 commits into
tinyhumansai:mainfrom
oxoxDev:fix/observability-append-report-bound

Conversation

@oxoxDev

@oxoxDev oxoxDev commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Summary

AppendWorker's drain loop reports every failed durable append with eprintln! — stateless, once per item, with no level and no way for the host application to filter it. Under a persistent sink failure (a volume flips read-only, a disk fills) every queued observation emits the identical line, and the host's stderr becomes that one line repeated.

This is the crate's only eprintln!. Every other diagnostic already goes through tracing with a target (e.g. tinyagents::embeddings::ollama), so an embedder can filter everything except this. The fix removes an outlier rather than introducing a logging regime.

Failed appends were also uncounted. Queue-full drops have the dropped counter, but an append that fails after dequeue was lost with no counter at all — so the only signal that durable-log data was being lost was the noise.

The prompting incident was downstream: a 200-line log tail from a live tenant contained this line and nothing else, so an investigation into an unrelated problem could see nothing.

One correction to how this is usually described, in case it shapes review: there is no retry loop here. Each item is attempted once and dropped on Err. The flood is one line per arriving item, not per retry — so the fix is report-collapse plus a counter, not attempt-suppression. The worker deliberately keeps attempting while degraded: attempts are what detect recovery, they run off the critical path on the dedicated drain thread, and skipping them would turn a transient blip into guaranteed total loss of everything queued behind it.

API Or Behavior Changes

Behaviour change, stated up front: the stderr line disappears. An embedder with no tracing subscriber installed now sees nothing on append failure. That is the intended contract — the host should control this like every other tinyagents event — but it is silent-by-default for anyone who was relying on the old output, so it should be a deliberate choice rather than a surprise. The new append_failures() counter is the subscriber-free replacement signal.

The literal substring durable append failed is preserved in all four new messages, so existing log searches keep matching.

  • First failure of a run → tracing::error! (target tinyagents::observability, sink field). Durable-log data is being lost, and the module docs promise callers a lossless log modulo the counted drop policy.
  • Repeats suppressed, with a warn! reminder at most every APPEND_REPORT_COOLDOWN (300s) carrying the consecutive-failure count and the latest error text — so a changed cause (read-only → disk full) still surfaces within one cooldown without needing per-error keying.
  • Recovery → one warn! reporting how many appends were lost. warn! rather than info! because data was lost.
  • Shutdown while still degraded → one final warn! summary, so a run that never recovers is not silently quiet.
  • New append_failures: Arc<AtomicU64> + accessor + Debug field, mirroring the existing dropped counter exactly (same Relaxed ordering, same accessor pattern).

No new dependenciestracing is already a direct dependency.

Public API is unchanged. One addition is pub(crate): spawn_with_cooldown, which spawn delegates to with the 300s constant. Reviewer's call, and easy to drop — with the cooldown hardcoded, the reminder emission path is unreachable from a test, so only the should_report decision could be covered and the live log lines would sit uncovered against the 90% gate. If you would rather keep the surface minimal and accept those uncovered lines, say so and I will collapse it.

Tests

  • cargo fmt --all -- --check
  • cargo clippy --all-targets -- -D warnings
  • cargo clippy --all-targets --all-features -- -D warnings
  • cargo build --all-targets
  • cargo build --all-targets --all-features
  • cargo test — 112 suites ok
  • cargo test --all-features — 112 ok
  • cargo llvm-cov --all-features --workspace --fail-under-lines 9092.24%

worker.rs itself goes 85.84% → 94.69% lines, 100% function coverage. The Debug-output assertion is there deliberately: extending an untested Debug impl otherwise pushes the file's own coverage down.

Three new tests in src/harness/observability/test.rs, all clock-independent, following the shape of the existing append_worker_drops_and_counts_when_queue_is_full. Each was proven to fail against the pre-change behaviour before being kept:

1. removed the counter increment ->
   append_worker_counts_failed_appends
   assertion `left == right` failed: every failed durable append must be counted
     left: 0   right: 12

2. should_report -> always true ->
   assertion failed: !should_report(Some(start), start + Duration::from_secs(299), cooldown)

3. stopped attempting while degraded ->
   append_worker_failure_then_recovery_keeps_attempting
     left: 8   right: 3
   (the worker charged all 8 items as failures instead of recovering after 3)

The third is the one that matters most — it pins that a degraded worker keeps trying, which is the behaviour that makes recovery possible at all.

Documentation

  • src/harness/observability/README.md and src/graph/observability/README.md — the only two places in the tree claiming failures are "reported to stderr", which this change makes false. (graph's JournalGraphSink shares this same AppendWorker.) mod.rs's "backend errors are reported, not propagated" and types.rs's pointer to the drop/error policy stay accurate and were left alone.
  • The # Error policy section of the worker.rs module docs is rewritten: tracing target, first-failure level, suppression and cooldown, recovery summary, lifetime counter.

Related

Pre-existing and deliberately untouched, but worth naming since it is adjacent: if the tokio runtime fails to build, the drain thread runs while rx.recv().is_ok() {} — silently draining and discarding everything, with no counter and no report. It does not even increment append_failures, so that failure mode is completely invisible. Happy to fix it here or in a follow-up, whichever you prefer.

Context for why this was found: OpenCompany vendors tinyagents and hit the flood in a tenant container. Tracked there as tinyhumansai/opencompany#450. That repo's fix is a submodule-pointer bump once this lands; the same flood persists at the openhuman and tinycortex pins until they bump too.

Summary by CodeRabbit

  • Bug Fixes
    • Persistence failures are now reported through structured logs instead of standard error output.
    • Failed writes continue retrying, with persistence resuming automatically after temporary backend issues.
    • Failure reports are rate-limited while retaining counts for suppressed errors.
    • Recovery messages and shutdown summaries improve visibility into persistence health.
  • Documentation
    • Updated observability guidance for failure counting, reporting intervals, recovery notifications, and retries.
  • Tests
    • Added coverage for failure counting, retries, recovery, and rate-limited reporting.

The drain loop reported every failed durable append with a bare `eprintln!`,
so a persistent sink failure (read-only volume, full disk) emitted one
identical line per arriving observation and drowned the host's stderr. Failed
appends were also uncounted, unlike queue-full drops, leaving the flood itself
as the only signal that the durable log was losing data.

Replace it with a per-worker failure-run state machine local to the drain
thread: the first failure of a run reports at ERROR, subsequent failures are
counted silently with a WARN reminder at most once per APPEND_REPORT_COOLDOWN
(5 minutes) carrying the latest error, the first success emits one WARN
recovery summary with the number of observations lost, and a shutdown that is
still degraded emits a final WARN so a never-recovering run is not silently
quiet. Reporting moves to `tracing` on the `tinyagents::observability` target
with a `sink` field, matching the crate's existing emission idiom and removing
its only `eprintln!`.

Add an `append_failures` counter mirroring `dropped` so durable-log loss has a
subscriber-independent signal, and keep attempting every item while degraded:
the attempt is what detects recovery, it runs off the run's critical path, and
skipping it would turn a transient blip into guaranteed loss of everything
still queued.

BEHAVIOR CHANGE: the stderr line is gone. An embedder with no tracing
subscriber installed now sees nothing on append failure and must read
`append_failures` instead. The message keeps the literal substring
"durable append failed" so existing log searches still match.
Both module READMEs still described backend errors as "reported to stderr",
which the rate-limited tracing reporter makes false. Describe the actual
policy: errors are counted in `append_failures` and reported on the
`tinyagents::observability` target, rate-limited to one ERROR per failure run
plus a WARN reminder per cooldown and a WARN recovery summary, with the worker
still attempting every item while degraded. Note that reporting requires a
tracing subscriber, so `append_failures` is the subscriber-independent signal.
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f97eda33-7f78-4bc8-a236-eb030645761d

📥 Commits

Reviewing files that changed from the base of the PR and between 34c28cb and 98cfdce.

📒 Files selected for processing (2)
  • src/harness/observability/README.md
  • src/harness/observability/worker.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/harness/observability/README.md
  • src/harness/observability/worker.rs

📝 Walkthrough

Walkthrough

AppendWorker now tracks rejected appends separately from queue drops. It retries failed appends and reports failures through cooldown-limited tracing events, including recovery and shutdown summaries. Tests and documentation cover the new behavior.

Changes

Append failure reporting

Layer / File(s) Summary
Reporting contract and worker construction
src/harness/observability/worker.rs, src/harness/observability/README.md, src/graph/observability/README.md
Defines cooldown-aware reporting, initializes failure accounting, and documents the tracing contract.
Failure accounting and reporting
src/harness/observability/worker.rs
Counts rejected appends, preserves retries, emits failure and recovery events, and exposes the counter.
Failure and cooldown tests
src/harness/observability/test.rs
Tests failure counts, queue-drop separation, retries, recovery persistence, debug output, and cooldown decisions.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AppendWorker
  participant AppendSink
  participant tracing subscriber
  AppendWorker->>AppendSink: append payload
  AppendSink-->>AppendWorker: return success or error
  AppendWorker->>AppendWorker: update append_failures
  AppendWorker->>tracing subscriber: emit rate-limited report
Loading

Possibly related issues

  • tinyhumansai/opencompany issue 450 — Covers replacing unbounded stderr append-failure reporting with rate-limited tracing events and recovery summaries.

Poem

A rabbit counts each failed hop,
While queue drops stay in their own crop.
Tracing reports the errors clear,
Cooldowns keep repeated noise dear.
When sinks recover, the logs agree.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: limiting repeated durable-append failure reports.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/harness/observability/worker.rs`:
- Around line 260-269: Expose the subscriber-free append failure count through
public accessors on both JournalSink and JsonlSink, delegating to
AppendWorker::append_failures; update the corresponding sink definitions at
src/harness/observability/worker.rs:45-48 and :260-269, and update the
documentation at src/harness/observability/README.md:66-75 to describe the
public API. Do not leave the counter documented as crate-internal.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a93109b3-2c5c-4645-98ed-0906686b3b0d

📥 Commits

Reviewing files that changed from the base of the PR and between 27a3f39 and 34c28cb.

📒 Files selected for processing (4)
  • src/graph/observability/README.md
  • src/harness/observability/README.md
  • src/harness/observability/test.rs
  • src/harness/observability/worker.rs

Comment thread src/harness/observability/worker.rs
Both the module docs and the README told embedders to "read append_failures for
a subscriber-independent signal". They cannot: like the queue-full `dropped`
count it mirrors, the counter is `pub(crate)` and neither `JournalSink` nor
`JsonlSink` exposes it.

That mattered because it was the stated consolation for removing the stderr
line — so the one claim softening the behaviour change was the one that was not
true. Say plainly that a host with no subscriber sees nothing, and that
installing one is how durable-log loss is observed.

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tinysweeper found nothing blocking. Approving.

             $0.0487 · 35,866 in / 16,570 out · 28,556 cached (80%) · z-ai/glm-5.2
critique:    $0.0271 · 11,479 in / 10,277 out · 9,544 cached (83%)  · z-ai/glm-5.2
security:    $0.0036 · 7,578 in  / 753 out    · 6,405 cached (85%)  · z-ai/glm-5.2
tests:       $0.0061 · 6,850 in  / 1,808 out  · 5,398 cached (79%)  · z-ai/glm-5.2
description: $0.0103 · 8,507 in  / 3,252 out  · 6,183 cached (73%)  · z-ai/glm-5.2

@tinysweeper

tinysweeper Bot commented Aug 12, 2026

Copy link
Copy Markdown

What this change touches

4 files, +273 -14 across 2 components. The code graph knows nothing about these files yet — normal for newly added files, and a cold index otherwise.

flowchart LR
  n0["src/harness/observability<br/>3 files +268 -12"]:::changed
  n1["src/graph/observability<br/>1 file +5 -2"]:::changed
  classDef changed fill:#0d4429,stroke:#238636,color:#e6edf3
  classDef impacted fill:#161b22,stroke:#6e7681,color:#c9d1d9
  classDef flagged fill:#5a1e02,stroke:#d93f0b,color:#ffffff
  classDef blocking fill:#67060c,stroke:#f85149,color:#ffffff
Loading

Green: changed. Grey: untouched, reached through an import or a call. Orange: has findings. Red: has a finding that blocks the merge.

Component Files Lines Findings
src/harness/observability changed 3 +268 -12
src/graph/observability changed 1 +5 -2
Changed files

src/harness/observability

  • src/harness/observability/README.md
  • src/harness/observability/test.rs
  • src/harness/observability/worker.rs

src/graph/observability

  • src/graph/observability/README.md

tinysweeper 0.1.0

@tinysweeper tinysweeper Bot added the priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect. label Aug 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant