Current section

Files

Jump to
ace lib ace http2 stream.ex
Raw

lib/ace/http2/stream.ex

defmodule Ace.HTTP2.Stream do
@moduledoc false
@max_stream_window 2_147_483_647
@enforce_keys [
:id,
:status,
:worker,
:monitor,
:initial_window_size,
:incremented,
:sent,
:queue
]
defstruct @enforce_keys
def idle(stream_id, worker, initial_window_size) do
new(stream_id, {:idle, :idle}, worker, initial_window_size)
end
def reserve(stream_id, worker, initial_window_size) do
new(stream_id, {:reserved, :closed}, worker, initial_window_size)
end
def reserved(stream_id, worker, initial_window_size) do
new(stream_id, {:closed, :reserved}, worker, initial_window_size)
end
defp new(stream_id, status, worker, initial_window_size) when is_integer(initial_window_size) do
monitor = Process.monitor(worker)
%__MODULE__{
id: stream_id,
status: status,
worker: worker,
monitor: monitor,
initial_window_size: initial_window_size,
incremented: 0,
sent: 0,
queue: []
}
end
def outbound_window(stream) do
stream.incremented + stream.initial_window_size - stream.sent
end
def send_request(stream, request = %{body: body}) when is_boolean(body) do
case stream.status do
{:idle, :idle} ->
new_status = {:open, :idle}
headers = Ace.HTTP2.request_to_headers(request)
queue = [%{headers: headers, end_stream: !body}]
new_stream = %{stream | status: new_status, queue: stream.queue ++ queue}
{:ok, new_stream}
_ ->
{:error, :request_sent}
end
end
def send_request(stream, request = %{body: ""}) do
send_request(stream, %{request | body: false})
end
def send_request(stream, request = %{body: body}) do
case send_request(stream, %{request | body: true}) do
{:ok, stream_with_headers} ->
{:ok, new_stream} = send_data(stream_with_headers, Raxx.data(body))
send_tail(new_stream, Raxx.tail())
{:error, reason} ->
{:error, reason}
end
end
def send_response(stream, response = %{body: body}) when is_boolean(body) do
case stream.status do
{:idle, :idle} ->
{:error, :dont_send_response_first}
{:closed, :closed} ->
# DEBT happens to reset stream, notify worker data not sent
{:ok, stream}
{:idle, remote} ->
new_status = {:open, remote}
headers = Ace.HTTP2.response_to_headers(response)
queue = [%{headers: headers, end_stream: !body}]
new_stream = %{stream | status: new_status, queue: stream.queue ++ queue}
{:ok, new_stream}
{:reserved, :closed} ->
new_status = {:open, :closed}
headers = Ace.HTTP2.response_to_headers(response)
queue = [%{headers: headers, end_stream: !body}]
new_stream = %{stream | status: new_status, queue: stream.queue ++ queue}
{:ok, new_stream}
end
end
def send_response(stream, response = %{body: ""}) do
send_response(stream, %{response | body: false})
end
def send_response(stream, response = %{body: body}) do
case send_response(stream, %{response | body: true}) do
{:ok, stream_with_headers} ->
{:ok, new_stream} = send_data(stream_with_headers, Raxx.data(body))
send_tail(new_stream, Raxx.tail([]))
{:error, reason} ->
{:error, reason}
end
end
def send_data(stream, data = %Raxx.Data{}) do
case stream.status do
{:open, _remote} ->
queue = [data]
new_stream = %{stream | queue: stream.queue ++ queue}
{:ok, new_stream}
{:closed, :closed} ->
{:ok, stream}
end
end
def send_tail(stream, tail = %Raxx.Tail{}) do
new_stream = process_send_end_stream(stream)
queue = stream.queue ++ [tail]
new_stream = %{new_stream | queue: queue}
{:ok, new_stream}
end
# Sending a reset drops the queue
def send_reset(stream, error) do
new_status = {:closed, :closed}
queue = [{:reset, error}]
new_stream = %{stream | status: new_status, queue: queue}
{:ok, new_stream}
end
defp process_send_end_stream(stream) do
case stream.status do
{:open, remote} ->
new_status = {:closed, remote}
%{stream | status: new_status}
end
end
def receive_headers(stream, request) do
case stream.status do
{:reserved, :closed} ->
forward(stream, request)
new_status = {:reserved, :closed}
new_stream = %{stream | status: new_status}
{:ok, new_stream}
end
end
def receive_headers(stream, headers, end_stream) do
case stream.status do
{:idle, :idle} ->
case Ace.HTTP2.headers_to_request(headers, end_stream) do
{:ok, request} ->
forward(stream, request)
{:ok, {:idle, :open}}
{:error, reason} ->
{:error, reason}
end
{local, :idle} ->
{:ok, response} = Ace.HTTP2.headers_to_response(headers, end_stream)
forward(stream, response)
{:ok, {local, :open}}
{local, :reserved} ->
{:ok, response} = Ace.HTTP2.headers_to_response(headers, end_stream)
forward(stream, response)
{:ok, {local, :open}}
{local, :open} ->
# check end_stream
if end_stream do
trailers = Ace.HTTP2.headers_to_trailers(headers)
trailers = %Raxx.Tail{headers: trailers.headers}
forward(stream, trailers)
# Open but will be closed by handle process_received_end_stream
{:ok, {local, :open}}
else
{:error, {:protocol_error, "trailers must end the stream"}}
end
{_local, :closed} ->
{:error, {:stream_closed, "Headers received on closed stream"}}
end
|> case do
{:ok, new_status} ->
new_stream = %{stream | status: new_status}
{:ok, final_stream} =
if end_stream do
process_received_end_stream(new_stream)
else
{:ok, new_stream}
end
{:ok, final_stream}
{:error, reason} ->
{:error, reason}
end
end
def receive_promise(original, promised, request) do
promised_stream_ref = %Ace.HTTP.Channel{endpoint: self(), id: promised.id, socket: :h2_socket}
forward(original, {:promise, {promised_stream_ref, request}})
{:ok, original}
end
def receive_data(stream, data, end_stream) do
new_status =
case stream.status do
{local, :open} ->
forward(stream, %Raxx.Data{data: data})
if end_stream do
forward(stream, Raxx.tail())
end
{local, :open}
# errors
{:idle, :idle} ->
{
:error,
{:protocol_error, "DATA frame received on a stream in idle state. (RFC7540 5.1)"}
}
{_, :closed} ->
{:error, {:stream_closed, "Data received on closed stream"}}
end
new_stream = %{stream | status: new_status}
if end_stream do
process_received_end_stream(new_stream)
else
{:ok, new_stream}
end
end
def receive_window_update(stream, increment) do
case stream.status do
{:idle, :idle} ->
{
:error,
{
:protocol_error,
"WindowUpdate frame received on a stream in idle state. (RFC7540 5.1)"
}
}
{_local, _remote} ->
increase_window(stream, increment)
end
end
def process_received_end_stream(stream) do
case stream.status do
{local, :open} ->
new_status = {local, :closed}
{:ok, %{stream | status: new_status}}
_ ->
{:error, {:protocol_error, "Unexpected stream message"}}
end
end
def receive_reset(stream, reason) do
case stream.status do
{:idle, :idle} ->
{
:error,
{:protocol_error, "RstStream frame received on a stream in idle state. (RFC7540 5.1)"}
}
{:closed, :closed} ->
{:ok, stream}
{_, _} ->
forward(stream, {:reset, reason})
{:ok, %{stream | status: {:closed, :closed}}}
end
end
defp increase_window(stream, increment) do
new_stream = %{stream | incremented: stream.incremented + increment}
if outbound_window(new_stream) <= @max_stream_window do
{:ok, new_stream}
else
{:error, {:flow_control_error, "Stream window was increased beyond maximum"}}
end
end
defp forward(stream, message) do
stream_ref = %Ace.HTTP.Channel{endpoint: self(), id: stream.id, socket: :h2_socket}
# # Maybe send with same ref as used for reply
send(stream.worker, {stream_ref, message})
end
end