Skip to content

perf(commands): throttle progress-message DB writes to once per second per command - #266

Open
jordanfelle wants to merge 2 commits into
Chaptarr:developfrom
jordanfelle:perf-throttle-command-progress-message
Open

jordanfelle wants to merge 2 commits into
Chaptarr:developfrom
jordanfelle:perf-throttle-command-progress-message

Conversation

@jordanfelle

Copy link
Copy Markdown

Problem

ProgressMessageTarget forwards every progress log line to CommandQueueManager.SetMessage, which synchronously UPDATEs Message and LastProgressAt on the command row. An author refresh logs one line per book ("Checking Info for ..."), so a refresh paid one database round trip per book just to change display text.

Found by sampling a live bulk refresh with dotnet-stack (12 samples): RefreshBookService.LogProgress -> SetMessage -> SetFields -> UpdateFields was in 2 of 12 (~17%), alongside the per-book delete path (a separate change).

Change

  • ICommandQueueManager.SetProgressMessage(command, message): a default interface method that calls SetMessage, so existing implementers and hand-written test doubles are unaffected.
  • CommandQueueManager overrides it: the in-memory command (what the API reads while it runs) is updated on every call; the DB write happens at most once per second per command.
  • ProgressMessageTarget calls SetProgressMessage.
  • Explicit messages (Completed / Failed / Cancelled / Resumed) still use SetMessage: always persisted, and they clear that command's throttle state.
  • LastProgressAt already has its own 30s heartbeat (TouchProgress) and nothing else reads it.

Tests

CommandQueueManagerProgressFixture (4 tests): one DB write for 200 progress messages; in-memory message always current; SetMessage always persists and resets the throttle; throttling is per command. The throttle tests fail with the interval set to 0. Full Chaptarr.Core.Test: 3039 passed.

Not measured

Query-count change only; no before/after refresh timing yet.

…ond per command

ProgressMessageTarget turns every progress log line ("Checking Info for <book>") into
CommandQueueManager.SetMessage, which did a synchronous UPDATE of Message + LastProgressAt
on the command row. An author refresh emits one per book, so a refresh spent a database
round trip per book just to change display text. Stack samples of a live bulk refresh had
this in ~17% of samples (LogProgress -> SetMessage -> SetFields -> UpdateFields).

Add ICommandQueueManager.SetProgressMessage (a default interface method that calls
SetMessage, so existing implementers and test doubles are untouched). The real manager keeps
the in-memory command current on every call - that is what the API reads while the command
runs - and persists at most once per second per command. ProgressMessageTarget now calls it.
Explicit messages (Completed/Failed/Cancelled/Resumed) still go through SetMessage: always
persisted, and they clear the command's throttle state. LastProgressAt already has its own
30s heartbeat (TouchProgress) and nothing else reads it, so its cadence is unchanged in effect.

Tests: at most one DB write for 200 progress messages, in-memory message always current,
SetMessage always persists and resets the throttle, throttling is per command. The
throttle tests fail with the interval set to 0.
Adversarial review of Chaptarr#266: the exact write-count assertions assumed the 1s throttle window never elapses mid-test. Use a 1-2 bound for the burst test and relative counts for the SetMessage/reset test.
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.

1 participant