Skip to content

Commit aef83ea

Browse files
committed
Emit an event right before the event loop starts.
1 parent f17d10c commit aef83ea

7 files changed

Lines changed: 72 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Features
66

77
* `fees.Percentage` now supports charging fees in base currency.
8+
* Both `BacktestingDispatcher` and `RealtimeDispatcher` now support `subscribe_event_loop_started` to register handlers that are called once when the dispatch loop starts.
89

910
## 1.10.2
1011

basana/core/dispatcher/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from .base import (
2020
EventDispatcher,
2121
EventHandler,
22+
LoopStartedHandler,
2223
SchedulerJob,
2324
)
2425

basana/core/dispatcher/backtesting.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ def _set_now(self, now: datetime.datetime):
5353
self._last_dt = now
5454

5555
async def _dispatch_loop(self):
56+
self._last_dt = self._event_mux.peek_next_event_dt()
57+
await self._notify_loop_started()
58+
5659
while not self.stopped:
5760
# Time is driven by event timestamps, so "now" advances only when events are processed.
5861
next_dt = self._event_mux.peek_next_event_dt()

basana/core/dispatcher/base.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333

3434
logger = logging.getLogger(__name__)
3535
EventHandler = Callable[[core_event.Event], Awaitable[Any]]
36+
LoopStartedHandler = Callable[[], Awaitable[Any]]
3637

3738

3839
# Indices for prefetched event tuples stored in the EventMultiplexer heap.
@@ -114,6 +115,7 @@ def __init__(self, max_concurrent: int):
114115
self._event_handlers: Dict[core_event.EventSource, List[EventHandler]] = defaultdict(list)
115116
self._sniffers_pre: List[EventHandler] = []
116117
self._sniffers_post: List[EventHandler] = []
118+
self._loop_started_handlers: List[LoopStartedHandler] = []
117119
self._producers: Set[core_event.Producer] = set()
118120
# Task group for core tasks like producers and dispatch loop.
119121
self._core_tasks: Optional[helpers.TaskGroup] = None
@@ -179,6 +181,17 @@ def subscribe_all(self, event_handler: EventHandler, front_run: bool = False):
179181
if event_handler not in sniffers:
180182
sniffers.append(event_handler)
181183

184+
def subscribe_event_loop_started(self, handler: LoopStartedHandler):
185+
"""Registers an async callable that will be called once when the event loop starts.
186+
187+
:param handler: An async callable that receives no arguments.
188+
"""
189+
190+
assert not self._running, "Subscribing once we're running is not currently supported."
191+
192+
if handler not in self._loop_started_handlers:
193+
self._loop_started_handlers.append(handler)
194+
182195
def schedule(self, when: datetime.datetime, job: SchedulerJob):
183196
"""Schedules a function to be executed at a given time.
184197
@@ -248,6 +261,10 @@ async def _core_task_group(self):
248261
finally:
249262
self._core_tasks = None
250263

264+
async def _notify_loop_started(self):
265+
if self._loop_started_handlers:
266+
await gather_no_raise(*[handler() for handler in self._loop_started_handlers])
267+
251268
async def _dispatch_event(self, event: core_event.Event, handlers: List[EventHandler]):
252269
if self._debug_enabled:
253270
logger.debug(logs.StructuredMessage(

basana/core/dispatcher/realtime.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ def subscribe_idle(self, idle_handler: IdleHandler):
5555
self._idle_handlers.append(idle_handler)
5656

5757
async def _dispatch_loop(self):
58+
await self._notify_loop_started()
5859
while not self.stopped:
5960
now = dt.utc_now()
6061
# Feed the task pool with scheduled jobs and events that are ready for processing.

tests/test_backtesting_dispatcher.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,37 @@ def test_now_fails_if_no_events_were_processed(backtesting_dispatcher):
219219
backtesting_dispatcher.now()
220220

221221

222+
async def test_event_loop_started(backtesting_dispatcher):
223+
started = []
224+
225+
async def on_started():
226+
assert backtesting_dispatcher.now_available
227+
started.append(True)
228+
229+
src = event.FifoQueueEventSource(events=[event.Event(dt.utc_now())])
230+
backtesting_dispatcher.subscribe(src, lambda e: None)
231+
backtesting_dispatcher.subscribe_event_loop_started(on_started)
232+
await backtesting_dispatcher.run()
233+
234+
assert len(started) == 1
235+
236+
237+
async def test_duplicate_loop_started_subscription_is_ignored(backtesting_dispatcher):
238+
started = []
239+
240+
async def on_started():
241+
assert backtesting_dispatcher.now_available
242+
started.append(True)
243+
244+
src = event.FifoQueueEventSource(events=[event.Event(dt.utc_now())])
245+
backtesting_dispatcher.subscribe(src, lambda e: None)
246+
backtesting_dispatcher.subscribe_event_loop_started(on_started)
247+
backtesting_dispatcher.subscribe_event_loop_started(on_started)
248+
await backtesting_dispatcher.run()
249+
250+
assert len(started) == 1
251+
252+
222253
async def test_recursive_schedule_bug(backtesting_dispatcher):
223254
jobs_processed = 0
224255

tests/test_realtime_dispatcher.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,24 @@ async def on_idle():
218218
assert handler_calls == 2
219219

220220

221+
async def test_event_loop_started(realtime_dispatcher):
222+
started = []
223+
224+
async def on_started():
225+
started.append(True)
226+
227+
async def on_idle():
228+
realtime_dispatcher.stop()
229+
230+
async def test_main():
231+
realtime_dispatcher.subscribe_event_loop_started(on_started)
232+
realtime_dispatcher.subscribe_idle(on_idle)
233+
await realtime_dispatcher.run()
234+
assert len(started) == 1
235+
236+
await asyncio.wait_for(test_main(), 2)
237+
238+
221239
async def test_cancelation_is_forwarded(realtime_dispatcher):
222240
async def test_main():
223241
with pytest.raises(asyncio.CancelledError):

0 commit comments

Comments
 (0)