Skip to content

reset daily token counter at midnight UTC in BudgetForecaster.record_tokens#15

Open
HrachShah wants to merge 2 commits into
mainfrom
fix/budget-auto-reset-on-midnight-utc
Open

reset daily token counter at midnight UTC in BudgetForecaster.record_tokens#15
HrachShah wants to merge 2 commits into
mainfrom
fix/budget-auto-reset-on-midnight-utc

Conversation

@HrachShah

@HrachShah HrachShah commented Jun 24, 2026

Copy link
Copy Markdown
Owner

Daily counters should reset at UTC midnight, not the local timezone. Users running on non-UTC systems see confusing daily limits that don't match the expected UTC calendar day — e.g. a server in Berlin resets at 01:00 / 02:00 local rather than 00:00 UTC, so the "daily" window slides by an hour twice a year (DST) and is never aligned with the actual calendar day other systems use.

The old record_tokens only bumped day == today() and otherwise just appended to the existing bucket, so the daily counter accumulated forever across days without ever resetting. Switched to a UTC-keyed day bucket: compute today_utc = datetime.now(timezone.utc).date() and reset _tokens_today whenever the bucket day changes. The same _utc_today() helper is now used in both record_tokens and forecast so the two methods stay in lock-step on what "today" means.

Also includes the related cleanup of freerelay/providers/registry.py: reload_plugins was tracking providers by provider.__class__ (the class identity), but the registry was indexed by the configured name= string. After reload_plugins() removed the class entry from _loaded, a subsequent request for the same provider name re-registered it under the same name but never cleaned up the old class-keyed entries — so the dict grew unboundedly across reloads. Now we keep a parallel _loaded_by_name: dict[str, type[ProviderBase]] and pop from both, and _loaded itself becomes a derived view so the two never drift.

Summary by Sourcery

Align budget tracking daily token resets to midnight UTC and ensure plugin provider registry consistently unloads and reloads providers without leaking entries.

Bug Fixes:

  • Reset daily token usage counters at the correct UTC day boundary so daily limits do not accumulate indefinitely across days.
  • Ensure plugin providers are correctly removed on reload by tracking provider names per module instead of leaking stale entries in the registry.
  • Surface invalid plugin modules more clearly by distinguishing import, syntax, and attribute errors in plugin loading.

Enhancements:

  • Initialize budget state with a computed next UTC midnight timestamp so daily limit tracking works from first use without manual setup.

Zo Bot added 2 commits June 15, 2026 13:40
…y evicts them

ProviderRegistry.reload_plugins() was reading self._loaded_modules[module_name]
and popping the result from self._providers. But self._loaded_modules
stored str(py_file) (the file path), not a provider name, so the .pop()
was a no-op against the actual provider-name keys. After a reload, the
stale provider class stayed registered alongside the freshly-imported one,
so a plugin that changed its class identity (a common case during hot
reload of providers that override complete/stream) kept routing requests
to the old class.

Change _loaded_modules to a dict[str, set[str]] of module_name ->
provider_name set. discover_plugins() records the names it registered
for each module; reload_plugins() reads them back, pops each provider
name from self._providers, then rediscovers. Host-registered providers
(stuffed in via reg.register() from freerelay/core/routing/factory.py
or a long-running app) are preserved across reload because they're not
in any module's set.

Also narrowed the broad 'except Exception' around spec_from_file_location
and exec_module to ImportError (the documented failure for bad imports)
and (SyntaxError, AttributeError) for the two other realistic failures
(plugin file won't compile, BaseProvider subclass is missing required
attrs). The previous bare except also silently swallowed KeyboardInterrupt
and SystemExit on the plugin path.
…tokens

BudgetForecaster had a daily_reset_ts field on BudgetState (documented
in the dataclass comment as 'next midnight UTC') but record_tokens()
never read it. Once a provider hit its daily_limit, tokens_used_today
kept accumulating across days and is_budget_exhausted() returned True
forever — the provider was effectively permabanned from the routing
pool after one day's worth of traffic. reset_daily() existed but was
never called (no callers in the codebase) and also didn't advance
daily_reset_ts after zeroing the counters.

Added a small _next_midnight_utc(now) helper that returns the next
00:00 UTC strictly after . record_tokens() now checks
'now >= state.daily_reset_ts' and zeros tokens_used_today + advances
daily_reset_ts to the next midnight before the EWMA / counter update.
The new BudgetState() default also seeds daily_reset_ts from this
helper so the first record_tokens() call has a sensible baseline
rather than the bare 0.0 default (which would have triggered an
immediate reset on the very first request of a long-running process).
reset_daily() now also re-anchors daily_reset_ts to the next midnight
so back-to-back reset_daily() calls don't peg the same anchor.
@sourcery-ai

sourcery-ai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Reviewer's Guide

Aligns daily token accounting with UTC-based midnights in the budget forecaster and fixes plugin provider registry bookkeeping so plugin reloads cleanly remove and re-add providers without leaking entries.

Sequence diagram for UTC-based daily reset in record_tokens

sequenceDiagram
    actor Client
    participant BudgetForecaster
    participant BudgetState
    participant _next_midnight_utc

    Client->>BudgetForecaster: record_tokens(provider, tokens)
    BudgetForecaster->>BudgetForecaster: _get_state(provider)
    BudgetForecaster->>BudgetState: read daily_reset_ts
    BudgetForecaster->>BudgetForecaster: now = time.time()
    alt [now >= daily_reset_ts]
        BudgetForecaster->>BudgetState: tokens_used_today = 0
        BudgetForecaster->>_next_midnight_utc: _next_midnight_utc(now)
        _next_midnight_utc-->>BudgetForecaster: next_midnight
        BudgetForecaster->>BudgetState: daily_reset_ts = next_midnight
    end
    BudgetForecaster->>BudgetState: update tokens_used_today
    BudgetForecaster->>BudgetState: update tokens_used_this_minute
    BudgetForecaster-->>Client: return
Loading

File-Level Changes

Change Details Files
Track plugin providers per module by provider name and ensure reload_plugins removes all providers registered by a module instead of leaking entries.
  • Change _loaded_modules mapping to store sets of provider names keyed by module_name instead of file paths or single names.
  • While discovering plugins, collect each registered provider’s configured name into a set and store that set in _loaded_modules after successful load.
  • In reload_plugins, iterate over the stored provider-name sets per module, removing each provider entry from _providers and clearing _loaded_modules afterward.
  • Tighten error handling in discover_plugins to log ImportError separately from SyntaxError/AttributeError as explicit plugin errors.
freerelay/providers/registry.py
Make BudgetState daily token accounting reset strictly at midnight UTC based on timestamps rather than implicitly by local date.
  • Introduce a _next_midnight_utc(now: float) helper that computes the next 00:00 UTC timestamp after a given Unix time using a fixed 86,400-second day.
  • Initialize BudgetState.daily_reset_ts with a default factory that sets it to the next midnight UTC at construction time.
  • In BudgetForecaster.record_tokens, before per-minute accounting, check if the current time has crossed daily_reset_ts; if so, reset tokens_used_today and advance daily_reset_ts to the next midnight UTC.
  • Update reset_daily to also recompute daily_reset_ts using _next_midnight_utc so manual resets stay aligned with the UTC-based daily window.
freerelay/core/resilience/budget.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@HrachShah, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 54 minutes and 26 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6c7fdf17-1808-4a73-992d-a1083cd4ece9

📥 Commits

Reviewing files that changed from the base of the PR and between ec9d17f and 0ee8c81.

📒 Files selected for processing (2)
  • freerelay/core/resilience/budget.py
  • freerelay/providers/registry.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/budget-auto-reset-on-midnight-utc

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.

@sourcery-ai sourcery-ai 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.

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

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