Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure wire always contains a full H/2 frame #15

Merged
merged 7 commits into from
Jun 10, 2024

Conversation

maruth-stripe
Copy link

@maruth-stripe maruth-stripe commented Nov 6, 2023

Follow-up to socketry/async-io#72 to fix #14
Updates read_header to use Stream#peek instead of `Stream#read. We maintain the invariant that the read buffer / wire is always frame-aligned. Instead of reading data off the wire when reading the header for an H/2 frame, we just peek the header and then read the full frame.

Types of Changes

  • Bug fix.

Contribution

@maruth-stripe maruth-stripe marked this pull request as ready for review November 9, 2023 15:00
@maruth-stripe
Copy link
Author

maruth-stripe commented Nov 9, 2023

Verified the fix works with the following script (modification to the repro in #14)

# requires
require "stringio"
require "async/reactor"


class FunkyIO
  def initialize
    @f = Protocol::HTTP2::DataFrame.new(401, 0, Protocol::HTTP2::DataFrame::TYPE, 13, "a" * 13)
    @sio = StringIO.new
    @f.write_header(@sio)
    @sio.rewind

    @state = :read_header
  end

  def peek(size, buf = nil)
    if @state == :read_header || @state == :try_header_again
      res = @sio.read(size, buf)

      case @state
      when :read_header 
        @state = :now_timeout
      when :try_header_again
        @state = :read_frame
      end

      @sio = StringIO.new
      @f.write_header(@sio)
      @f.write_payload(@sio)
      @sio.rewind
      
      return res
    end
  end

  def read(size, buf = nil)
    case @state
    when :now_timeout
      @state = :try_header_again
      raise Async::TimeoutError
    when :read_frame
      res = @sio.read(size, buf)
      res
    end
  end
end

f = Protocol::HTTP2::Framer.new(FunkyIO.new)
Async::Reactor.run do
  begin
    p(f.read_frame)
  rescue Async::TimeoutError
    # try again
  end

  p(f.read_frame)
end

outputs
#<Protocol::HTTP2::DataFrame stream_id=401 flags=0 13b>

@ioquatix
Copy link
Member

I released async-io v1.37.0 which I assume we can use here?

@ioquatix ioquatix self-assigned this Nov 10, 2023
@ioquatix ioquatix added the bug Something isn't working label Nov 10, 2023
@maruth-stripe
Copy link
Author

Hi @ioquatix really sorry for disappearing on this PR for a bit, got caught up with other stuff!

I've updated it a bit, and tested it with the follwing script:

# requires
require "stringio"
require "async"
require "async/reactor"
require "protocol/http2/framer"
require "protocol/http2/data_frame"



class FunkyIO
  def initialize
    @f = Protocol::HTTP2::DataFrame.new(401, 0, Protocol::HTTP2::DataFrame::TYPE, 13, "a" * 13)
    @sio = StringIO.new
    @f.write_header(@sio)
    @sio.rewind

    @state = :read_header
  end

	def sync=(x)
	end

  def read_nonblock(size, buf = nil, exception)
		puts "read_nonblock(#{size}) state: #{@state}"
    if @state == :read_header || @state == :try_header_again
      res = @sio.read(size, buf)

      case @state
      when :read_header 
        @state = :now_timeout
      when :try_header_again
        @state = :read_frame
      end

      @sio = StringIO.new
      @f.write_payload(@sio)
      @sio.rewind
      
      return res
		elsif @state == :now_timeout
      @state = :try_header_again
			raise Async::TimeoutError
		elsif @state == :read_frame
			return @sio.read(size, buf)
    end
  end
end

f = Protocol::HTTP2::Framer.new(FunkyIO.new)
Async::Reactor.run do
  begin
    p(f.read_frame)
  rescue Async::TimeoutError
    # try again
  end

  p(f.read_frame)
end

which outputs

read_nonblock(65536) state: read_header
read_nonblock(65536) state: now_timeout
read_nonblock(65536) state: try_header_again
#<Protocol::HTTP2::DataFrame stream_id=401 flags=0 13b>

I'm not sure why but bundle exec bake test seems to be taking quite a while. Unsure if it's something messed up with my local setup.

@maruth-stripe
Copy link
Author

One note: this PR now implicitly adds a requirement that the stream passed into Framer must implement sync= and read_nonblock (or be an `Async::IO::Stream)

I believe StringIO, Socket do implement this.

@maruth-stripe
Copy link
Author

@ioquatix do you know why the test suite might be timing out here?

@ioquatix
Copy link
Member

The next step for this code is to work with standard IO instances. However, I don't think a method like peek exists for IO. Do you have any ideas?

@ioquatix
Copy link
Member

After considering this further, I believe we should adopt the peek(n) model in this code. We will have to use an appropriate wrapper.

@ioquatix ioquatix changed the base branch from main to frame-header-peek June 10, 2024 07:55
@ioquatix ioquatix merged commit cb304c8 into socketry:frame-header-peek Jun 10, 2024
7 of 18 checks passed
@ioquatix
Copy link
Member

I'm going to merge this into another branch so I can finish it off.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
2 participants