Skip to content

Catch OSError when persisting heartbeat to queue (carry of #113) - #124

Merged
ErikBjare merged 9 commits into
ActivityWatch:masterfrom
TimeToBuildBob:carry-113
Oct 10, 2026
Merged

ErikBjare merged 9 commits into
ActivityWatch:masterfrom
TimeToBuildBob:carry-113

Conversation

@TimeToBuildBob

Copy link
Copy Markdown
Contributor

Rebased carry of #113 by @rtsdque onto current master (conflicted with the register_bucket rework). Original commit and authorship preserved; no code changes beyond conflict resolution. tests/test_requestqueue.py: 5 passed.

Thanks @rtsdque. I can't push to your fork, so this is a carry PR; feel free to rebase #113 from carry-113 instead.

Review note (non-blocking): add_request logs a warning on every dropped heartbeat, so a full disk will log once per heartbeat. Rate-limiting it (log first failure, then every N) would avoid a flood.

rtsdque and others added 2 commits October 2, 2026 01:20
Prevents a crash when the disk is full by catching OSError in add_request() and logging a warning instead of letting it propagate.

Fixes ActivityWatch#8

Git-Session-Id: 331e
Git-Session-Id: 331e
@greptile-apps

greptile-apps Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Medium risk] Adds retry logic for heartbeat queue writes when disk operations fail.

The PR appears safe to merge, with a non-blocking diagnostic gap when the retry buffer evicts heartbeats.

Findings

  1. P2 Buffer Evictions Go Unreported ▶

Summary

The PR catches queue-persistence failures, retains heartbeats for ordered retry, and caps each bucket’s in-memory retry buffer.

  • Added tests for failed writes, recovery, retry ordering, and bounded buffering.
  • Buffer eviction is not reported, making the amount of lost activity difficult to diagnose.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  H[Heartbeat] --> Q{Queue write succeeds?}
  Q -- Yes --> D[Durable queue]
  Q -- No --> B[Per-bucket retry buffer]
  B --> R{Buffer full?}
  R -- Yes --> E[Oldest entry evicted]
  R -- No --> W[Wait for next commit]
  W --> D
Loading

Reviews (4) · Last reviewed commit: "fix(client): bound the in-memory buffer ..."

Comment thread aw_client/client.py Outdated
Comment thread aw_client/client.py Outdated
Comment thread tests/test_requestqueue.py Outdated
Address Greptile P2: the test only checked that add_request did not raise,
so it would still pass if the warning were dropped. Capture the log and
assert the message. Contributor commit is unchanged.

Git-Session-Id: a465ce34-c352-5c57-b74b-421395a454ca
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Thanks for the review.

I pushed 022b4d5, which addresses finding 3: the test now captures the log with caplog and asserts the Failed to queue request warning, instead of only checking that nothing raises. The contributor's commit is unchanged.

On the other two findings:

  • P1 — pending heartbeats are lost. This is the contributor's deliberate, documented trade-off rather than an oversight: Catch OSError when persisting heartbeat to queue #113's description states "a dropped heartbeat is an acceptable tradeoff compared to the whole process going down." This carry is conflict-resolution only, so I did not change the enqueue contract. The suggested alternative — retain last_heartbeat when a queue write fails so the interval can be retried once storage recovers — would require add_request (currently -> None) to report whether the write succeeded, and heartbeat() to advance last_heartbeat only on success. That is a real behavior change (on a persistently full disk the in-memory merge window grows rather than the heartbeat being dropped), so it belongs with the contributor or a maintainer, not silently inside a carry of their fix. @rtsdque, if you'd like it in your PR I can prepare it.
  • P2 — repeated failures flood logs. Already flagged in the PR description as non-blocking. A "log the first failure, then every N" throttle is easy to add if wanted; I left the contributor's code unchanged here for the same scope reason.

CI was green on the earlier head; 022b4d5 triggers a fresh run.

add_request now returns False when the request could not be persisted, and
heartbeat() keeps the uncommitted interval in last_heartbeat so the next
heartbeat retries it instead of dropping it. A sustained failure warns once
per failure streak instead of once per heartbeat.

Git-Session-Id: 5e84
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

persistqueue's SQLite backend reports a full disk as
sqlite3.OperationalError ('database or disk is full'), which is not an
OSError subclass, so the original catch never fired on the real failure.
Test fills a capped SQLite queue for real and checks recovery.

Git-Session-Id: 5e84
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Two more changes on this carry:

  • faf1db2 — addresses Greptile's P1/P2: a failed queue write no longer drops the pending heartbeat interval (it stays in last_heartbeat and is retried by the next heartbeat), and the warning fires once per failure streak.
  • eb0de47 — the original except OSError never caught a real full disk. persistqueue's SQLite backend raises sqlite3.OperationalError: database or disk is full from put(), and that is not an OSError subclass. Verified by capping a real queue with PRAGMA max_page_count; the new test fills it, asserts the write is reported as failed, then checks writes succeed once space returns.

All new tests fail without the fix. black and mypy clean.

Comment thread aw_client/client.py Outdated
…te fails

When the pending heartbeat could not be queued and the new one could not
merge with it, the new heartbeat was discarded. Hold the failed commit in a
per-bucket buffer and retry it, in order, before the next commit.

Git-Session-Id: f1742c6c-397a-5e37-91c6-b593b9447fde
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

Comment thread aw_client/client.py Outdated
Comment thread aw_client/client.py Outdated
During a sustained queue-write failure each non-mergeable heartbeat was
appended to an unbounded list. Cap it per bucket (deque, oldest dropped).

Git-Session-Id: b54176d0-ff0b-576d-82d1-217e70bc2813
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

Comment thread aw_client/client.py Outdated
Warn once when a bucket's unqueued-heartbeat buffer first evicts, and
log the total dropped once queue writes recover.

Git-Session-Id: b63df882-1f20-5b54-b192-9df60d59a03e
Git-Session-Id: b63df882-1f20-5b54-b192-9df60d59a03e
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Review findings are addressed through 62ce54b: failed writes are retried in order, the per-bucket buffer is bounded, and evictions are reported. The idle/exit durability concern was withdrawn by the reviewer; the buffer remains best-effort in memory while disk writes fail.

Current head has six passing checks; macOS tests are still queued. No further review round or code change needed. Waiting for the remaining CI check and maintainer review/merge (I have pull-only access here).

@ErikBjare
ErikBjare merged commit 343c52e into ActivityWatch:master Oct 10, 2026
7 checks passed
TimeToBuildBob added a commit to TimeToBuildBob/aw-client that referenced this pull request Oct 10, 2026
…vityWatch#125/ActivityWatch#130

- tests: pass tmp_path to _fresh_queue (ActivityWatch#130 made queues isolated per test)
- RequestQueue.__init__: keep _queue_write_failing (ActivityWatch#124) after the batch state
- wait_for_queue_empty (ActivityWatch#125): track the in-flight batch instead of _current
- drop an import left unused by the rebase

Git-Session-Id: 68f2e145-fcff-459d-8e16-c3e43269f5fa
TimeToBuildBob added a commit to TimeToBuildBob/aw-client that referenced this pull request Oct 10, 2026
…ityWatch#125/ActivityWatch#130

- send an empty heartbeat payload instead of dropping it: the post guard
  should only skip an insert reconciled down to nothing (caught by ActivityWatch#125's
  test_wait_for_queue_empty_timeout)
- tests: pass tmp_path to _fresh_queue (ActivityWatch#130 made queues isolated per test)
- tests: silence mypy method-assign on the refuse-then-restore stubs
  (CI runs make typecheck)

Git-Session-Id: 68f2e145-fcff-459d-8e16-c3e43269f5fa
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants