diff --git a/lib/ione/byte_buffer.rb b/lib/ione/byte_buffer.rb index 503fc02..6744ca9 100644 --- a/lib/ione/byte_buffer.rb +++ b/lib/ione/byte_buffer.rb @@ -253,22 +253,35 @@ def update(location, bytes) # in situations where a loop wants to offer some bytes but can't be sure # how many will be accepted — for example when writing to a socket. # + # By providing the readonly argument, the peek is even cheaper in that it + # only considers the read buffer. In this mode, the method returns nil + # to signify that the read buffer is empty. With just a single reader, + # like the IO reactor, where reads only happen in one thread, this means + # that `cheap_peek(true)` can be called without holding a lock to protect + # against concurrent writes + # # @example feeding bytes to a socket # while true # _, writables, _ = IO.select(nil, sockets) # if writables # writables.each do |io| - # n = io.write_nonblock(buffer.cheap_peek) + # bytes = buffer.cheap_peek(true) + # unless bytes + # bytes = buffer_lock.synchronize { buffer.cheap_peak } + # end + # n = io.write_nonblock(bytes) # buffer.discard(n) # end # end # - # @return [String] some bytes from the start of the buffer - def cheap_peek - if @offset >= @read_buffer.bytesize - swap_buffers + # @param [Boolean] readonly to specify to only look at the read buffer + # @return [String, nil] some bytes from the start of the buffer, or nil when read buffer empty in readonly mode + def cheap_peek(readonly = false) + has_read_buffer = @offset < @read_buffer.bytesize + if has_read_buffer || !readonly + swap_buffers unless has_read_buffer + @read_buffer[@offset, @read_buffer.bytesize - @offset] end - @read_buffer[@offset, @read_buffer.bytesize - @offset] end def eql?(other) diff --git a/lib/ione/io/base_connection.rb b/lib/ione/io/base_connection.rb index da65f2e..c69c400 100644 --- a/lib/ione/io/base_connection.rb +++ b/lib/ione/io/base_connection.rb @@ -163,18 +163,29 @@ def write(bytes=nil) def flush should_close = false if @state == CONNECTED_STATE || @state == DRAINING_STATE - @lock.lock - begin - if @writable - bytes_written = @io.write_nonblock(@write_buffer.cheap_peek) - @write_buffer.discard(bytes_written) + bytes = @write_buffer.cheap_peek(true) + unless bytes + @lock.lock + begin + if @writable + bytes = @write_buffer.cheap_peek + end + ensure + @lock.unlock end - @writable = !@write_buffer.empty? - if @state == DRAINING_STATE && !@writable - should_close = true + end + if bytes + bytes_written = @io.write_nonblock(bytes) + @lock.lock + begin + @write_buffer.discard(bytes_written) + @writable = !@write_buffer.empty? + if @state == DRAINING_STATE && !@writable + should_close = true + end + ensure + @lock.unlock end - ensure - @lock.unlock end close if should_close end diff --git a/spec/ione/byte_buffer_spec.rb b/spec/ione/byte_buffer_spec.rb index ad16afd..835a726 100644 --- a/spec/ione/byte_buffer_spec.rb +++ b/spec/ione/byte_buffer_spec.rb @@ -330,6 +330,28 @@ module Ione x.bytesize.should be <= buffer.bytesize buffer.to_str.should start_with(x) end + + it 'considers contents in the write when read buffer consumed' do + buffer.append('foo') + buffer.append('bar') + buffer.read_byte + buffer.discard(5) + buffer.append('hello') + x = buffer.cheap_peek + x.bytesize.should be > 0 + x.bytesize.should be <= buffer.bytesize + buffer.to_str.should start_with(x) + end + + it 'returns nil in readonly mode when read buffer is consumed' do + buffer.append('foo') + buffer.append('bar') + buffer.read_byte + buffer.discard(5) + buffer.append('hello') + x = buffer.cheap_peek(true) + x.should be_nil + end end describe '#getbyte' do