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

refactor request queue mechanics #172

Merged
merged 3 commits into from May 22, 2021
Merged

refactor request queue mechanics #172

merged 3 commits into from May 22, 2021

Conversation

dpatti
Copy link
Collaborator

@dpatti dpatti commented Apr 12, 2020

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 dpatti requested a review from seliopou April 12, 2020 19:16
seliopou pushed a commit that referenced this pull request Apr 28, 2020
These are just the reqd changes from #172
seliopou added a commit that referenced this pull request Apr 28, 2020
seliopou added a commit that referenced this pull request Apr 28, 2020
Part of #172, but changed a constructor name.
seliopou added a commit that referenced this pull request Apr 28, 2020
Part of #172, but changed a constructor name.
Writer.next t.writer;
) else (
match Reqd.input_state reqd with
| Ready -> assert false
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't this just be Writer.next t.writer? A request handler could be done writing a response while still reading a request. Not sure of the wisdom of writing a request handler that does that but it is possible.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That's a good point. This makes me think that a good way to improve testing would be to checkout master and just randomly delete lines from server_connection.ml to see what still passes. Then we can either write a test that makes it fail or decide it wasn't necessary.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is fixed. See last commit for details.

@@ -155,6 +155,7 @@ let error_code t =
else None

let shutdown t =
Queue.clear t.request_queue;
Copy link
Member

Choose a reason for hiding this comment

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

If there is an active connection, this will cause any active request handlers to hang on reading from the request body.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah I wasn't thrilled by this. Definitely worth revisiting and writing more tests for.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This was removed. See last commit for details.

@dpatti dpatti force-pushed the refactor-request-queue branch 2 times, most recently from 568a2d8 to 9c526e7 Compare April 2, 2021 20:28
@dpatti dpatti changed the base branch from master to test-requests-queued-at-close April 2, 2021 20:29
@dpatti dpatti force-pushed the test-requests-queued-at-close branch 2 times, most recently from cfa3085 to e8e8f89 Compare April 3, 2021 23:14
@dpatti dpatti force-pushed the refactor-request-queue branch 2 times, most recently from 4ca78f2 to 6b14105 Compare April 3, 2021 23:27
@dpatti dpatti requested a review from seliopou April 3, 2021 23:28
@dpatti
Copy link
Collaborator Author

dpatti commented Apr 3, 2021

I rewrote the PR description (and first commit message) since they were both out of date after we pulled even more chunks off. I'm feeling pretty good about this now.

Base automatically changed from test-requests-queued-at-close to master April 4, 2021 21:31
)
else Reader.next t.reader
Copy link
Member

Choose a reason for hiding this comment

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

Read looks good.

next
;;

let next_write_operation t = _next_write_operation t
Copy link
Member

Choose a reason for hiding this comment

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

Writer looks good.

@dpatti dpatti force-pushed the refactor-request-queue branch 4 times, most recently from 3178faa to cce55fd Compare April 22, 2021 19:23
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.
This is because the writer is always woken by the appropriate calls that
push chunks onto the body or writer or calls that close the body.

Had to import an additional line from a recent band-aid fix regarding
setting the flag on non-chunked streaming responses. It feels like we
should find an alternative means of maintaining this piece of
information.
We basically never want to call `Queue.clear` because the head of the
queue has special semantic meaning. Instead, we never try to clear the
queue and rely on the fact that the queue will never be advanced. This
is easy to reason about because the only time we advance the request
queue is when the current request is not persistent. I added an explicit
test of this situation to build confidence.

Additionally, there was an incorrect assertion that you couldn't finish
a write with reads still pending. A test was added upstream and it no
longer fails with this fix.

The final change was some subtle but unused code. In the write loop, we
have something that decides to shutdown the connection if the reader is
closed, parallel to the next read operation. But this felt weird, the
reader should always be awake in the case that it is closed, which means
that either 1) it will shutdown the connection or 2) it will wait for
the writer, which will wake the reader once it's advanced the request
queue, and then it will shutdown the connection.
@seliopou
Copy link
Member

Is this good? I think this is good.

@dpatti
Copy link
Collaborator Author

dpatti commented May 22, 2021

@seliopou I think we should release this. We don't have to tag until more of the other features are released, and having this in master would definitely simplify the development story.

@seliopou seliopou merged commit cc7478a into master May 22, 2021
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.

None yet

3 participants