FFI: Thread-safe writer - #8773
Conversation
|
@claude review in depth, in particular wrt to correctness and write performance |
|
Claude finished @0ax1's task in 4m 5s —— View job Claude PR Review —
|
|
This seems like a weird API? The user pushes chunks from multiple threads, so they're not deterministically ordered? I think you might just want to expose functions for driving the CurrentThreadRuntime. Like "runtime.drive(2 seconds)" and then you just drive the runtime until your 2 seconds timer is up. |
|
I don't think we should expose timers and driving the runtime in general. Presumably user should care about ordering on their side if they want parallel processing. If we want to be on par with Parquet, fastparquet/dask allows partition-based parallelism, and this PR does the same. |
af977fb to
3efd921
Compare
Pull request was converted to draft
3efd921 to
e6df2b4
Compare
e6df2b4 to
5d54925
Compare
Polar Signals Profiling ResultsLatest Run
Previous Runs (1)
Powered by Polar Signals Cloud |
Benchmarks: Vortex queries 📖Verdict: Likely regression (medium confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed (1.148x ❌, 0↑ 2↓)
datafusion / parquet (0.941x ➖, 0↑ 0↓)
duckdb / vortex-file-compressed (1.017x ➖, 0↑ 0↓)
duckdb / parquet (1.000x ➖, 0↑ 0↓)
No file size changes detected. |
|
I think this may need another iteration since we also want ordering in some sense but with parallel decoding |
df94d0a to
3571bd2
Compare
3571bd2 to
eb2b623
Compare
| vx_error *error = nullptr; | ||
| for (const Array &array : arrays) { | ||
| vx_array_sink_push(handle_.get(), Access::c_ptr(array), &error); | ||
| vx_writer_push(handle_.get(), Access::c_ptr(array), &error); |
There was a problem hiding this comment.
Is there a race here with finish below? A concurrent finish could reset the handle after we call get but before vx_writer_push has finished using it, which would be UB, right?
| let inner = writer.inner.read(); | ||
| let mut sender = inner | ||
| .sender | ||
| .clone() |
There was a problem hiding this comment.
afaik this clone would create a slot for this new sender, increasing the channel capacity by 1. We do this clone on every push so even if I set concurrent_array_limit to 1 then call push 5 times I would have 6 in-flight arrays on the channel. So when cloning the sender we kind of defeat the intended backpressure
|
|
||
| match send_result { | ||
| Ok(_) => Ok(()), | ||
| Err(_) => Err(writer.take_error()), |
There was a problem hiding this comment.
so this means one push thread would consume the error, so all other threads that call push would get channel closed instead. I think that is fine, but also the thread that calls close would also not get the actual error, and get channel closed. I wonder if we should use Arc<Err> here to persist and report the same error for all push threads and whoever is calling close
| }; | ||
| let task = task.ok_or_else(|| vortex_err!("writer is closed"))?; | ||
|
|
||
| let summary = RUNTIME.block_on(task)?; |
There was a problem hiding this comment.
am I right in understanding that this method and the push method is the only place we do work on the runtime? If so I think we should change this. If we have a channel on push, the sender.send call would complete near immediately because it is not blocking as long as we have capacity on the channel. For any actual work to happen on the runtime, we need to call close, and that is only one thread.
The only time we can progress on multiple threads is if the sender.send blocks on push, which is super confusing.
Replace vx_array_sink with vx_writer interface: