Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 63 additions & 8 deletions io.c
Original file line number Diff line number Diff line change
Expand Up @@ -1360,6 +1360,55 @@ rb_io_write_memory(rb_io_t *fptr, const void *buf, size_t count)
return (ssize_t)rb_io_blocking_region_wait(fptr, internal_write_func, &iis, RUBY_IO_WRITABLE);
}

struct io_write_buffer_arguments {
VALUE scheduler;
rb_io_t *fptr;
const void *buffer;
size_t size;
ssize_t result;
int error;
};

static VALUE
io_write_buffer_fiber_scheduler_body(VALUE argument)
{
struct io_write_buffer_arguments *args = (struct io_write_buffer_arguments *)argument;
VALUE result = rb_fiber_scheduler_io_write_memory(args->scheduler, args->fptr->self,
args->buffer, args->size);

if (!UNDEF_P(result)) {
args->result = rb_fiber_scheduler_io_result_apply(result);
args->error = errno;
}
return result;
}

static bool
io_write_buffer_fiber_scheduler(VALUE scheduler, rb_io_t *fptr, const void *buffer,
size_t size, ssize_t *result)
{
struct io_write_buffer_arguments args = {
.scheduler = scheduler,
.fptr = fptr,
.buffer = buffer,
.size = size,
};
int state = 0;
VALUE ret = rb_protect(io_write_buffer_fiber_scheduler_body, (VALUE)&args, &state);

if (state) {
/* Retrying after unwinding without a byte count may replay written bytes. */
fptr->wbuf.off = 0;
fptr->wbuf.len = 0;
Comment on lines +1401 to +1402

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

rb_jump_tag(state);
}
if (UNDEF_P(ret)) return false;

*result = args.result;
errno = args.error;
return true;
}

#ifdef HAVE_WRITEV
static ssize_t
rb_writev_internal(rb_io_t *fptr, const struct iovec *iov, int iovcnt)
Expand All @@ -1371,10 +1420,17 @@ rb_writev_internal(rb_io_t *fptr, const struct iovec *iov, int iovcnt)
VALUE scheduler = rb_fiber_scheduler_current_for_threadptr(th);
if (scheduler != Qnil) {
// This path assumes at least one `iov`:
VALUE result = rb_fiber_scheduler_io_write_memory(scheduler, fptr->self, iov[0].iov_base, iov[0].iov_len);

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) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we use this branch only in some cases?

ssize_t result;
if (io_write_buffer_fiber_scheduler(scheduler, fptr, iov[0].iov_base, iov[0].iov_len, &result)) {
return result;
}
}
else {
VALUE result = rb_fiber_scheduler_io_write_memory(scheduler, fptr->self, iov[0].iov_base, iov[0].iov_len);
if (!UNDEF_P(result)) {
return rb_fiber_scheduler_io_result_apply(result);
}
}
}

Expand Down Expand Up @@ -1425,16 +1481,15 @@ io_flush_buffer_sync(void *arg)
static inline VALUE
io_flush_buffer_fiber_scheduler(VALUE scheduler, rb_io_t *fptr)
{
VALUE ret = rb_fiber_scheduler_io_write_memory(scheduler, fptr->self, fptr->wbuf.ptr+fptr->wbuf.off, fptr->wbuf.len);
if (!UNDEF_P(ret)) {
ssize_t result = rb_fiber_scheduler_io_result_apply(ret);
ssize_t result;
if (io_write_buffer_fiber_scheduler(scheduler, fptr, fptr->wbuf.ptr + fptr->wbuf.off, fptr->wbuf.len, &result)) {
if (result > 0) {
fptr->wbuf.off += result;
fptr->wbuf.len -= result;
Comment on lines 1487 to 1488

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

}
return result >= 0 ? (VALUE)0 : (VALUE)-1;
}
return ret;
return RUBY_Qundef;
}

static VALUE
Expand Down
62 changes: 62 additions & 0 deletions test/fiber/test_io.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@
class TestFiberIO < Test::Unit::TestCase
MESSAGE = "Hello World"

class YieldAfterWriteScheduler < IOBufferScheduler
def initialize
super
@yielded = false
end

def io_write(...)
result = super
if result > 0 && !@yielded
@yielded = true
transfer
end
result
end
end

def test_read
omit unless defined?(UNIXSocket)

Expand Down Expand Up @@ -275,4 +291,50 @@ def test_close_while_reading_on_thread
reading_thread&.join rescue nil
end
end

def test_interrupted_flush_does_not_replay_buffered_bytes
assert_no_replayed_buffer { |io| io.flush }
end

def test_interrupted_write_does_not_replay_buffered_bytes
assert_no_replayed_buffer { |io| io.write("x" * 16_384) }
end

def test_interrupted_writev_does_not_replay_buffered_bytes
assert_no_replayed_buffer { |io| io.write("x" * 16_384, "y") }
end

private

def assert_no_replayed_buffer

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

omit("UNIXSocket is not defined!") unless defined?(UNIXSocket)

UNIXSocket.pair do |reader, writer|
writer.sync = false
thread = Thread.new do
Fiber.set_scheduler(YieldAfterWriteScheduler.new)
fiber = Fiber.schedule do
begin
writer.write(MESSAGE)
yield(writer)
ensure
writer.close
end
end
assert_equal(MESSAGE, reader.read_nonblock(MESSAGE.bytesize))
assert_raise(Interrupt) { fiber.raise(Interrupt) }
ensure
Fiber.set_scheduler(nil)
end

thread.value
assert_predicate(writer, :closed?)
assert_equal("", reader.read)
ensure
if thread&.alive?
thread.kill
thread.join
end
end
end
end
Loading