Latest Results
fix(daft-file): release GIL in PyDaftFile methods to prevent deadlock (#7257)
PyDaftFile's #[pymethods] (_from_file_reference, read, seek,
_supports_range_requests) and PyFileReference::__enter__ hold the Python
GIL while calling block_within_async_context, which blocks via rx.recv()
waiting for async S3 I/O on DAFTIO tokio threads. When concurrent scan
tasks need the GIL on the same DAFTIO threads (via Python::attach), all
threads deadlock — the GIL holder waits for a DAFTIO thread to execute
the S3 request, while all DAFTIO threads wait for the GIL.
The fix wraps the blocking I/O calls in py.detach() to release the GIL
before blocking, following the existing pattern used by the exists()
method in the same file. For read() and seek(), cursor ownership is
temporarily moved into the detach closure via take() and restored after
completion.
Deadlock conditions: Ray distributed mode + multiple concurrent scan
tasks per worker + S3 latency >= ~50ms + DAFTIO threads >= 8
(os.cpu_count() >= 8). The official AWS benchmark (4 vCPU, S3 <1ms) does
not trigger this due to low thread count and latency.
## Changes Made
Modified `src/daft-file/src/python.rs` — added `py.detach()` to 5
methods that
hold the GIL while calling `block_within_async_context`:
- `PyFileReference::__enter__`: added `py: Python<'_>`, wrapped
`load_blocking()`
in `py.detach()` with cloned `FileReference`.
- `PyDaftFile::_from_file_reference`: same pattern — clone data,
`py.detach()`,
call `load_blocking()` inside.
- `PyDaftFile::read`: added `py: Python<'_>`, used `cursor.take()` to
move cursor
into `py.detach()` closure (Rust ownership requirement), performed read
without
GIL, restored cursor after completion.
- `PyDaftFile::seek`: same take/restore pattern as `read()`.
- `PyDaftFile::_supports_range_requests`: added `py: Python<'_>`,
wrapped
`block_within_async_context` call in `py.detach()` with cloned `uri` and
`source`.
All changes follow the existing `py.detach()` pattern used by `exists()`
at
line 102 in the same file. No behavioral changes — same inputs, same
outputs,
only the GIL hold timing differs.
## Related Issues
Closes #7256
---------
Co-authored-by: zhouwenjia.zwj <zhouwenjia.zwj@alibaba-inc.com> feat(iceberg): push down starts_with and is_nan/not_nan filters (#7266)
## Changes Made
Extends `IcebergPredicateVisitor` so three more predicates are forwarded
to PyIceberg for manifest- and file-level pruning, instead of silently
falling back to a full scan + post-filter.
- `col.startswith(prefix)` → pyiceberg `StartsWith`
- `col.is_nan()` → pyiceberg `IsNaN`
- `col.not_nan()` → pyiceberg `NotNaN`
Previously these expressions reached `visit_function`, which raises
`ValueError`; `convert_filter` swallows that and drops the predicate
from pushdown, so Iceberg scanned every file and Daft filtered
afterwards. PyIceberg natively supports all three
(`StartsWith`/`IsNaN`/`NotNaN`).
Implementation notes:
- `starts_with` is dispatched through the visitor's `visit_starts_with`
hook (mirrors the existing Paimon predicate visitor).
- `is_nan`/`not_nan` have no dedicated `visit_*` hook on the base
`PredicateVisitor`, so they are handled inside `visit_function` by name.
- Negation composes for free via the existing `visit_not` (e.g.
`~col.startswith(...)` → `Not(StartsWith(...))`).
## Testing
- Added `test_starts_with`, `test_not_starts_with`, `test_is_nan`,
`test_not_nan` in `tests/io/iceberg/test_iceberg_expressions.py`.
- Full `tests/io/iceberg/` suite passes (179 passed, 21 skipped);
pre-commit (ruff, mypy, format) clean.
## Related Issues
None.
Co-authored-by: Claude <noreply@anthropic.com> Latest Branches
0%
yk/fix-progress-bar-fd-leak 0%
yk/lerobot-hand-tracking-bench 0%
chuqCTC:fix/escape-sql-string-literals © 2026 CodSpeed Technology