2026-07-22 02:11:10 +00:00
|
|
|
'''
|
|
|
|
|
Deterministic history-backfill regressions.
|
|
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
from functools import partial
|
|
|
|
|
from types import SimpleNamespace
|
|
|
|
|
|
|
|
|
|
import numpy as np
|
2026-07-22 02:50:09 +00:00
|
|
|
from pendulum import (
|
|
|
|
|
datetime,
|
|
|
|
|
from_timestamp,
|
|
|
|
|
)
|
|
|
|
|
import pytest
|
2026-07-22 02:11:10 +00:00
|
|
|
import trio
|
|
|
|
|
|
|
|
|
|
from piker.brokers import DataUnavailable
|
|
|
|
|
from piker.data._source import def_iohlcv_fields
|
2026-07-22 02:50:09 +00:00
|
|
|
from piker.tsp._history import (
|
|
|
|
|
notify_backfill,
|
|
|
|
|
publish_latest_frame,
|
|
|
|
|
start_backfill,
|
|
|
|
|
)
|
2026-07-22 02:11:10 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def run_exhausted_backfill(get_hist) -> None:
|
|
|
|
|
'''
|
|
|
|
|
Run a provider-exhaustion case without actor services.
|
|
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
async def main() -> None:
|
|
|
|
|
with trio.fail_after(0.5):
|
|
|
|
|
async with trio.open_nursery() as nursery:
|
|
|
|
|
await nursery.start(partial(
|
|
|
|
|
start_backfill,
|
|
|
|
|
get_hist=get_hist,
|
|
|
|
|
def_frame_duration=None,
|
|
|
|
|
mod=SimpleNamespace(name='fake'),
|
|
|
|
|
mkt=SimpleNamespace(fqme='x.fake'),
|
|
|
|
|
shm=object(),
|
|
|
|
|
timeframe=60,
|
|
|
|
|
backfill_from_shm_index=100,
|
|
|
|
|
backfill_from_dt=datetime(2026, 1, 2),
|
|
|
|
|
sampler_stream=object(),
|
|
|
|
|
backfill_until_dt=datetime(2026, 1, 1),
|
|
|
|
|
storage=None,
|
|
|
|
|
write_tsdb=False,
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
trio.run(main)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_data_unavailable_completes() -> None:
|
|
|
|
|
'''
|
|
|
|
|
Provider exhaustion exits without orphaned completion waits.
|
|
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
async def get_hist(*args, **kwargs):
|
|
|
|
|
raise DataUnavailable('history exhausted')
|
|
|
|
|
|
|
|
|
|
run_exhausted_backfill(get_hist)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_empty_frame_completes() -> None:
|
|
|
|
|
'''
|
|
|
|
|
An empty provider frame is checked before endpoint indexing.
|
|
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
frame = np.empty(
|
|
|
|
|
0,
|
|
|
|
|
dtype=np.dtype(def_iohlcv_fields),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
async def get_hist(*args, **kwargs):
|
|
|
|
|
end_dt = kwargs['end_dt']
|
|
|
|
|
return frame, end_dt, end_dt
|
|
|
|
|
|
|
|
|
|
run_exhausted_backfill(get_hist)
|
2026-07-22 02:50:09 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_storage_receives_full_provider_delta() -> None:
|
|
|
|
|
'''
|
|
|
|
|
SHM capacity truncation does not truncate durable history.
|
|
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
frame = np.zeros(
|
|
|
|
|
2,
|
|
|
|
|
dtype=np.dtype(def_iohlcv_fields),
|
|
|
|
|
)
|
|
|
|
|
frame['index'] = [0, 1]
|
|
|
|
|
frame['time'] = [60, 120]
|
|
|
|
|
|
|
|
|
|
events: list[str] = []
|
|
|
|
|
|
|
|
|
|
class Shm:
|
|
|
|
|
def __init__(self) -> None:
|
|
|
|
|
self.pushed: list[np.ndarray] = []
|
|
|
|
|
|
|
|
|
|
def push(self, array, **kwargs) -> None:
|
|
|
|
|
self.pushed.append(array.copy())
|
|
|
|
|
events.append('shm')
|
|
|
|
|
|
|
|
|
|
class Sampler:
|
|
|
|
|
async def send(self, msg) -> None:
|
|
|
|
|
events.append('sampler')
|
|
|
|
|
|
|
|
|
|
class Storage:
|
|
|
|
|
def __init__(self) -> None:
|
|
|
|
|
self.frames: list[np.ndarray] = []
|
|
|
|
|
|
|
|
|
|
async def update_ohlcv(
|
|
|
|
|
self,
|
|
|
|
|
fqme: str,
|
|
|
|
|
ohlcv: np.ndarray,
|
|
|
|
|
timeframe: int,
|
|
|
|
|
) -> None:
|
|
|
|
|
self.frames.append(ohlcv.copy())
|
|
|
|
|
events.append('storage')
|
|
|
|
|
|
|
|
|
|
shm = Shm()
|
|
|
|
|
storage = Storage()
|
|
|
|
|
mkt = SimpleNamespace(
|
|
|
|
|
fqme='x.test',
|
|
|
|
|
dst=SimpleNamespace(atype='crypto'),
|
|
|
|
|
src=SimpleNamespace(atype='crypto_currency'),
|
|
|
|
|
get_fqme=lambda **kwargs: 'x.test',
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
async def get_hist(*args, **kwargs):
|
|
|
|
|
return (
|
|
|
|
|
frame,
|
|
|
|
|
from_timestamp(60),
|
|
|
|
|
from_timestamp(120),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
async def main() -> None:
|
|
|
|
|
with trio.fail_after(0.5):
|
|
|
|
|
await start_backfill(
|
|
|
|
|
get_hist=get_hist,
|
|
|
|
|
def_frame_duration=None,
|
|
|
|
|
mod=SimpleNamespace(name='fake'),
|
|
|
|
|
mkt=mkt,
|
|
|
|
|
shm=shm,
|
|
|
|
|
timeframe=60,
|
|
|
|
|
backfill_from_shm_index=1,
|
|
|
|
|
backfill_from_dt=from_timestamp(180),
|
|
|
|
|
sampler_stream=Sampler(),
|
|
|
|
|
backfill_until_dt=from_timestamp(60),
|
|
|
|
|
storage=storage,
|
|
|
|
|
write_tsdb=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
trio.run(main)
|
|
|
|
|
assert shm.pushed[0]['time'].tolist() == [120]
|
|
|
|
|
assert storage.frames[0]['time'].tolist() == [60, 120]
|
|
|
|
|
assert events == ['storage', 'shm', 'sampler']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_latest_frame_is_persisted_before_shm() -> None:
|
|
|
|
|
'''
|
|
|
|
|
Startup persists the most-recent provider frame before publication.
|
|
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
frame = np.zeros(
|
|
|
|
|
2,
|
|
|
|
|
dtype=np.dtype(def_iohlcv_fields),
|
|
|
|
|
)
|
|
|
|
|
frame['time'] = [60, 120]
|
|
|
|
|
events: list[str] = []
|
|
|
|
|
|
|
|
|
|
class Storage:
|
|
|
|
|
async def update_ohlcv(self, *args) -> None:
|
|
|
|
|
events.append('storage')
|
|
|
|
|
|
|
|
|
|
class Shm:
|
|
|
|
|
def push(self, array, **kwargs) -> None:
|
|
|
|
|
events.append('shm')
|
|
|
|
|
|
|
|
|
|
async def main() -> None:
|
|
|
|
|
with trio.fail_after(0.5):
|
|
|
|
|
await publish_latest_frame(
|
|
|
|
|
storage=Storage(),
|
|
|
|
|
mkt=SimpleNamespace(
|
|
|
|
|
fqme='x.test',
|
|
|
|
|
dst=SimpleNamespace(atype='crypto'),
|
|
|
|
|
src=SimpleNamespace(atype='crypto_currency'),
|
|
|
|
|
get_fqme=lambda **kwargs: 'x.test',
|
|
|
|
|
),
|
|
|
|
|
shm=Shm(),
|
|
|
|
|
array=frame,
|
|
|
|
|
timeframe=60,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
trio.run(main)
|
|
|
|
|
assert events == ['storage', 'shm']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_backfill_notification_timeout_is_bounded(
|
|
|
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
|
|
|
) -> None:
|
|
|
|
|
'''
|
|
|
|
|
A wedged sampler can not indefinitely shield actor teardown.
|
|
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
'piker.tsp._history._notify_timeout_s',
|
|
|
|
|
0.01,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
class Sampler:
|
|
|
|
|
async def send(self, msg) -> None:
|
|
|
|
|
await trio.sleep_forever()
|
|
|
|
|
|
|
|
|
|
async def main() -> None:
|
|
|
|
|
with trio.fail_after(0.1):
|
|
|
|
|
await notify_backfill(
|
|
|
|
|
Sampler(),
|
|
|
|
|
SimpleNamespace(fqme='x.test'),
|
|
|
|
|
60,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
trio.run(main)
|