blob: 824ba8e6ea138f5e560c04c70ff73e64c075c730 [file] [log] [blame]
Low-Level Details
=================
.. warning:: This section of the documentation covers low-level implementation
details of hyper-h2. This is most likely to be of use to hyper-h2
developers and to other HTTP/2 implementers, though it could well
be of general interest. Feel free to peruse it, but if you're
looking for information about how to *use* hyper-h2 you should
consider looking elsewhere.
State Machines
--------------
hyper-h2 is fundamentally built on top of a pair of interacting Finite State
Machines. One of these FSMs manages per-connection state, and another manages
per-stream state. Almost without exception (see :ref:`priority` for more
details) every single frame is unconditionally translated into events for
both state machines and those state machines are turned.
The advantages of a system such as this is that the finite state machines can
very densely encode the kinds of things that are allowed at any particular
moment in a HTTP/2 connection. However, most importantly, almost all protocols
are defined *in terms* of finite state machines: that is, protocol descriptions
can be reduced to a number of states and inputs. That makes FSMs a very natural
tool for implementing protocol stacks.
Indeed, most protocol implementations that do not explicitly encode a finite
state machine almost always *implicitly* encode a finite state machine, by
using classes with a bunch of variables that amount to state-tracking
variables, or by using the call-stack as an implicit state tracking mechanism.
While these methods are not immediately problematic, they tend to lack
*explicitness*, and can lead to subtle bugs of the form "protocol action X is
incorrectly allowed in state Y".
For these reasons, we have implemented two *explicit* finite state machines.
These machines aim to encode most of the protocol-specific state, in particular
regarding what frame is allowed at what time. This target goal is sometimes not
achieved: in particular, as of this writing the *stream* FSM contains a number
of other state variables that really ought to be rolled into the state machine
itself in the form of new states, or in the form of a transformation of the
FSM to use state *vectors* instead of state *scalars*.
The following sections contain some implementers notes on these FSMs.
Connection State Machine
~~~~~~~~~~~~~~~~~~~~~~~~
The "outer" state machine, the first one that is encountered when sending or
receiving data, is the connection state machine. This state machine tracks
whole-connection state.
This state machine is primarily intended to forbid certain actions on the basis
of whether the implementation is acting as a client or a server. For example,
clients are not permitted to send ``PUSH_PROMISE`` frames: this state machine
forbids that by refusing to define a valid transition from the ``CLIENT_OPEN``
state for the ``SEND_PUSH_PROMISE`` event.
Otherwise, this particular state machine triggers no side-effects. It has a
very coarse, high-level, functionality.
A visual representation of this FSM is shown below:
.. image:: _static/h2.connection.H2ConnectionStateMachine.dot.png
:alt: A visual representation of the connection FSM.
:target: _static/h2.connection.H2ConnectionStateMachine.dot.png
.. _stream-state-machine:
Stream State Machine
~~~~~~~~~~~~~~~~~~~~
Once the connection state machine has been spun, any frame that belongs to a
stream is passed to the stream state machine for its given stream. Each stream
has its own instance of the state machine, but all of them share the transition
table: this is because the table itself is sufficiently large that having it be
per-instance would be a ridiculous memory overhead.
Unlike the connection state machine, the stream state machine is quite complex.
This is because it frequently needs to encode some side-effects. The most
common side-effect is emitting a ``RST_STREAM`` frame when an error is
encountered: the need to do this means that far more transitions need to be
encoded than for the connection state machine.
Many of the side-effect functions in this state machine also raise
:class:`ProtocolError <h2.exceptions.ProtocolError>` exceptions. This is almost
always done on the basis of an extra state variable, which is an annoying code
smell: it should always be possible for the state machine itself to police
these using explicit state management. A future refactor will hopefully address
this problem by making these additional state variables part of the state
definitions in the FSM, which will lead to an expansion of the number of states
but a greater degree of simplicity in understanding and tracking what is going
on in the state machine.
The other action taken by the side-effect functions defined here is returning
:ref:`events <h2-events-basic>`. Most of these events are returned directly to
the user, and reflect the specific state transition that has taken place, but
some of the events are purely *internal*: they are used to signal to other
parts of the hyper-h2 codebase what action has been taken.
The major use of the internal events functionality at this time is for
validating header blocks: there are different rules for request headers than
there are for response headers, and different rules again for trailers. The
internal events are used to determine *exactly what* kind of data the user is
attempting to send, and using that information to do the correct kind of
validation. This approach ensures that the final source of truth about what's
happening at the protocol level lives inside the FSM, which is an extremely
important design principle we want to continue to enshrine in hyper-h2.
A visual representation of this FSM is shown below:
.. image:: _static/h2.stream.H2StreamStateMachine.dot.png
:alt: A visual representation of the stream FSM.
:target: _static/h2.stream.H2StreamStateMachine.dot.png
.. _priority:
Priority
~~~~~~~~
In the :ref:`stream-state-machine` section we said that any frame that belongs
to a stream is passed to the stream state machine. This turns out to be not
quite true.
Specifically, while ``PRIORITY`` frames are technically sent on a given stream
(that is, `RFC 7540 Section 6.3`_ defines them as "always identifying a stream"
and forbids the use of stream ID ``0`` for them), in practice they are almost
completely exempt from the usual stream FSM behaviour. Specifically, the RFC
has this to say:
The ``PRIORITY`` frame can be sent on a stream in any state, though it
cannot be sent between consecutive frames that comprise a single
header block (Section 4.3).
Given that the consecutive header block requirement is handled outside of the
FSMs, this section of the RFC essentially means that there is *never* a
situation where it is invalid to receive a ``PRIORITY`` frame. This means that
including it in the stream FSM would require that we allow ``SEND_PRIORITY``
and ``RECV_PRIORITY`` in all states.
This is not a totally onerous task: however, another key note is that hyper-h2
uses the *absence* of a stream state machine to flag a closed stream. This is
primarily for memory conservation reasons: if we needed to keep around an FSM
for every stream we've ever seen, that would cause long-lived HTTP/2
connections to consume increasingly large amounts of memory. On top of this,
it would require us to create a stream FSM each time we received a ``PRIORITY``
frame for a given stream, giving a malicious peer an easy route to force a
hyper-h2 user to allocate nearly unbounded amounts of memory.
For this reason, hyper-h2 circumvents the stream FSM entirely for ``PRIORITY``
frames. Instead, these frames are treated as being connection-level frames that
*just happen* to identify a specific stream. They do not bring streams into
being, or in any sense interact with hyper-h2's view of streams. Their stream
details are treated as strictly metadata that hyper-h2 is not interested in
beyond being able to parse it out.
.. _RFC 7540 Section 6.3: https://tools.ietf.org/html/rfc7540#section-6.3