Conversation
## Why I cover cancellation after a real socket write but before the fiber scheduler reports the byte count. Closing the stream then flushes the same buffered bytes a second time. The regression exercises explicit flush, single-argument write, and multi-argument write through Ruby's existing scheduler fixture. I observe bytes at the socket peer rather than private buffer fields, and use no third-party scheduler dependency. ## Verification Local runs on Ruby 4.0.6 and Ruby 4.1.0dev at 55562be each report 3 tests, 18 assertions, and 3 expected failures, with no errors or skips. Each failure receives the buffered message again after cancellation. This commit intentionally contains only the regression tests, not the fix. Assisted-By: devx/2cdbdf48-1a60-4b23-b35e-42bee58a4326
|
|
||
| private | ||
|
|
||
| def assert_no_replayed_buffer |
There was a problem hiding this comment.
This is backported from Shopify/dalli#81
The goal is to show that if the fiber gets interrupted after being blocked in a write, the internal buffer's accounting doesn't account for the bytes written to the kernel's buffer, closing the Ruby socket will flush the same bytes again.
## Why A scheduler can transfer bytes and then unwind without returning their count. Ruby cannot safely retry that pending output buffer: close may send bytes that already reached the peer a second time. I discard the uncertain buffer and propagate the original interruption instead. I apply the guard to explicit buffer flushes and to the buffered prefix of vector writes, which also covers buffer-overflow writes. Successful results, ordinary errno returns, and the fallback for schedulers without a write hook retain their existing handling. This does not roll back bytes already written or promise delivery of the remaining buffered data. ## Verification On Ruby 4.1.0dev, the rebuilt source passes the three deterministic real-socket regressions with 3 tests and 18 assertions, and the complete `test/fiber/test_io.rb` with 13 tests and 53 assertions. Both runs have no failures, errors, or skips. Runtime identity confirms that the executable and loaded shared library come from the patched build. On Ruby 4.0.6, the same regressions pass, as do the native io_uring reproduction and its no-cancellation control. The IO/fiber selection passes 347 tests with one portability skip, the IO bootstrap selection passes 18 tests, and the IO specs pass 1517 examples.
| fptr->wbuf.off = 0; | ||
| fptr->wbuf.len = 0; |
There was a problem hiding this comment.
How do we know that the bytes have actually been written and the write occurred, without checking the return value (which we can't be cause there was an exception).
Dropping bytes could be as bad as replaying them
|
|
||
| if (!UNDEF_P(result)) { | ||
| return rb_fiber_scheduler_io_result_apply(result); | ||
| if (fptr->wbuf.len && iov[0].iov_base == fptr->wbuf.ptr + fptr->wbuf.off) { |
There was a problem hiding this comment.
Why do we use this branch only in some cases?
| fptr->wbuf.off += result; | ||
| fptr->wbuf.len -= result; |
There was a problem hiding this comment.
It feels like this is the code that needs to run, but the challenge is that we're not getting result? (because of the interrupt)
|
Interrupted writes should properly clear the write buffer. I'm working on sorting this out. |
Situation
A fiber scheduler can write buffered bytes to the peer and then be interrupted before returning their count. Ruby's write-buffer position has not advanced, so closing the IO can send those bytes again.
Execution
I add a test-only reproduction covering explicit flush and buffered single- and multi-argument writes. The existing scheduler fixture performs real socket I/O, then yields before reporting the first write's result. The test reads the initial message from the peer, interrupts the writer, and asserts that closing it does not transmit another copy.
This draft intentionally contains failing tests, without a fix or any Async/io-event dependency.
Local verification
Ruby 4.0.6 and Ruby 4.1.0dev at
55562be3deeach report 3 tests, 18 assertions, and 3 expected failures, with no errors or skips. Each failure observesHello Worldafter the initial copy has already been read, instead of the expected empty remainder.