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

Support upgrades via I/O operations #159

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open

Support upgrades via I/O operations #159

wants to merge 8 commits into from

Conversation

seliopou
Copy link
Member

Add Reqd.respond_with_upgrade, which will create response with a with status code 101, and the provided headers. On the next write operation, this response, along with the inspiring request will be provided to the runtime. It is up to the runtime to serialize the resposne and send it on the wire before handing off the socket to an upgrade handler.

The runtime code almost certainly has bugs in it, so this isn't going to be merged immediately. Specifically, I'm pretty sure that both in async and lwt the sockets will be closed right after the upgrade handlers are called. Also, neither currently serialize the response on the wire.

Subsumes #134.

@seliopou seliopou requested a review from dpatti October 15, 2019 02:27
@@ -34,6 +34,7 @@

open Lwt.Infix

(* Based on the Buffer module in httpaf_async.ml. *)
Copy link
Member Author

Choose a reason for hiding this comment

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

Delete this.

@andreas
Copy link

andreas commented Apr 10, 2020

There's been renewed interest in using httpaf with ocaml-graphql-server, so I've spent some time revisiting the idea. The primary blocker is websocket support, which this PR is a crucial part of. As such, I've played around with integrating this PR with websocketaf for the sake of understanding it better. Here are my thoughts based on that attempt, though I might be trying to apply respond_with_upgrade wrongly given the details have not been fleshed out yet.

On the next write operation, this response, along with the inspiring request will be provided to the runtime. It is up to the runtime to serialize the resposne and send it on the wire before handing off the socket to an upgrade handler.

It seems to me that the runtime is not particularly well suited for writing the response to the wire. The code for serialising a response is currently kept in Writer, which is not exposed by Httpaf. I assume the runtime would thus need to duplicate the function write_response — or maybe you have something else in mind 🤔

I'm wondering whether a better invariant would be for `Upgrade to signal to the runtime that the upgrade response has already been written to the wire, and that the socket can be handed off to an upgrade handler. I've experimentally implemented that here on top of this branch.

Based on those semantics of `Upgrade, I've revisited the existing PR for websocket server connections and updated it to use respond_with_upgrade. For the sake of completeness, the PR also includes a Lwt runtime and example programs (echo_server.exe and wscat.exe are now compatible because of respond_with_upgrade! 🥳).

It could easily be the case I'm missing something, but on the other hand maybe this feedback is useful. Regardless, I'd be happy to see upgrade support in httpaf. Let me know if I can help 🙏

@dpatti
Copy link
Collaborator

dpatti commented Apr 11, 2020

Hey @andreas, this has been on our minds as well recently. My plan was to spend some time working on this this weekend, and I'll definitely take a look at your PR. The example programs are especially appreciated!

dpatti added a commit that referenced this pull request Apr 12, 2020
This is a prelude to #159 which introduces streaming requests, and much
of the diff is identical, with a few trivial changes in `Reqd` and a few
major changes in `Server_connection`.

The goals here are:

1. Make `Reqd` return better types that encapsulate its state instead of
   requiring the user to probe it with `requires_<input|output>` and
   `is_complete`.

2. Try to make queue management easier to reason about by folding bits
   of logic from `advance_request_queue_if_necessary` into
   `next_read_operation` and `next_write_operation` such that we only
   perform side-effects when the operation in question demands it.

One of the ways I tried to make this easier to reason about was to make
the `yield_<reader|writer>` and `next_<read|write>_operation` functions
very parallel. For one, the extra logic in `yield_writer` was puzzling.
Ideally, if you're calling `yield_writer`, you're doing so because you
just called `next_action` and were told to `Yield`, so all of the
conditions being checked should not be possible.

Looking at the next-operation functions, they both start out with a
short-circuit for shutting down when the server can no longer make
progress (reader is closed and queue is empty). This doesn't feel like
it belongs here. Perhaps this check should be part of
`advance_request_queue` with some extra logic triggering in
`shutdown_reader`? After that, the next-operation functions use some
very simple probing of the input/output state of `Reqd` to determine
what to do next. Only in the case of `Complete` do we move into a
separate function (to make it easier to read):
`_final_<read|write>_operation`.

In these functions, we decide if we should shutdown the respective
reader/writer or consider the `reqd` complete and move it off the queue.
When we do shift it off, we recursively ask for the next operation given
the new queue state. In all cases, before we return the result, we
wakeup the other side so that it too can evaluate the next operation
given the new queue state.

Though on the surface, these pieces feel fairly straightforward, there
are still a slew of re-entrancy bugs to consider. I think there are two
things that we can do to make this drastically easier to manage:

1. We call `t.request_handler` in two places, and this is mostly because
   we want to keep the invariant that the head of the request queue has
   already been passed off to the handler. I feel like splitting this up
   into a simple queue of unhandled requests and a [Reqd.t option] that
   represents the current request would be easier to manage.

2. It would be nice to schedule calls. Things like waking up the writer
   before you let the read loop know its next operation just immediately
   makes my mind fall apart and lose track of state. There's a fairly
   obvious solution of asking for a `schedule : (unit -> unit) -> unit`
   function from the runtime that promises to not call the thunk
   synchronously, but rather waits until it is outside of the read and
   write loops. But maybe we can solve it using what we have now, like
   establishing a contract that when the reader/writer is woken up, they
   must schedule their work for a fresh call stack and not immediately
   ask for operations.

I added a `Queue.clear` to shutdown, not because it was necessary in any
sense, but because it was part of `advance_request_queue_if_necessary`,
which could have come into play in certain situations where `shutdown`
was called from the runtime (e.g., in case of some exception).

I would like to note that despite the fact that all tests pass, I have
very little confidence in this being correct right now and would like to
do some further testing within the actual runtimes.
dpatti added a commit that referenced this pull request Apr 12, 2020
This is a prelude to #159 which introduces upgrade requests, and much
of the diff is identical, with a few trivial changes in `Reqd` and a few
major changes in `Server_connection`.

The goals here are:

1. Make `Reqd` return better types that encapsulate its state instead of
   requiring the user to probe it with `requires_<input|output>` and
   `is_complete`.

2. Try to make queue management easier to reason about by folding bits
   of logic from `advance_request_queue_if_necessary` into
   `next_read_operation` and `next_write_operation` such that we only
   perform side-effects when the operation in question demands it.

One of the ways I tried to make this easier to reason about was to make
the `yield_<reader|writer>` and `next_<read|write>_operation` functions
very parallel. For one, the extra logic in `yield_writer` was puzzling.
Ideally, if you're calling `yield_writer`, you're doing so because you
just called `next_action` and were told to `Yield`, so all of the
conditions being checked should not be possible.

Looking at the next-operation functions, they both start out with a
short-circuit for shutting down when the server can no longer make
progress (reader is closed and queue is empty). This doesn't feel like
it belongs here. Perhaps this check should be part of
`advance_request_queue` with some extra logic triggering in
`shutdown_reader`? After that, the next-operation functions use some
very simple probing of the input/output state of `Reqd` to determine
what to do next. Only in the case of `Complete` do we move into a
separate function (to make it easier to read):
`_final_<read|write>_operation`.

In these functions, we decide if we should shutdown the respective
reader/writer or consider the `reqd` complete and move it off the queue.
When we do shift it off, we recursively ask for the next operation given
the new queue state. In all cases, before we return the result, we
wakeup the other side so that it too can evaluate the next operation
given the new queue state.

Though on the surface, these pieces feel fairly straightforward, there
are still a slew of re-entrancy bugs to consider. I think there are two
things that we can do to make this drastically easier to manage:

1. We call `t.request_handler` in two places, and this is mostly because
   we want to keep the invariant that the head of the request queue has
   already been passed off to the handler. I feel like splitting this up
   into a simple queue of unhandled requests and a [Reqd.t option] that
   represents the current request would be easier to manage.

2. It would be nice to schedule calls. Things like waking up the writer
   before you let the read loop know its next operation just immediately
   makes my mind fall apart and lose track of state. There's a fairly
   obvious solution of asking for a `schedule : (unit -> unit) -> unit`
   function from the runtime that promises to not call the thunk
   synchronously, but rather waits until it is outside of the read and
   write loops. But maybe we can solve it using what we have now, like
   establishing a contract that when the reader/writer is woken up, they
   must schedule their work for a fresh call stack and not immediately
   ask for operations.

I added a `Queue.clear` to shutdown, not because it was necessary in any
sense, but because it was part of `advance_request_queue_if_necessary`,
which could have come into play in certain situations where `shutdown`
was called from the runtime (e.g., in case of some exception).

I would like to note that despite the fact that all tests pass, I have
very little confidence in this being correct right now and would like to
do some further testing within the actual runtimes.
anmonteiro referenced this pull request in anmonteiro/httpun Apr 18, 2020
This is a prelude to #159 which introduces upgrade requests, and much
of the diff is identical, with a few trivial changes in `Reqd` and a few
major changes in `Server_connection`.

The goals here are:

1. Make `Reqd` return better types that encapsulate its state instead of
   requiring the user to probe it with `requires_<input|output>` and
   `is_complete`.

2. Try to make queue management easier to reason about by folding bits
   of logic from `advance_request_queue_if_necessary` into
   `next_read_operation` and `next_write_operation` such that we only
   perform side-effects when the operation in question demands it.

One of the ways I tried to make this easier to reason about was to make
the `yield_<reader|writer>` and `next_<read|write>_operation` functions
very parallel. For one, the extra logic in `yield_writer` was puzzling.
Ideally, if you're calling `yield_writer`, you're doing so because you
just called `next_action` and were told to `Yield`, so all of the
conditions being checked should not be possible.

Looking at the next-operation functions, they both start out with a
short-circuit for shutting down when the server can no longer make
progress (reader is closed and queue is empty). This doesn't feel like
it belongs here. Perhaps this check should be part of
`advance_request_queue` with some extra logic triggering in
`shutdown_reader`? After that, the next-operation functions use some
very simple probing of the input/output state of `Reqd` to determine
what to do next. Only in the case of `Complete` do we move into a
separate function (to make it easier to read):
`_final_<read|write>_operation`.

In these functions, we decide if we should shutdown the respective
reader/writer or consider the `reqd` complete and move it off the queue.
When we do shift it off, we recursively ask for the next operation given
the new queue state. In all cases, before we return the result, we
wakeup the other side so that it too can evaluate the next operation
given the new queue state.

Though on the surface, these pieces feel fairly straightforward, there
are still a slew of re-entrancy bugs to consider. I think there are two
things that we can do to make this drastically easier to manage:

1. We call `t.request_handler` in two places, and this is mostly because
   we want to keep the invariant that the head of the request queue has
   already been passed off to the handler. I feel like splitting this up
   into a simple queue of unhandled requests and a [Reqd.t option] that
   represents the current request would be easier to manage.

2. It would be nice to schedule calls. Things like waking up the writer
   before you let the read loop know its next operation just immediately
   makes my mind fall apart and lose track of state. There's a fairly
   obvious solution of asking for a `schedule : (unit -> unit) -> unit`
   function from the runtime that promises to not call the thunk
   synchronously, but rather waits until it is outside of the read and
   write loops. But maybe we can solve it using what we have now, like
   establishing a contract that when the reader/writer is woken up, they
   must schedule their work for a fresh call stack and not immediately
   ask for operations.

I added a `Queue.clear` to shutdown, not because it was necessary in any
sense, but because it was part of `advance_request_queue_if_necessary`,
which could have come into play in certain situations where `shutdown`
was called from the runtime (e.g., in case of some exception).

I would like to note that despite the fact that all tests pass, I have
very little confidence in this being correct right now and would like to
do some further testing within the actual runtimes.
Add `Reqd.respond_with_upgrade`, which will create response with a with
status code 101, and the provided headers. On the next write operation,
this response, along with the inspiring request will be provided to the
runtime. It is up to the runtime to serialize the resposne and send it
on the wire before handing off the socket to an upgrade handler.
The future returned by the ugprade handler will dicatate when the fd
will be closed.
The deferred returned by the ugprade handler will dicatate when the fd
will be closed.
@seliopou
Copy link
Member Author

@andreas I force-pushed to this branch so you may want to update your fork. I'm going to try and add some more tests so I can merge #172 and then get this in.

Do you think this approach is workable in general? I'm hoping that something like this can also be adapted for use with sendfile, and other such things.

dpatti added a commit that referenced this pull request May 6, 2020
This is a prelude to #159 which introduces upgrade requests, and much
of the diff is identical, with a few trivial changes in `Reqd` and a few
major changes in `Server_connection`.

The goals here are:

1. Make `Reqd` return better types that encapsulate its state instead of
   requiring the user to probe it with `requires_<input|output>` and
   `is_complete`.

2. Try to make queue management easier to reason about by folding bits
   of logic from `advance_request_queue_if_necessary` into
   `next_read_operation` and `next_write_operation` such that we only
   perform side-effects when the operation in question demands it.

One of the ways I tried to make this easier to reason about was to make
the `yield_<reader|writer>` and `next_<read|write>_operation` functions
very parallel. For one, the extra logic in `yield_writer` was puzzling.
Ideally, if you're calling `yield_writer`, you're doing so because you
just called `next_action` and were told to `Yield`, so all of the
conditions being checked should not be possible.

Looking at the next-operation functions, they both start out with a
short-circuit for shutting down when the server can no longer make
progress (reader is closed and queue is empty). This doesn't feel like
it belongs here. Perhaps this check should be part of
`advance_request_queue` with some extra logic triggering in
`shutdown_reader`? After that, the next-operation functions use some
very simple probing of the input/output state of `Reqd` to determine
what to do next. Only in the case of `Complete` do we move into a
separate function (to make it easier to read):
`_final_<read|write>_operation`.

In these functions, we decide if we should shutdown the respective
reader/writer or consider the `reqd` complete and move it off the queue.
When we do shift it off, we recursively ask for the next operation given
the new queue state. In all cases, before we return the result, we
wakeup the other side so that it too can evaluate the next operation
given the new queue state.

Though on the surface, these pieces feel fairly straightforward, there
are still a slew of re-entrancy bugs to consider. I think there are two
things that we can do to make this drastically easier to manage:

1. We call `t.request_handler` in two places, and this is mostly because
   we want to keep the invariant that the head of the request queue has
   already been passed off to the handler. I feel like splitting this up
   into a simple queue of unhandled requests and a [Reqd.t option] that
   represents the current request would be easier to manage.

2. It would be nice to schedule calls. Things like waking up the writer
   before you let the read loop know its next operation just immediately
   makes my mind fall apart and lose track of state. There's a fairly
   obvious solution of asking for a `schedule : (unit -> unit) -> unit`
   function from the runtime that promises to not call the thunk
   synchronously, but rather waits until it is outside of the read and
   write loops. But maybe we can solve it using what we have now, like
   establishing a contract that when the reader/writer is woken up, they
   must schedule their work for a fresh call stack and not immediately
   ask for operations.

I added a `Queue.clear` to shutdown, not because it was necessary in any
sense, but because it was part of `advance_request_queue_if_necessary`,
which could have come into play in certain situations where `shutdown`
was called from the runtime (e.g., in case of some exception).

I would like to note that despite the fact that all tests pass, I have
very little confidence in this being correct right now and would like to
do some further testing within the actual runtimes.
@andreas
Copy link

andreas commented May 10, 2020

@andreas I force-pushed to this branch so you may want to update your fork. I'm going to try and add some more tests so I can merge #172 and then get this in.

Thanks for the heads up. I've updated my branch to be based on the latest version of this branch (upgrade...andreas:upgrade2).

Do you think this approach is workable in general? I'm hoping that something like this can also be adapted for use with sendfile, and other such things.

I like the approach of signalling an upgrade to the runtime. To recap, these are the two different approaches, as I understand them:

A. The semantics suggested in this PR, where `Upgrade means the runtime is responsible for writing the 101 Switching Protocols to the wire.
B. The semantics suggested in my branch, where `Upgrade means 101 Switching Protocols has already been written to the wire.

Do I understand correctly, that you prefer (A) because it would more easily support something like sendfile?
I personally found (B) to be easiest to work with, as I was adapting websocketaf to make use of respond_with_upgrade.

@seliopou
Copy link
Member Author

Yeah, my question was regarding how the upgrade is signaled to the runtime. I don't quite know which of A or B to do just yet. We're changing a bunch of stuff related to the server connection module and underlying machinery. Once that work's complete I'll rebase this and make a final determination.

But I'd say as long as the state machine changes for flushing the write and then surfacing the upgrade is simple and straightforward, I wouldn't be opposed to that. Probably when I first wrote this I either thought it was more trouble than it's worth or actually bumped into some issue that made it problematic. We'll see once more once the dust settles.

@andreas
Copy link

andreas commented May 11, 2020

Makes sense — looking forward to see how it works out 🙂

dpatti added a commit that referenced this pull request May 16, 2020
This is a prelude to #159 which introduces upgrade requests, and much
of the diff is identical, with a few trivial changes in `Reqd` and a few
major changes in `Server_connection`.

The goals here are:

1. Make `Reqd` return better types that encapsulate its state instead of
   requiring the user to probe it with `requires_<input|output>` and
   `is_complete`.

2. Try to make queue management easier to reason about by folding bits
   of logic from `advance_request_queue_if_necessary` into
   `next_read_operation` and `next_write_operation` such that we only
   perform side-effects when the operation in question demands it.

One of the ways I tried to make this easier to reason about was to make
the `yield_<reader|writer>` and `next_<read|write>_operation` functions
very parallel. For one, the extra logic in `yield_writer` was puzzling.
Ideally, if you're calling `yield_writer`, you're doing so because you
just called `next_action` and were told to `Yield`, so all of the
conditions being checked should not be possible.

Looking at the next-operation functions, they both start out with a
short-circuit for shutting down when the server can no longer make
progress (reader is closed and queue is empty). This doesn't feel like
it belongs here. Perhaps this check should be part of
`advance_request_queue` with some extra logic triggering in
`shutdown_reader`? After that, the next-operation functions use some
very simple probing of the input/output state of `Reqd` to determine
what to do next. Only in the case of `Complete` do we move into a
separate function (to make it easier to read):
`_final_<read|write>_operation`.

In these functions, we decide if we should shutdown the respective
reader/writer or consider the `reqd` complete and move it off the queue.
When we do shift it off, we recursively ask for the next operation given
the new queue state. In all cases, before we return the result, we
wakeup the other side so that it too can evaluate the next operation
given the new queue state.

Though on the surface, these pieces feel fairly straightforward, there
are still a slew of re-entrancy bugs to consider. I think there are two
things that we can do to make this drastically easier to manage:

1. We call `t.request_handler` in two places, and this is mostly because
   we want to keep the invariant that the head of the request queue has
   already been passed off to the handler. I feel like splitting this up
   into a simple queue of unhandled requests and a [Reqd.t option] that
   represents the current request would be easier to manage.

2. It would be nice to schedule calls. Things like waking up the writer
   before you let the read loop know its next operation just immediately
   makes my mind fall apart and lose track of state. There's a fairly
   obvious solution of asking for a `schedule : (unit -> unit) -> unit`
   function from the runtime that promises to not call the thunk
   synchronously, but rather waits until it is outside of the read and
   write loops. But maybe we can solve it using what we have now, like
   establishing a contract that when the reader/writer is woken up, they
   must schedule their work for a fresh call stack and not immediately
   ask for operations.

I added a `Queue.clear` to shutdown, not because it was necessary in any
sense, but because it was part of `advance_request_queue_if_necessary`,
which could have come into play in certain situations where `shutdown`
was called from the runtime (e.g., in case of some exception).

I would like to note that despite the fact that all tests pass, I have
very little confidence in this being correct right now and would like to
do some further testing within the actual runtimes.
seliopou pushed a commit that referenced this pull request May 19, 2020
This is a prelude to #159 which introduces upgrade requests, and much
of the diff is identical, with a few trivial changes in `Reqd` and a few
major changes in `Server_connection`.

The goals here are:

1. Make `Reqd` return better types that encapsulate its state instead of
   requiring the user to probe it with `requires_<input|output>` and
   `is_complete`.

2. Try to make queue management easier to reason about by folding bits
   of logic from `advance_request_queue_if_necessary` into
   `next_read_operation` and `next_write_operation` such that we only
   perform side-effects when the operation in question demands it.

One of the ways I tried to make this easier to reason about was to make
the `yield_<reader|writer>` and `next_<read|write>_operation` functions
very parallel. For one, the extra logic in `yield_writer` was puzzling.
Ideally, if you're calling `yield_writer`, you're doing so because you
just called `next_action` and were told to `Yield`, so all of the
conditions being checked should not be possible.

Looking at the next-operation functions, they both start out with a
short-circuit for shutting down when the server can no longer make
progress (reader is closed and queue is empty). This doesn't feel like
it belongs here. Perhaps this check should be part of
`advance_request_queue` with some extra logic triggering in
`shutdown_reader`? After that, the next-operation functions use some
very simple probing of the input/output state of `Reqd` to determine
what to do next. Only in the case of `Complete` do we move into a
separate function (to make it easier to read):
`_final_<read|write>_operation`.

In these functions, we decide if we should shutdown the respective
reader/writer or consider the `reqd` complete and move it off the queue.
When we do shift it off, we recursively ask for the next operation given
the new queue state. In all cases, before we return the result, we
wakeup the other side so that it too can evaluate the next operation
given the new queue state.

Though on the surface, these pieces feel fairly straightforward, there
are still a slew of re-entrancy bugs to consider. I think there are two
things that we can do to make this drastically easier to manage:

1. We call `t.request_handler` in two places, and this is mostly because
   we want to keep the invariant that the head of the request queue has
   already been passed off to the handler. I feel like splitting this up
   into a simple queue of unhandled requests and a [Reqd.t option] that
   represents the current request would be easier to manage.

2. It would be nice to schedule calls. Things like waking up the writer
   before you let the read loop know its next operation just immediately
   makes my mind fall apart and lose track of state. There's a fairly
   obvious solution of asking for a `schedule : (unit -> unit) -> unit`
   function from the runtime that promises to not call the thunk
   synchronously, but rather waits until it is outside of the read and
   write loops. But maybe we can solve it using what we have now, like
   establishing a contract that when the reader/writer is woken up, they
   must schedule their work for a fresh call stack and not immediately
   ask for operations.

I added a `Queue.clear` to shutdown, not because it was necessary in any
sense, but because it was part of `advance_request_queue_if_necessary`,
which could have come into play in certain situations where `shutdown`
was called from the runtime (e.g., in case of some exception).

I would like to note that despite the fact that all tests pass, I have
very little confidence in this being correct right now and would like to
do some further testing within the actual runtimes.
dpatti added a commit that referenced this pull request Apr 2, 2021
This is a prelude to #159 which introduces upgrade requests, and much
of the diff is identical, with a few trivial changes in `Reqd` and a few
major changes in `Server_connection`.

The goals here are:

1. Make `Reqd` return better types that encapsulate its state instead of
   requiring the user to probe it with `requires_<input|output>` and
   `is_complete`.

2. Try to make queue management easier to reason about by folding bits
   of logic from `advance_request_queue_if_necessary` into
   `next_read_operation` and `next_write_operation` such that we only
   perform side-effects when the operation in question demands it.

One of the ways I tried to make this easier to reason about was to make
the `yield_<reader|writer>` and `next_<read|write>_operation` functions
very parallel. For one, the extra logic in `yield_writer` was puzzling.
Ideally, if you're calling `yield_writer`, you're doing so because you
just called `next_action` and were told to `Yield`, so all of the
conditions being checked should not be possible.

Looking at the next-operation functions, they both start out with a
short-circuit for shutting down when the server can no longer make
progress (reader is closed and queue is empty). This doesn't feel like
it belongs here. Perhaps this check should be part of
`advance_request_queue` with some extra logic triggering in
`shutdown_reader`? After that, the next-operation functions use some
very simple probing of the input/output state of `Reqd` to determine
what to do next. Only in the case of `Complete` do we move into a
separate function (to make it easier to read):
`_final_<read|write>_operation`.

In these functions, we decide if we should shutdown the respective
reader/writer or consider the `reqd` complete and move it off the queue.
When we do shift it off, we recursively ask for the next operation given
the new queue state. In all cases, before we return the result, we
wakeup the other side so that it too can evaluate the next operation
given the new queue state.

Though on the surface, these pieces feel fairly straightforward, there
are still a slew of re-entrancy bugs to consider. I think there are two
things that we can do to make this drastically easier to manage:

1. We call `t.request_handler` in two places, and this is mostly because
   we want to keep the invariant that the head of the request queue has
   already been passed off to the handler. I feel like splitting this up
   into a simple queue of unhandled requests and a [Reqd.t option] that
   represents the current request would be easier to manage.

2. It would be nice to schedule calls. Things like waking up the writer
   before you let the read loop know its next operation just immediately
   makes my mind fall apart and lose track of state. There's a fairly
   obvious solution of asking for a `schedule : (unit -> unit) -> unit`
   function from the runtime that promises to not call the thunk
   synchronously, but rather waits until it is outside of the read and
   write loops. But maybe we can solve it using what we have now, like
   establishing a contract that when the reader/writer is woken up, they
   must schedule their work for a fresh call stack and not immediately
   ask for operations.

I added a `Queue.clear` to shutdown, not because it was necessary in any
sense, but because it was part of `advance_request_queue_if_necessary`,
which could have come into play in certain situations where `shutdown`
was called from the runtime (e.g., in case of some exception).

I would like to note that despite the fact that all tests pass, I have
very little confidence in this being correct right now and would like to
do some further testing within the actual runtimes.
dpatti added a commit that referenced this pull request Apr 2, 2021
This is a prelude to #159 which introduces upgrade requests, and much
of the diff is identical, with a few trivial changes in `Reqd` and a few
major changes in `Server_connection`.

The goals here are:

1. Make `Reqd` return better types that encapsulate its state instead of
   requiring the user to probe it with `requires_<input|output>` and
   `is_complete`.

2. Try to make queue management easier to reason about by folding bits
   of logic from `advance_request_queue_if_necessary` into
   `next_read_operation` and `next_write_operation` such that we only
   perform side-effects when the operation in question demands it.

One of the ways I tried to make this easier to reason about was to make
the `yield_<reader|writer>` and `next_<read|write>_operation` functions
very parallel. For one, the extra logic in `yield_writer` was puzzling.
Ideally, if you're calling `yield_writer`, you're doing so because you
just called `next_action` and were told to `Yield`, so all of the
conditions being checked should not be possible.

Looking at the next-operation functions, they both start out with a
short-circuit for shutting down when the server can no longer make
progress (reader is closed and queue is empty). This doesn't feel like
it belongs here. Perhaps this check should be part of
`advance_request_queue` with some extra logic triggering in
`shutdown_reader`? After that, the next-operation functions use some
very simple probing of the input/output state of `Reqd` to determine
what to do next. Only in the case of `Complete` do we move into a
separate function (to make it easier to read):
`_final_<read|write>_operation`.

In these functions, we decide if we should shutdown the respective
reader/writer or consider the `reqd` complete and move it off the queue.
When we do shift it off, we recursively ask for the next operation given
the new queue state. In all cases, before we return the result, we
wakeup the other side so that it too can evaluate the next operation
given the new queue state.

Though on the surface, these pieces feel fairly straightforward, there
are still a slew of re-entrancy bugs to consider. I think there are two
things that we can do to make this drastically easier to manage:

1. We call `t.request_handler` in two places, and this is mostly because
   we want to keep the invariant that the head of the request queue has
   already been passed off to the handler. I feel like splitting this up
   into a simple queue of unhandled requests and a [Reqd.t option] that
   represents the current request would be easier to manage.

2. It would be nice to schedule calls. Things like waking up the writer
   before you let the read loop know its next operation just immediately
   makes my mind fall apart and lose track of state. There's a fairly
   obvious solution of asking for a `schedule : (unit -> unit) -> unit`
   function from the runtime that promises to not call the thunk
   synchronously, but rather waits until it is outside of the read and
   write loops. But maybe we can solve it using what we have now, like
   establishing a contract that when the reader/writer is woken up, they
   must schedule their work for a fresh call stack and not immediately
   ask for operations.

I added a `Queue.clear` to shutdown, not because it was necessary in any
sense, but because it was part of `advance_request_queue_if_necessary`,
which could have come into play in certain situations where `shutdown`
was called from the runtime (e.g., in case of some exception).

I would like to note that despite the fact that all tests pass, I have
very little confidence in this being correct right now and would like to
do some further testing within the actual runtimes.
dpatti added a commit that referenced this pull request Apr 3, 2021
This is a prelude to #159 which introduces upgrade requests, and much
of the diff is identical, with a few trivial changes in `Reqd` and a few
major changes in `Server_connection`.

The goals here are:

1. Make `Reqd` return better types that encapsulate its state instead of
   requiring the user to probe it with `requires_<input|output>` and
   `is_complete`.

2. Try to make queue management easier to reason about by folding bits
   of logic from `advance_request_queue_if_necessary` into
   `next_read_operation` and `next_write_operation` such that we only
   perform side-effects when the operation in question demands it.

One of the ways I tried to make this easier to reason about was to make
the `yield_<reader|writer>` and `next_<read|write>_operation` functions
very parallel. For one, the extra logic in `yield_writer` was puzzling.
Ideally, if you're calling `yield_writer`, you're doing so because you
just called `next_action` and were told to `Yield`, so all of the
conditions being checked should not be possible.

Looking at the next-operation functions, they both start out with a
short-circuit for shutting down when the server can no longer make
progress (reader is closed and queue is empty). This doesn't feel like
it belongs here. Perhaps this check should be part of
`advance_request_queue` with some extra logic triggering in
`shutdown_reader`? After that, the next-operation functions use some
very simple probing of the input/output state of `Reqd` to determine
what to do next. Only in the case of `Complete` do we move into a
separate function (to make it easier to read):
`_final_<read|write>_operation`.

In these functions, we decide if we should shutdown the respective
reader/writer or consider the `reqd` complete and move it off the queue.
When we do shift it off, we recursively ask for the next operation given
the new queue state. In all cases, before we return the result, we
wakeup the other side so that it too can evaluate the next operation
given the new queue state.

Though on the surface, these pieces feel fairly straightforward, there
are still a slew of re-entrancy bugs to consider. I think there are two
things that we can do to make this drastically easier to manage:

1. We call `t.request_handler` in two places, and this is mostly because
   we want to keep the invariant that the head of the request queue has
   already been passed off to the handler. I feel like splitting this up
   into a simple queue of unhandled requests and a [Reqd.t option] that
   represents the current request would be easier to manage.

2. It would be nice to schedule calls. Things like waking up the writer
   before you let the read loop know its next operation just immediately
   makes my mind fall apart and lose track of state. There's a fairly
   obvious solution of asking for a `schedule : (unit -> unit) -> unit`
   function from the runtime that promises to not call the thunk
   synchronously, but rather waits until it is outside of the read and
   write loops. But maybe we can solve it using what we have now, like
   establishing a contract that when the reader/writer is woken up, they
   must schedule their work for a fresh call stack and not immediately
   ask for operations.

I added a `Queue.clear` to shutdown, not because it was necessary in any
sense, but because it was part of `advance_request_queue_if_necessary`,
which could have come into play in certain situations where `shutdown`
was called from the runtime (e.g., in case of some exception).

I would like to note that despite the fact that all tests pass, I have
very little confidence in this being correct right now and would like to
do some further testing within the actual runtimes.
dpatti added a commit that referenced this pull request Apr 3, 2021
This is a prelude to #159 which introduces upgrade requests, with a few
major changes in `Server_connection`.

The goals here is to try to make queue management easier to reason about
by folding bits of logic from `advance_request_queue_if_necessary` into
`next_read_operation` and `next_write_operation` such that we only
perform side-effects when the operation in question demands it.

One of the ways I tried to make this easier to reason about was to make
the `next_<read|write>_operation` functions very parallel. Getting the
read operation starts out with a short-circuit for shutting down when
the server can no longer make progress (reader is closed and queue is
empty). This doesn't feel like it belongs here. Perhaps this check
should be part of `advance_request_queue` with some extra logic
triggering in `shutdown_reader`? After that, the next-operation
functions use some very simple probing of the input/output state of
`Reqd` to determine what to do next. Only in the case of `Complete` do
we move into a separate function (to make it easier to read):
`_final_<read|write>_operation`.

In these functions, we decide if we should shutdown the respective
reader/writer or consider the `reqd` complete and move it off the queue.
What's happening is that we don't know if the write action or read
action will be last, so each function checks the state of the other to
see if they're both complete. When we do shift it off, we recursively
ask for the next operation given the new queue state.

In the case of the writer triggering the advancing, before we return the
result, we wakeup the reader so that it can evaluate the next operation
given the new queue state.

Note that in the case of a non-persistent connection, the queue is never
advanced and the connection is shut down when both sides are done.

Though on the surface, these pieces feel fairly straightforward, there
are still a slew of re-entrancy bugs to consider. I think there are two
things that we can do to make this drastically easier to manage:

1. We call `t.request_handler` in two places, and this is mostly because
   we want to keep the invariant that the head of the request queue has
   already been passed off to the handler. I feel like splitting this up
   into a simple queue of unhandled requests and a [Reqd.t option] that
   represents the current request would be easier to manage.

2. It would be nice to schedule calls. Things like waking up the writer
   before you let the read loop know its next operation just immediately
   makes my mind fall apart and lose track of state. There's a fairly
   obvious solution of asking for a `schedule : (unit -> unit) -> unit`
   function from the runtime that promises to not call the thunk
   synchronously, but rather waits until it is outside of the read and
   write loops. But maybe we can solve it using what we have now, like
   establishing a contract that when the reader/writer is woken up, they
   must schedule their work for a fresh call stack and not immediately
   ask for operations.
dpatti added a commit that referenced this pull request Apr 22, 2021
This is a prelude to #159 which introduces upgrade requests, with a few
major changes in `Server_connection`.

The goals here is to try to make queue management easier to reason about
by folding bits of logic from `advance_request_queue_if_necessary` into
`next_read_operation` and `next_write_operation` such that we only
perform side-effects when the operation in question demands it.

One of the ways I tried to make this easier to reason about was to make
the `next_<read|write>_operation` functions very parallel. Getting the
read operation starts out with a short-circuit for shutting down when
the server can no longer make progress (reader is closed and queue is
empty). This doesn't feel like it belongs here. Perhaps this check
should be part of `advance_request_queue` with some extra logic
triggering in `shutdown_reader`? After that, the next-operation
functions use some very simple probing of the input/output state of
`Reqd` to determine what to do next. Only in the case of `Complete` do
we move into a separate function (to make it easier to read):
`_final_<read|write>_operation`.

In these functions, we decide if we should shutdown the respective
reader/writer or consider the `reqd` complete and move it off the queue.
What's happening is that we don't know if the write action or read
action will be last, so each function checks the state of the other to
see if they're both complete. When we do shift it off, we recursively
ask for the next operation given the new queue state.

In the case of the writer triggering the advancing, before we return the
result, we wakeup the reader so that it can evaluate the next operation
given the new queue state.

Note that in the case of a non-persistent connection, the queue is never
advanced and the connection is shut down when both sides are done.

Though on the surface, these pieces feel fairly straightforward, there
are still a slew of re-entrancy bugs to consider. I think there are two
things that we can do to make this drastically easier to manage:

1. We call `t.request_handler` in two places, and this is mostly because
   we want to keep the invariant that the head of the request queue has
   already been passed off to the handler. I feel like splitting this up
   into a simple queue of unhandled requests and a [Reqd.t option] that
   represents the current request would be easier to manage.

2. It would be nice to schedule calls. Things like waking up the writer
   before you let the read loop know its next operation just immediately
   makes my mind fall apart and lose track of state. There's a fairly
   obvious solution of asking for a `schedule : (unit -> unit) -> unit`
   function from the runtime that promises to not call the thunk
   synchronously, but rather waits until it is outside of the read and
   write loops. But maybe we can solve it using what we have now, like
   establishing a contract that when the reader/writer is woken up, they
   must schedule their work for a fresh call stack and not immediately
   ask for operations.
dpatti added a commit that referenced this pull request Apr 22, 2021
This is a prelude to #159 which introduces upgrade requests, with a few
major changes in `Server_connection`.

The goals here is to try to make queue management easier to reason about
by folding bits of logic from `advance_request_queue_if_necessary` into
`next_read_operation` and `next_write_operation` such that we only
perform side-effects when the operation in question demands it.

One of the ways I tried to make this easier to reason about was to make
the `next_<read|write>_operation` functions very parallel. Getting the
read operation starts out with a short-circuit for shutting down when
the server can no longer make progress (reader is closed and queue is
empty). This doesn't feel like it belongs here. Perhaps this check
should be part of `advance_request_queue` with some extra logic
triggering in `shutdown_reader`? After that, the next-operation
functions use some very simple probing of the input/output state of
`Reqd` to determine what to do next. Only in the case of `Complete` do
we move into a separate function (to make it easier to read):
`_final_<read|write>_operation`.

In these functions, we decide if we should shutdown the respective
reader/writer or consider the `reqd` complete and move it off the queue.
What's happening is that we don't know if the write action or read
action will be last, so each function checks the state of the other to
see if they're both complete. When we do shift it off, we recursively
ask for the next operation given the new queue state.

In the case of the writer triggering the advancing, before we return the
result, we wakeup the reader so that it can evaluate the next operation
given the new queue state.

Note that in the case of a non-persistent connection, the queue is never
advanced and the connection is shut down when both sides are done.

Though on the surface, these pieces feel fairly straightforward, there
are still a slew of re-entrancy bugs to consider. I think there are two
things that we can do to make this drastically easier to manage:

1. We call `t.request_handler` in two places, and this is mostly because
   we want to keep the invariant that the head of the request queue has
   already been passed off to the handler. I feel like splitting this up
   into a simple queue of unhandled requests and a [Reqd.t option] that
   represents the current request would be easier to manage.

2. It would be nice to schedule calls. Things like waking up the writer
   before you let the read loop know its next operation just immediately
   makes my mind fall apart and lose track of state. There's a fairly
   obvious solution of asking for a `schedule : (unit -> unit) -> unit`
   function from the runtime that promises to not call the thunk
   synchronously, but rather waits until it is outside of the read and
   write loops. But maybe we can solve it using what we have now, like
   establishing a contract that when the reader/writer is woken up, they
   must schedule their work for a fresh call stack and not immediately
   ask for operations.
@dhouse-js dhouse-js mentioned this pull request Apr 26, 2021
@dhouse-js
Copy link
Contributor

Hi folks. This PR no longer applies at all cleanly after #172. However, I have tried to recreate a PR with basically the same design in #203.

My PR takes Option B as described by @andreas. This seemed like the much easier method to me, especially as, as was pointed out, Httpaf provides no other way for the client application to write the response to the wire.

dpatti added a commit that referenced this pull request Apr 26, 2021
This is a prelude to #159 which introduces upgrade requests, with a few
major changes in `Server_connection`.

The goals here is to try to make queue management easier to reason about
by folding bits of logic from `advance_request_queue_if_necessary` into
`next_read_operation` and `next_write_operation` such that we only
perform side-effects when the operation in question demands it.

One of the ways I tried to make this easier to reason about was to make
the `next_<read|write>_operation` functions very parallel. Getting the
read operation starts out with a short-circuit for shutting down when
the server can no longer make progress (reader is closed and queue is
empty). This doesn't feel like it belongs here. Perhaps this check
should be part of `advance_request_queue` with some extra logic
triggering in `shutdown_reader`? After that, the next-operation
functions use some very simple probing of the input/output state of
`Reqd` to determine what to do next. Only in the case of `Complete` do
we move into a separate function (to make it easier to read):
`_final_<read|write>_operation`.

In these functions, we decide if we should shutdown the respective
reader/writer or consider the `reqd` complete and move it off the queue.
What's happening is that we don't know if the write action or read
action will be last, so each function checks the state of the other to
see if they're both complete. When we do shift it off, we recursively
ask for the next operation given the new queue state.

In the case of the writer triggering the advancing, before we return the
result, we wakeup the reader so that it can evaluate the next operation
given the new queue state.

Note that in the case of a non-persistent connection, the queue is never
advanced and the connection is shut down when both sides are done.

Though on the surface, these pieces feel fairly straightforward, there
are still a slew of re-entrancy bugs to consider. I think there are two
things that we can do to make this drastically easier to manage:

1. We call `t.request_handler` in two places, and this is mostly because
   we want to keep the invariant that the head of the request queue has
   already been passed off to the handler. I feel like splitting this up
   into a simple queue of unhandled requests and a [Reqd.t option] that
   represents the current request would be easier to manage.

2. It would be nice to schedule calls. Things like waking up the writer
   before you let the read loop know its next operation just immediately
   makes my mind fall apart and lose track of state. There's a fairly
   obvious solution of asking for a `schedule : (unit -> unit) -> unit`
   function from the runtime that promises to not call the thunk
   synchronously, but rather waits until it is outside of the read and
   write loops. But maybe we can solve it using what we have now, like
   establishing a contract that when the reader/writer is woken up, they
   must schedule their work for a fresh call stack and not immediately
   ask for operations.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants