Skip to content

fix(history): per-request events to avoid KeyError + shared-event race#52

Merged
rundef merged 1 commit intorundef:mainfrom
olivier-babelcast:fix/historical-race
Apr 20, 2026
Merged

fix(history): per-request events to avoid KeyError + shared-event race#52
rundef merged 1 commit intorundef:mainfrom
olivier-babelcast:fix/historical-race

Conversation

@olivier-babelcast
Copy link
Copy Markdown
Contributor

Summary

Two race conditions in HistoryPlant stemming from a single shared asyncio.Event per plant are fixed by switching to per-request events keyed by {symbol}_{type} (time bars) or {symbol} (ticks).

Bug A — KeyError on empty response

When Rithmic returns only the is_last_bar marker with no data bars, the shared event fires within the 5s timeout, so the TimeoutError fallback path is skipped. historical_*_data.pop(key) then raises KeyError because no callback ever populated that key.

Fix: pop(key, []) with an empty-list default — empty responses return [] instead of crashing.

Bug B — Concurrent callers share one event

The second get_historical_time_bars call reassigns self.historical_time_bar_event to a fresh Event, overwriting the first caller's event. The first response then fires the second caller's event, waking caller B before its own data has arrived.

Fix: Each call allocates its own asyncio.Event, registers it in a dict keyed by {symbol}_{type} (or {symbol} for ticks), awaits, and cleans up in a finally. In _process_response, the is_last_bar path looks up the specific event by key from response.symbol + response.type and sets only that one.

Real-world trigger

KeyError: 'MYMM6_2' on production bots during low-volume historical-bar backfill (seen 2026-04-13 03:29 and 05:35 ET).

Test plan

  • Existing test suite still passes — 26/26 pre-existing tests green
  • 3 new regression tests in tests/test_history_races.py:
    • test_empty_response_returns_empty_list — covers Bug A for time bars
    • test_empty_tick_response_returns_empty_list — covers Bug A for ticks
    • test_concurrent_different_symbols — covers Bug B with two callers racing
  • Full suite: 29/29 passing on upstream main base
  • Fix deployed to one live trading bot (dart_propfirms) on 2026-04-13 — no KeyError: 'MYMM*_2' logged since

Files changed

  • async_rithmic/plants/history.py — per-request events + pop-with-default
  • tests/test_history_races.py — 3 new regression tests

The historical bar/tick retrieval had two race conditions stemming from a
single shared asyncio.Event per plant:

  A) Empty response: when Rithmic returns only the is_last_bar marker (no
     data bars), the event fires within 5s so the TimeoutError fallback
     is skipped, and historical_*_data.pop(key) raises KeyError because
     no callback ever populated that key.

  B) Concurrent calls: the second get_historical_time_bars call replaces
     the shared event instance. The first call's response then sets the
     second call's event, waking caller B before its data has arrived.

Replace the shared event with a dict of per-request events keyed by
"{symbol}_{type}" (time bars) or "{symbol}" (ticks). Each call allocates
its own Event, registers it, waits, and cleans up in a finally. The pop
uses a default of [] so empty responses return an empty list instead of
raising.

In _process_response, the is_last_bar path now looks up the specific
event by key from response.symbol + response.type and sets only that one.

3 regression tests added under tests/test_history_races.py. Full suite:
29 passed (26 existing + 3 new).

Symptom that triggered this fix: KeyError: 'MYMM6_2' on live_propfirms
during low-volume backfill (seen 2026-04-13 03:29 and 05:35 ET).
@rundef rundef merged commit 40ed80a into rundef:main Apr 20, 2026
4 checks passed
@rundef
Copy link
Copy Markdown
Owner

rundef commented Apr 20, 2026

Thank you for your contribution !

@olivier-babelcast
Copy link
Copy Markdown
Contributor Author

I typically run it live for a few days before pushing the PR

@olivier-babelcast olivier-babelcast deleted the fix/historical-race branch April 22, 2026 06:34
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.

2 participants