hermes - 💡(How to fix) Fix bug(signal_rate_limit): TOCTOU race in get_scheduler() — concurrent calls can create duplicate SignalAttachmentScheduler instances

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

gateway/platforms/signal_rate_limit.py:get_scheduler() (line 351) uses a bare if _scheduler is None: guard with no lock. Gateway platforms dispatch concurrent coroutines, so two concurrent calls can both see _scheduler is None, both construct SignalAttachmentScheduler(), and the loser's instance is orphaned with its internal token-bucket state.

_scheduler: Optional[SignalAttachmentScheduler] = None  # no lock

def get_scheduler() -> SignalAttachmentScheduler:
    global _scheduler
    if _scheduler is None:              # ← race
        _scheduler = SignalAttachmentScheduler()  # ← duplicate instances

Root Cause

gateway/platforms/signal_rate_limit.py:get_scheduler() (line 351) uses a bare if _scheduler is None: guard with no lock. Gateway platforms dispatch concurrent coroutines, so two concurrent calls can both see _scheduler is None, both construct SignalAttachmentScheduler(), and the loser's instance is orphaned with its internal token-bucket state.

_scheduler: Optional[SignalAttachmentScheduler] = None  # no lock

def get_scheduler() -> SignalAttachmentScheduler:
    global _scheduler
    if _scheduler is None:              # ← race
        _scheduler = SignalAttachmentScheduler()  # ← duplicate instances

Fix Action

Fix

Add import threading and _scheduler_lock = threading.Lock(). Apply double-checked locking to get_scheduler(). Same pattern as #24731.

Code Example

_scheduler: Optional[SignalAttachmentScheduler] = None  # no lock

def get_scheduler() -> SignalAttachmentScheduler:
    global _scheduler
    if _scheduler is None:              # ← race
        _scheduler = SignalAttachmentScheduler()  # ← duplicate instances
RAW_BUFFERClick to expand / collapse

Summary

gateway/platforms/signal_rate_limit.py:get_scheduler() (line 351) uses a bare if _scheduler is None: guard with no lock. Gateway platforms dispatch concurrent coroutines, so two concurrent calls can both see _scheduler is None, both construct SignalAttachmentScheduler(), and the loser's instance is orphaned with its internal token-bucket state.

_scheduler: Optional[SignalAttachmentScheduler] = None  # no lock

def get_scheduler() -> SignalAttachmentScheduler:
    global _scheduler
    if _scheduler is None:              # ← race
        _scheduler = SignalAttachmentScheduler()  # ← duplicate instances

Impact

  • Two scheduler instances means rate-limit tokens are not shared — each instance has its own bucket, effectively doubling the allowed burst rate.
  • _reset_scheduler() (test helper) already shows the singleton is expected to be unique.
  • No threading import in this file — needs to be added.

Fix

Add import threading and _scheduler_lock = threading.Lock(). Apply double-checked locking to get_scheduler(). Same pattern as #24731.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

hermes - 💡(How to fix) Fix bug(signal_rate_limit): TOCTOU race in get_scheduler() — concurrent calls can create duplicate SignalAttachmentScheduler instances