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()
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(

View File

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