134 lines
4.2 KiB
Python
134 lines
4.2 KiB
Python
|
|
'''
|
||
|
|
EMS broadcast ownership regressions.
|
||
|
|
|
||
|
|
'''
|
||
|
|
from contextlib import asynccontextmanager as acm
|
||
|
|
|
||
|
|
import trio
|
||
|
|
from trio.testing import wait_all_tasks_blocked
|
||
|
|
from tractor.trionics import broadcast_receiver
|
||
|
|
|
||
|
|
from piker.clearing import _ems
|
||
|
|
|
||
|
|
|
||
|
|
def test_dark_clearing_owns_non_strict_subscription(
|
||
|
|
monkeypatch,
|
||
|
|
) -> None:
|
||
|
|
'''
|
||
|
|
Dark clearing must not mutate another feed consumer's lag policy.
|
||
|
|
|
||
|
|
`clear_dark_triggers()` previously assigned `_raise_on_lag` on
|
||
|
|
whichever root or temporary receiver was stored in `Flume.stream`.
|
||
|
|
That changed private shared state and made behavior depend on feed
|
||
|
|
cache ownership. Supply a stream whose subscription records the
|
||
|
|
public lag-policy argument and yields a distinct child. Replace
|
||
|
|
the hot trigger loop with a checkpointing probe, then prove the
|
||
|
|
non-strict child is entered, passed to the core, and closed only
|
||
|
|
after processing completes while the source remains untouched.
|
||
|
|
|
||
|
|
'''
|
||
|
|
events: list[tuple] = []
|
||
|
|
child = object()
|
||
|
|
|
||
|
|
class QuoteStream:
|
||
|
|
'''
|
||
|
|
Record public subscription ownership without quote traffic.
|
||
|
|
|
||
|
|
'''
|
||
|
|
@acm
|
||
|
|
async def subscribe(self, raise_on_lag: bool = True):
|
||
|
|
'''
|
||
|
|
Yield one dedicated child for the wrapper lifetime.
|
||
|
|
|
||
|
|
'''
|
||
|
|
events.append(('enter', raise_on_lag))
|
||
|
|
try:
|
||
|
|
yield child
|
||
|
|
finally:
|
||
|
|
events.append(('exit', raise_on_lag))
|
||
|
|
|
||
|
|
async def clear_core(*args) -> None:
|
||
|
|
'''
|
||
|
|
Prove the child remains owned while processing runs.
|
||
|
|
|
||
|
|
'''
|
||
|
|
events.append(('core', args[2] is child))
|
||
|
|
await trio.lowlevel.checkpoint()
|
||
|
|
|
||
|
|
monkeypatch.setattr(
|
||
|
|
_ems,
|
||
|
|
'_clear_dark_triggers',
|
||
|
|
clear_core,
|
||
|
|
)
|
||
|
|
|
||
|
|
async def main() -> None:
|
||
|
|
stream = QuoteStream()
|
||
|
|
await _ems.clear_dark_triggers(
|
||
|
|
object(),
|
||
|
|
object(),
|
||
|
|
stream,
|
||
|
|
'ib',
|
||
|
|
'nvda.nasdaq.ib',
|
||
|
|
object(),
|
||
|
|
)
|
||
|
|
assert not hasattr(stream, '_raise_on_lag')
|
||
|
|
|
||
|
|
trio.run(main)
|
||
|
|
assert events == [
|
||
|
|
('enter', False),
|
||
|
|
('core', True),
|
||
|
|
('exit', False),
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
def test_dark_clearing_with_real_broadcast() -> None:
|
||
|
|
'''
|
||
|
|
Dark clearing must consume quotes without owning sibling lifetime.
|
||
|
|
|
||
|
|
Exercise the production trigger loop and real Tractor broadcaster
|
||
|
|
together. Deliver one trade tick to both EMS and a sibling cursor;
|
||
|
|
DarkBook.lasts proves EMS processed it. Cancel only the EMS task
|
||
|
|
and verify its subscription disappears while the sibling still
|
||
|
|
receives the next tick with its strict lag policy intact.
|
||
|
|
|
||
|
|
The book has no order triggers, so this requires no broker or
|
||
|
|
account. Unlike the imported wrapper test, no stream or clearing
|
||
|
|
function is replaced.
|
||
|
|
|
||
|
|
'''
|
||
|
|
async def main() -> None:
|
||
|
|
symbol = 'qqq.nasdaq.ib'
|
||
|
|
book = _ems.DarkBook(broker='ib', triggers={}, lasts={})
|
||
|
|
tx, rx = trio.open_memory_channel(8)
|
||
|
|
root = broadcast_receiver(rx, 8)
|
||
|
|
quote = {symbol: {'ticks': [{'type': 'trade', 'price': 100}]}}
|
||
|
|
|
||
|
|
with trio.fail_after(2):
|
||
|
|
async with tx, rx, root, root.subscribe() as sibling:
|
||
|
|
async with trio.open_nursery() as nursery:
|
||
|
|
nursery.start_soon(
|
||
|
|
_ems.clear_dark_triggers,
|
||
|
|
None,
|
||
|
|
None,
|
||
|
|
root,
|
||
|
|
'ib',
|
||
|
|
symbol,
|
||
|
|
book,
|
||
|
|
)
|
||
|
|
await wait_all_tasks_blocked()
|
||
|
|
assert len(root._state.subs) == 3
|
||
|
|
await tx.send(quote)
|
||
|
|
assert await sibling.receive() == quote
|
||
|
|
await wait_all_tasks_blocked()
|
||
|
|
assert book.lasts[symbol] == 100
|
||
|
|
assert root._raise_on_lag
|
||
|
|
assert sibling._raise_on_lag
|
||
|
|
nursery.cancel_scope.cancel()
|
||
|
|
|
||
|
|
assert len(root._state.subs) == 2
|
||
|
|
assert not sibling._closed
|
||
|
|
await tx.send(quote)
|
||
|
|
assert await sibling.receive() == quote
|
||
|
|
|
||
|
|
trio.run(main)
|