From 51795a4e072902df205953d6402e17a178ee0ad1 Mon Sep 17 00:00:00 2001 From: goodboy Date: Fri, 6 Nov 2026 15:20:58 -0500 Subject: [PATCH] Contain provider failures during SHM gap repair Partially backport only the `get_hist()` exception catch from 0846cbd4a4197e47415689cf5e9d146ba3174c32, authored by goodboy . Keep the existing gap fallback. A timed-out IB query raises `NoData` from null-segment repair. Letting it escape cancels sibling history and live quote tasks while `pikerd` can remain alive. Catch `NoData` and `DataUnavailable` locally and stop querying this repair pass. Add a regression using real null detection and a waiting sibling in the same nursery. Both provider failures must return without canceling the sibling. The two cases failed before this catch; the focused history files passed 17 tests with it. Partial-backport-of: 0846cbd4a4197e47415689cf5e9d146ba3174c32 Prompt-IO: ai/prompt-io/20261106T185628Z_495c0915_215e9b_prompt_io.md (this patch was generated in some part by `codex` (`openai`)) --- ...61106T185628Z_495c0915_215e9b_prompt_io.md | 45 ++++++++++ ...6T185628Z_495c0915_215e9b_prompt_io.raw.md | 22 +++++ ai/prompt-io/README.md | 11 +++ piker/tsp/_history.py | 29 ++++-- tests/test_history_backfill.py | 88 ++++++++++++++++++- 5 files changed, 184 insertions(+), 11 deletions(-) create mode 100644 ai/prompt-io/20261106T185628Z_495c0915_215e9b_prompt_io.md create mode 100644 ai/prompt-io/20261106T185628Z_495c0915_215e9b_prompt_io.raw.md create mode 100644 ai/prompt-io/README.md diff --git a/ai/prompt-io/20261106T185628Z_495c0915_215e9b_prompt_io.md b/ai/prompt-io/20261106T185628Z_495c0915_215e9b_prompt_io.md new file mode 100644 index 00000000..fc652098 --- /dev/null +++ b/ai/prompt-io/20261106T185628Z_495c0915_215e9b_prompt_io.md @@ -0,0 +1,45 @@ +--- +created_at: 2026-11-06T18:56:28.621577+00:00 +generated_by: + harness: codex + provider: OpenAI +git_ref: 495c0915 +scope: code +substantive: true +raw_file: 20261106T185628Z_495c0915_215e9b_prompt_io.raw.md +--- + +## Prompt + +User reported a different-feed overlap failure and asked whether +a fix could be extracted without importing broader history work. +PDB confirmed MNQ March 2027 one-second history timed out and +returned None, producing NoData. After continuing PDB, sibling +history and quote tasks terminated while pikerd remained alive. +User requested continuing with the narrow backport. + +## Response summary + +Partial backport of the NoData/DataUnavailable catch from +0846cbd4a4197e47415689cf5e9d146ba3174c32 into +maybe_fill_null_segments(). The existing gap fallback remains in use. +A new parametrized regression uses real null detection and verifies +that a sibling in the same nursery survives the failed repair. + +Both new cases failed before the catch. The focused history suites +passed after it: 17 passed. This does not qualify live overlap or fix +IB reset churn or the separate Tractor completion-callback assertion. + +## Files changed + +- `piker/tsp/_history.py`: existing downstream exception catch. +- `tests/test_history_backfill.py`: sibling survival regression. + +## Human edits + +User supplied live reproduction, PDB locals and cancellation logs, +and explicitly constrained scope to a small history extraction. +The production catch is copied from existing downstream commit +0846cbd4, not newly designed here. New regression is agent-authored; +no direct human source edits observed during this patch. Review and +live qualification remain with the user. diff --git a/ai/prompt-io/20261106T185628Z_495c0915_215e9b_prompt_io.raw.md b/ai/prompt-io/20261106T185628Z_495c0915_215e9b_prompt_io.raw.md new file mode 100644 index 00000000..a0f6ab0a --- /dev/null +++ b/ai/prompt-io/20261106T185628Z_495c0915_215e9b_prompt_io.raw.md @@ -0,0 +1,22 @@ +--- +created_at: 2026-11-06T18:56:28.621577+00:00 +generated_by: + harness: codex + provider: OpenAI +git_ref: 495c0915 +diff_cmd: git diff HEAD~1..HEAD +--- + +> `git diff HEAD~1..HEAD -- piker/tsp/_history.py` + +> `git diff HEAD~1..HEAD -- tests/test_history_backfill.py` + +Partial backport of the NoData/DataUnavailable catch from +0846cbd4a4197e47415689cf5e9d146ba3174c32 into +maybe_fill_null_segments(). The existing gap fallback remains in use. +A new parametrized regression uses real null detection and verifies +that a sibling in the same nursery survives the failed repair. + +Both new cases failed before the catch. The focused history suites +passed after it: 17 passed. This does not qualify live overlap or fix +IB reset churn or the separate Tractor completion-callback assertion. diff --git a/ai/prompt-io/README.md b/ai/prompt-io/README.md new file mode 100644 index 00000000..5ed19630 --- /dev/null +++ b/ai/prompt-io/README.md @@ -0,0 +1,11 @@ +# AI Prompt I/O Log + +This directory records substantive AI-assisted changes, prompts, +original harness identity, and unedited outputs. Human contributors +remain accountable for code decisions. + +Records follow the [NLNet generative AI policy][policy]. Each +structured entry links its immutable `.raw.md` partner. Code output +uses Git diff references to avoid duplication. + +[policy]: https://nlnet.nl/foundation/policies/generativeAI/ diff --git a/piker/tsp/_history.py b/piker/tsp/_history.py index e99e1b92..8dc65ff8 100644 --- a/piker/tsp/_history.py +++ b/piker/tsp/_history.py @@ -239,15 +239,26 @@ async def maybe_fill_null_segments( await tractor.pause() break - ( - array, - next_start_dt, - next_end_dt, - ) = await get_hist( - timeframe, - start_dt=start_dt, - end_dt=end_dt, - ) + try: + ( + array, + next_start_dt, + next_end_dt, + ) = await get_hist( + timeframe, + start_dt=start_dt, + end_dt=end_dt, + ) + except ( + NoData, + DataUnavailable, + ) as exc: + log.warning( + f'Provider could not repair SHM gap:\n' + f'{start_dt} -> {end_dt}\n' + f'{exc}\n' + ) + break if array.size == 0: log.warning( diff --git a/tests/test_history_backfill.py b/tests/test_history_backfill.py index 1a58e460..a3bdb098 100644 --- a/tests/test_history_backfill.py +++ b/tests/test_history_backfill.py @@ -7,11 +7,19 @@ from types import SimpleNamespace import numpy as np from pendulum import datetime +import pytest import trio +from trio import TaskStatus -from piker.brokers import DataUnavailable +from piker.brokers import ( + DataUnavailable, + NoData, +) from piker.data._source import def_iohlcv_fields -from piker.tsp._history import start_backfill +from piker.tsp._history import ( + maybe_fill_null_segments, + start_backfill, +) def run_exhausted_backfill(get_hist) -> None: @@ -52,6 +60,82 @@ def test_data_unavailable_completes() -> None: run_exhausted_backfill(get_hist) +@pytest.mark.parametrize('error_type', [ + NoData, + DataUnavailable, +]) +def test_gap_repair_failure_preserves_sibling( + error_type: type[Exception], +) -> None: + ''' + An unavailable gap query must not cancel sibling feed tasks. + + Use real null detection and a provider that raises on its first + query. After repair returns, release a sibling waiting in the + same nursery and require its acknowledgement. This checks local + failure containment, not Gateway reset or quote reconnection. + + ''' + frame: np.ndarray = np.zeros( + 5, + dtype=np.dtype(def_iohlcv_fields), + ) + frame['index'] = np.arange(5) + frame['time'] = datetime(2026, 11, 6).timestamp() + np.arange(5) + frame['time'][1:3] = 0 + frame['close'][0] = 100 + calls: list[dict] = [] + + async def get_hist(timeframe: float, **kwargs) -> None: + assert timeframe == 1 + assert kwargs['start_dt'] < kwargs['end_dt'] + calls.append(kwargs) + raise error_type('provider cannot repair gap') + + async def main() -> None: + repaired: trio.Event = trio.Event() + resume: trio.Event = trio.Event() + survived: trio.Event = trio.Event() + send, receive = trio.open_memory_channel(1) + + async def sibling( + task_status: TaskStatus[None] = trio.TASK_STATUS_IGNORED, + ) -> None: + task_status.started() + await resume.wait() + survived.set() + + async def repair() -> None: + await maybe_fill_null_segments( + shm=SimpleNamespace(array=frame, _array=frame), + timeframe=1, + get_hist=get_hist, + sampler_stream=send, + mkt=SimpleNamespace(fqme='mnq.cme.fake'), + backfill_until_dt=datetime(2026, 11, 5), + ) + repaired.set() + + with trio.fail_after(1): + async with trio.open_nursery() as nursery: + await nursery.start(sibling) + nursery.start_soon(repair) + await repaired.wait() + resume.set() + await survived.wait() + nursery.cancel_scope.cancel() + + assert len(calls) == 1 + assert receive.receive_nowait() == { + 'broadcast_all': { + 'backfilling': ('mnq.cme.fake', 1), + }, + } + assert (frame['time'] > 0).all() + + trio.run(main) + + def test_empty_frame_completes() -> None: ''' An empty provider frame is checked before endpoint indexing.