✨ Report update loops throttled by the update scheduler

When the scheduler throttles an update loop, it only logs a console
warning, so a loop that no longer crashes leaves no trace. Send an
error report too, with the events in flight and the last events, at
most once per report window.

Relates to #12580

AI-assisted-by: claude-opus-5-5
This commit is contained in:
Eva Marco 2026-10-23 13:16:04 +02:00
parent 379470bdd2
commit eeb89aecab
2 changed files with 42 additions and 6 deletions

View File

@ -438,17 +438,30 @@
(volatile! 0))
(defn on-update-loop
"Handler for the rumext update scheduler trips: logs the throttled
update loop with the events being processed, at most once per
"Handler for the rumext update scheduler trips: logs and reports the
throttled update loop with the events being processed, at most once per
`report-window-ms`. Runs inside store watchers and effect bodies, so it
never throws."
[_]
[{:keys [rounds]}]
(try
(let [now (inst-ms (ct/now))]
(when (>= (- now @update-loop-warned-at) report-window-ms)
(vreset! update-loop-warned-at now)
(.warn js/console "[update-scheduler] update loop throttled; in-flight events:"
(into-array (map ptk/repr-event (ptk/in-flight-events st/state))))))
(let [events (ptk/in-flight-events st/state)
cause (ex-info "update loop throttled"
{:type :internal
:code :update-loop-throttled
:rounds rounds})]
(.warn js/console "[update-scheduler] update loop throttled; in-flight events:"
(into-array (map ptk/repr-event events)))
(when (seq events)
(.set in-flight-by-cause cause (in-flight-lines events)))
;; Report outside the loop that tripped the scheduler
(ts/schedule
#(submit-report :event-name "handled-exception"
:hint (ex-message cause)
:report (fn [] (generate-report cause))
:cause cause)))))
(catch :default _
nil)))

View File

@ -14,7 +14,8 @@
- events in flight – the full report lists the events the
store was processing when it caught the exception; a failing stream
pipeline lists the emitters of the value, not its own event
- on-update-loop – never throws into the scheduler's caller
- on-update-loop – never throws into the scheduler's caller;
reports the throttled loop with the events in flight
- flash schedules async report and toast – neither the report nor
ntf/show is emitted synchronously
- organization SSO recovery – expired SSO sessions go back to the provider
@ -554,6 +555,28 @@
{ptk/in-flight-events (fn [_] (throw (js/Error. "in-flight read failure")))}
(t/is (nil? (errors/on-update-loop {:rounds 26}))))))
(t/deftest ^:async update-loop-is-reported-with-the-events-in-flight
(errors/reset-report-governor!)
(vreset! @#'errors/update-loop-warned-at 0)
(let [events (atom [])
loop-ev (ptk/reify ::test-update-loop-source)]
(await
(mock/with-mocks*
{st/emit! (mock/stub (fn [& emitted] (swap! events into emitted)))
ptk/in-flight-events (fn [_] [loop-ev])
st/format-last-events (mock/stub (fn [& _] "(stub last events)"))
rt/get-current-href (constantly "https://penpot.example.com/#/workspace")
tm/schedule (mock/stub (fn [f] (f)))}
(errors/on-update-loop {:rounds 26})
;; A second trip within the window is not reported again
(errors/on-update-loop {:rounds 26})
(t/is (= 1 (count @events)))
(let [props (deref (first @events))]
(t/is (= "handled-exception" (::ev/name props)))
(t/is (= "update loop throttled" (:hint props)))
(t/is (str/includes? (in-flight-section (:report props))
"test-update-loop-source")))))))
;; ---------------------------------------------------------------------------
;; Expired organization SSO session
;;