Contain provider failures during SHM gap repair

Partially backport only the `get_hist()` exception catch from
0846cbd4a4, authored by goodboy
<goodboy_foss@protonmail.com>. 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: 0846cbd4a4
Prompt-IO: ai/prompt-io/20261106T185628Z_495c0915_215e9b_prompt_io.md

(this patch was generated in some part by `codex` (`openai`))
wkt/land_flake_update
Gud Boi 2026-11-06 15:20:58 -05:00
parent 495c091546
commit 51795a4e07
5 changed files with 184 additions and 11 deletions

View File

@ -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.

View File

@ -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.

View File

@ -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/

View File

@ -239,15 +239,26 @@ async def maybe_fill_null_segments(
await tractor.pause() await tractor.pause()
break break
( try:
array, (
next_start_dt, array,
next_end_dt, next_start_dt,
) = await get_hist( next_end_dt,
timeframe, ) = await get_hist(
start_dt=start_dt, timeframe,
end_dt=end_dt, 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: if array.size == 0:
log.warning( log.warning(

View File

@ -7,11 +7,19 @@ from types import SimpleNamespace
import numpy as np import numpy as np
from pendulum import datetime from pendulum import datetime
import pytest
import trio 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.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: def run_exhausted_backfill(get_hist) -> None:
@ -52,6 +60,82 @@ def test_data_unavailable_completes() -> None:
run_exhausted_backfill(get_hist) 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: def test_empty_frame_completes() -> None:
''' '''
An empty provider frame is checked before endpoint indexing. An empty provider frame is checked before endpoint indexing.