Contain provider failures during SHM gap repair
Partially backport only the `get_hist()` exception catch fromwkt/land_flake_update0846cbd4a4, 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:0846cbd4a4Prompt-IO: ai/prompt-io/20261106T185628Z_495c0915_215e9b_prompt_io.md (this patch was generated in some part by `codex` (`openai`))
parent
495c091546
commit
51795a4e07
|
|
@ -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.
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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/
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue