Module: Legion::Extensions::MicrosoftTeams::Helpers::ThrottleAware
- Included in:
- Actor::ApiIngest, Actor::MeetingIngest, Actor::PresencePoller
- Defined in:
- lib/legion/extensions/microsoft_teams/helpers/throttle_aware.rb
Overview
Mixin that lets an Actors::Every-style poller honour a Graph
throttle by deferring its own next run instead of re-firing on
its fixed timer interval.
Background: Faraday::RetryAfter raises Errors::Throttled
centrally when it exhausts retries (or when the advertised wait
alone exceeds the retry budget), and Faraday::ThrottleCircuit
opens a shared circuit so the whole fleet stops hammering a quota
that Graph has already flagged. But the actors that drive those
calls schedule themselves with a Concurrent::TimerTask whose
execution_interval is fixed at boot. Catching Throttled in a
rescue block stops the current tick, but the next tick still
fires on the original cadence — so a poller on a 30–300s interval
keeps walking straight back into an open circuit, emitting an
ERROR every interval and contributing nothing but log noise (and,
via the shared circuit, refreshing the block for cheaper callers).
This mixin closes that gap. An actor wraps its Graph work in
#with_throttle_deferral. On Errors::Throttled it records a
"suppress until" instant of now + retry_after (falling back to a
bounded default when the server gave no usable Retry-After).
Subsequent ticks short-circuit via #throttle_suppressed? until
that instant passes, so the actor effectively reschedules itself
at now + retry_after rather than now + interval — exactly the
behaviour the throttle integration spec and the 0.6.51 CHANGELOG
flagged as the outstanding follow-up.
State is per-actor-instance and guarded by a monitor because an
actor's manual runs on a Concurrent thread-pool worker. The
mixin reads the carried retry_after only when
retry_after_known? is true; otherwise it applies
DEFAULT_DEFERRAL so a throttle with a missing/garbage header
still backs the poller off rather than letting it spin.
Constant Summary collapse
- DEFAULT_DEFERRAL =
Fallback deferral (seconds) used when a
Throttledcarries no usableretry_after(header absent or unparseable). One minute matches Microsoft Graph's documented typical Retry-After and the circuit middleware'sDEFAULT_FALLBACK_TTL. 60.0- MAX_DEFERRAL =
Hard ceiling (seconds) on a single deferral so a pathological advertised
Retry-Aftercan't park a poller for an unbounded time. Mirrors the circuit middleware's 600s cap inThrottleCircuit#set_hard_circuit. 600.0
Instance Method Summary collapse
-
#defer_for(seconds, now: Time.now) ⇒ Object
Open (or extend) a deferral window of
secondsfromnow. -
#reset_throttle_deferral ⇒ Object
Clear any active deferral.
-
#throttle_remaining(now: Time.now) ⇒ Object
Seconds remaining in the current deferral window (0.0 if none / elapsed).
-
#throttle_suppressed?(now: Time.now) ⇒ Boolean
True while the actor is inside a deferral window opened by a previous throttle.
-
#throttled_until ⇒ Object
The instant before which the actor should not poll again, or nil if no deferral is active.
-
#with_throttle_deferral(now: Time.now) ⇒ Object?
Run
blockunless the actor is currently deferring after a recent throttle.
Instance Method Details
#defer_for(seconds, now: Time.now) ⇒ Object
Open (or extend) a deferral window of seconds from now. A
later throttle never shortens an existing window — we keep the
furthest-out instant so overlapping throttles compose safely.
103 104 105 106 107 108 109 110 111 112 113 114 |
# File 'lib/legion/extensions/microsoft_teams/helpers/throttle_aware.rb', line 103 def defer_for(seconds, now: Time.now) window = clamp_deferral(seconds) target = now + window throttle_monitor.synchronize do @throttled_until = if @throttled_until.nil? || target > @throttled_until target else @throttled_until end end window end |
#reset_throttle_deferral ⇒ Object
Clear any active deferral. Called implicitly is unnecessary —
#throttle_suppressed? expires on its own once the clock passes
throttled_until — but exposed for tests and explicit resets.
119 120 121 |
# File 'lib/legion/extensions/microsoft_teams/helpers/throttle_aware.rb', line 119 def reset_throttle_deferral throttle_monitor.synchronize { @throttled_until = nil } end |
#throttle_remaining(now: Time.now) ⇒ Object
Seconds remaining in the current deferral window (0.0 if none / elapsed). Useful for logging and for tests.
92 93 94 95 96 97 98 |
# File 'lib/legion/extensions/microsoft_teams/helpers/throttle_aware.rb', line 92 def throttle_remaining(now: Time.now) until_at = throttled_until return 0.0 if until_at.nil? remaining = until_at - now remaining.positive? ? remaining : 0.0 end |
#throttle_suppressed?(now: Time.now) ⇒ Boolean
Returns true while the actor is inside a deferral window opened by a previous throttle.
79 80 81 82 |
# File 'lib/legion/extensions/microsoft_teams/helpers/throttle_aware.rb', line 79 def throttle_suppressed?(now: Time.now) until_at = throttled_until !until_at.nil? && now < until_at end |
#throttled_until ⇒ Object
The instant before which the actor should not poll again, or nil if no deferral is active. Thread-safe read.
86 87 88 |
# File 'lib/legion/extensions/microsoft_teams/helpers/throttle_aware.rb', line 86 def throttled_until throttle_monitor.synchronize { @throttled_until } end |
#with_throttle_deferral(now: Time.now) ⇒ Object?
Run block unless the actor is currently deferring after a
recent throttle. If a Throttled escapes the block, record the
deferral window and swallow it (the throttle has already been
logged at the middleware/circuit layer; re-raising would just
trip the actor's generic rescue StandardError and double-log).
65 66 67 68 69 70 71 72 73 74 75 |
# File 'lib/legion/extensions/microsoft_teams/helpers/throttle_aware.rb', line 65 def with_throttle_deferral(now: Time.now) if throttle_suppressed?(now: now) log_throttle_skip(now: now) return nil end yield rescue Legion::Extensions::MicrosoftTeams::Errors::Throttled => e defer_after_throttle(e, now: now) nil end |