Repository navigation
bench: spike the v0.5 stream error paths behind a stream_spike feature (#256) - #389
Conversation
#256) Research for #256: before M1 builds to_batches() and the PyCapsule stream, can contract §15.1 (Python reads raise the LTSeq class) and §16.4 (the C stream ends with EIO/EINVAL and a message starting with the class name) be built, what does the Rust iterator cost, and does closing the reader stop the execution (OD-X6)? - `stream_spike` cargo feature (no shipped build carries it): `_SpikeBatches`, a PyO3 iterator that to_batches() wraps in RecordBatchReader.from_batches, with three GIL modes; a C stream with its own callbacks for errno and get_last_error; a classifier from DataFusion's error chain to the §20.1 classes; a live-execution counter. Stand-in classes and helpers are in ltseq._stream_spike. - benchmarks/stream_errors/: probe.py (every error source against every read path, pyarrow, DuckDB and Polars, and close/drop), bench.py (#251's m3 pipeline over the R3 input, idle and with a busy Python thread), results and REPORT.md. - py-ltseq/tests/test_stream_spike.py holds the spike to the report; it skips on a default build. CI runs clippy and the Rust tests on the feature. Findings: Python reads keep the class for every class tested, and pyarrow raises OSError/ArrowInvalid with the C stream's message intact; DuckDB and Polars carry the message under their own class. The iterator costs nothing measurable at 8,192-row batches, 1.1-1.4x at 1,024, and about three GIL waits a batch when another Python thread is busy. close() on the from_batches reader does not stop the execution (dropping it does), and a reader that stops on close() loses the class, so OD-X6 and §15.1's error sentence cannot both hold with pyarrow 26.0.1; REPORT.md puts the choice to the owner.
AI Code Review — Round 1Reviewed HEAD: FindingsR1-F1 [blocked]
|
…am spike (#256) Review round 1 of the #256 spike. R1-F1: the report said no pyarrow.RecordBatchReader can keep the LTSeq class on Python reads and also stop the execution on close(). That holds for the two readers pyarrow's Python API builds, not for the class. _closing_reader.pyx subclasses RecordBatchReader in Cython against pyarrow's shipped lib.pxd: it is allocated with __new__, shares the C++ reader from_batches built over the Rust iterator, keeps the iterator, and its close() closes the iterator. Its inherited reads raise the stand-in class for every injected class and real error, close() and leaving a with block take the live executions from 1 to 0, and pa.table, DuckDB and Polars read it. closing_reader.py compiles it; probe.py and test_stream_spike.py exercise it. REPORT.md adds it as option (e), with its C++ build and pyarrow ABI coupling, and now recommends keeping §15.1 and OD-X6 as written and building (e) in M1, with (d) upstream. R1-F2: INT64_MIN // -1 is a real execution overflow (arrow's checked division), raised as ArithmeticOverflowError with "Overflow happened on: -9223372036854775808 / -1". The probe and the tests read it on all three Python reads and both pyarrow C-stream consumers, at 1 and 4 partitions; REPORT.md, README.md and stream_spike/mod.rs no longer say the class has no execution error. results/probe.* are regenerated.
AI Code Review — Round 2Reviewed HEAD: FindingsR2-F1 [blocked]
|
AI Code Review — Round 3Reviewed HEAD: FindingsNone. SpecThe feature-gated prototype and report address the requested Python and C-stream error paths, consumer behavior, iterator cost, and reader-close feasibility. The reported native-subclass result is qualified by its build, ABI, platform and concurrency limitations. StandardsThe spike follows the repository's feature-gating and execution conventions without changing the default public API. The report separates measured outcomes from limitations and recommendations. AssessmentThe recorded evidence supports the spike's conclusions, with the noted limitations. No issue requiring a fix in this PR was identified. ObservationsR2-F1 no longer applies: the current PR description describes the native subclass, its Cython/C++ and pyarrow ABI costs, and recommends retaining the requirements in alignment with the report. R1-F2's real-overflow probe, results and regression coverage are present. The Python spike tests are skipped by the default CI build; the PR reports a separate feature-enabled run. The native subclass was tested on Linux only, as documented. VerificationInspected the full bound diff, issue, PR description and review history, checks, repository guidance, related prototype code and recorded results. The supplied head and base match the binding. macOS and memory checks passed; both Linux build-and-test checks were pending when read. SummaryNo findings. The previously outstanding PR-description finding has been resolved, and the spike's remaining limitations are documented. Needs another fix round: NO |
Spike for #256: before M1 builds
to_batches()and the PyCapsule stream, can the contract's two stream error paths be built as written (§15.1: Python reads raise the LTSeq class; §16.4: the C stream ends withEIO/EINVALand a message that starts with the class name), what does a Rust iterator underto_batches()cost, and does closing the reader stop the execution (OD-X6, kept by the owner "subject to feasibility verification")?The findings, setup and questions for the owner are in
benchmarks/stream_errors/REPORT.md; raw results are inbenchmarks/stream_errors/results/.Answers
read_next_batch, iteration andread_allraise the LTSeq class and message unchanged, after the batches before the error, for six injected classes and four real engine errors (integer//by zero,INT64_MIN // -1, a failed cast, a Parquet file deleted afterread_parquet)EIOgivesOSError,EINVALgivesArrowInvalid, and the message is exactly"CastError: ...". DuckDB 2.0.0 and Polars 2.1.0 carry the message but raise one class of their own whatever the errno. arrow-rs's exporter cannot do this, so M1 needs its own callbacks; the spike'scapsule.rsis a starting pointto_batches()takes 28-45x as long as DuckDB reading the tableclose()from_batches's reader keeps the class but ignoresclose()and leaving awithblock (dropping the last reference does stop it). A reader imported from the C stream stops onclose()but loses the class. Apyarrow.RecordBatchReadersubclass compiled in Cython, sharingfrom_batches's C++ reader and closing the Rust iterator inclose(), does both: typed errors on every read, andclose()or awithexit stop the executionSo OD-X6 and §15.1's error sentence can both hold with pyarrow 26.0.1. The price is the native subclass's: a second extension module built by Cython as C++20 and linked against pyarrow's
libarrow/libarrow_python(maturin does not build it, so the wheel build needs another step and every platform a C++20 toolchain), and an ABI tie to one pyarrow major version, because it relies onRecordBatchReader's layout fromlib.pxd, which is not a documented API. Readers derived from it, such asreader.cast(...), are plain pyarrow readers whoseclose()does not stop the execution. It was tested on Linux x86-64 only.REPORT.md's recommendation: keep §15.1 and OD-X6 as written, build the native subclass in M1 (option (e)), and ask pyarrow upstream to forward
close()to the iterator (option (d)) so the subclass can go once that pyarrow is the minimum. Narrowing the Resources line to "dropping the reader stops it" (a) or giving up the class on Python reads (b) would weaken a requirement the spike shows can be met. If the owner rejects the C++ build and the pyarrow pin, an LTSeq reader class that is not apyarrow.RecordBatchReader(c) keeps both requirements at the cost of §15.1's return type. The report also asks whether §16.4 should point DuckDB and Polars at the table itself instead oft.to_batches(), the slowest path measured, and what budget M1 has for the iterator.What is in the PR
src/stream_spike/behind a newstream_spikecargo feature (no shipped build carries it):reader.rs:_SpikeBatches, a PyO3 iterator over one execution, with three GIL modes.detachis ADR 0016's rule;attachedis a measurement lower bound, not a candidate;polltakes a ready batch without releasing the GIL.capsule.rs: a C stream with its ownget_nextandget_last_error.classify.rs: the §20.1 class of an execution error, from the bottom of DataFusion's error chain. It handlesLocalFiles'Genericwrapper around a missing file.close()or dropping a reader stopped one.py-ltseq/ltseq/_stream_spike.py: stand-ins for the §20.1 classes with the contract's bases (the real classes do not exist yet), plus the helpersto_batches,Streamandlive.benchmarks/stream_errors/:probe.py(every error source against every read path: pyarrow, DuckDB, Polars, plus close and drop),bench.py([Bench] v0.5 feasibility measurements: price six contract commitments before building them #251's m3 pipeline over the R3 input, idle and with a busy Python thread, one process per cell), results, README and REPORT._closing_reader.pyxis the native subclass, andclosing_reader.pycompiles it against the installed pyarrow undertarget/; the probe leaves its columns out when Cython or a C++20 compiler is missing.py-ltseq/tests/test_stream_spike.py: holds the spike to the report's outcomes, so a pyarrow, DuckDB or Polars upgrade that changes one fails here. It skips on a default build, except a test that the default build refuses the spike; the native-subclass tests also skip without Cython.--features stream_spike. Its Python build uses default features, so the Python spike tests skip there. The default build changes only bycfg'd lines inlib.rsand the feature inCargo.toml.How it was tested
cargo clippy --all-targets -- -D warnings, with and without--features stream_spike: clean (first commit; CI runs both on the head).cargo test --features stream_spike --lib stream_spike::: 6 passed.maturin develop --features stream_spike, CPython 3.13.18, pyarrow 26.0.1),pytest py-ltseq/tests/test_stream_spike.py: 154 passed, 1 skipped (the default-build test), including the real overflow and the native subclass at 1 and 4 partitions; the same after deleting the cached Cython build so it compiled from scratch.pytest py-ltseq/tests/test_stream_spike.py test_arrow_boundary.py test_streaming.py test_m0_prototypes.py: all pass excepttest_duckdb_reads_ltseq_directly. That test predates this change and fails only because DuckDB 2.0.0, installed in the local throwaway venv, removedfetchone(); the project's bench group pins DuckDB 1.5.5 or later.pyright py-ltseq/ltseqand the new test file: 0 errors.bench.pyproduced the timing results: release build, CPython 3.13.5, pyarrow 26.0.1, DuckDB 2.0.0, Polars 2.1.0, one 8-CPU VM.probe.pywas rerun after the native subclass and the overflow were added, on a debug build; it measures no time. The report's caveats cover noise and the stand-in classes.Closes #256