naiveproxy/net/docs/code-patterns.md

241 lines
11 KiB
Markdown
Raw Normal View History

2018-12-10 05:59:24 +03:00
# Chrome Network Stack Common Coding Patterns
## Combined error and byte count into a single value
At many places in the network stack, functions return a value that, if
positive, indicate a count of bytes that the the function read or
wrote, and if negative, indicates a network stack error code (see
[net_error_list.h][]).
Zero indicates either `net::OK` or zero bytes read (usually EOF)
depending on the context. This pattern is generally specified by
an `int` return type.
Many functions also have variables (often named `result` or `rv`) containing
such a value; this is especially common in the [DoLoop](#DoLoop) pattern
described below.
## Sync/Async Return
Many network stack routines may return synchronously or
asynchronously. These functions generally return an int as described
above. There are three cases:
* If the value is positive or zero, that indicates a synchronous
successful return, with a zero return value indicating either zero
bytes/EOF or indicating `net::OK`, depending on context.
* If the value is negative and != `net::ERR_IO_PENDING`, it is an error
code specifying a synchronous failure.
* If the return value is the special value `net::ERR_IO_PENDING`, it
indicates that the routine will complete asynchronously. A reference to
any provided IOBuffer will be retained by the called entity until
completion, to be written into or read from as required.
If there is a callback argument, that callback will be called upon
completion with the return value; if there is no callback argument, it
usually means that some known callback mechanism will be employed.
## DoLoop
The DoLoop pattern is used in the network stack to construct simple
state machines. It is used for cases in which processing is basically
single-threaded and could be written in a single function, if that
function could block waiting for input. Generally, initiation of a
state machine is triggered by some method invocation by a class
consumer, and that state machine is driven (possibly across
asynchronous IO initiated by the class) until the operation requested
by the method invocation completes, at which point the state variable is
set to `STATE_NONE` and the consumer notified.
Cases which do not fit into this single-threaded, single consumer
operation model are generally adapted in some way to fit the model,
either by multiple state machines (e.g. independent state machines for
reading and writing, if each can be initiated while the other is
outstanding) or by storing information across consumer invocations and
returns that can be used to restart the state machine in the proper
state.
Any class using this pattern will contain an enum listing all states
of that machine, and define a function, `DoLoop()`, to drive that state
machine. If a class has multiple state machines (as above) it will
have multiple methods (e.g. `DoReadLoop()` and `DoWriteLoop()`) to drive
those different machines.
The characteristics of the DoLoop pattern are:
* Each state has a corresponding function which is called by `DoLoop()`
for handling when the state machine is in that state. Generally the
states are named STATE`_<`STATE_NAME`>` (upper case separated by
underscores), and the routine is named Do`<`StateName`>` (CamelCase).
For example:
enum State {
STATE_NONE,
STATE_INIT,
STATE_FOO,
STATE_FOO_COMPLETE,
};
int DoInit();
int DoFoo();
int DoFooComplete(int result);
* Each state handling function has two basic responsibilities in
addition to state specific handling: Setting the data member
(named `next_state_` or something similar)
to specify the next state, and returning a `net::Error` (or combined
error and byte count, as above).
* On each `DoLoop()` iteration, the function saves the next state to a local
variable and resets to a default state (`STATE_NONE`),
and then calls the appropriate state handling based on the
original value of the next state. This looks like:
do {
State state = io_state_;
next_state_ = STATE_NONE;
switch (state) {
case STATE_INIT:
result = DoInit();
break;
...
This pattern is followed primarily to ensure that in the event of
a bug where the next state isn't set, the loop terminates rather
than loops infinitely. It's not a perfect mitigation, but works
well as a defensive measure.
* If a given state may complete asynchronously (for example,
writing to an underlying transport socket), then there will often
be split states, such as `STATE_WRITE` and
`STATE_WRITE_COMPLETE`. The first state is responsible for
starting/continuing the original operation, while the second state
is responsible for handling completion (e.g. success vs error,
complete vs. incomplete writes), and determining the next state to
transition to.
* While the return value from each call is propagated through the loop
to the next state, it is expected that for most state transitions the
return value will be `net::OK`, and that an error return will also
set the state to `STATE_NONE` or fail to override the default
assignment to `STATE_DONE` to exit the loop and return that
error to the caller. This is often asserted with a DCHECK, e.g.
case STATE_FOO:
DCHECK_EQ(result, OK);
result = DoFoo();
break;
The exception to this pattern is split states, where an IO
operation has been dispatched, and the second state is handling
the result. In that case, the second state's function takes the
result code:
case STATE_FOO_COMPLETE:
result = DoFooComplete(result);
break;
* If the return value from the state handling function is
`net::ERR_IO_PENDING`, that indicates that the function has arranged
for `DoLoop()` to be called at some point in the future, when further
progress can be made on the state transitions. The `next_state_` variable
will have been set to the proper value for handling that incoming
call. In this case, `DoLoop()` will exit. This often occurs between
split states, as described above.
* The DoLoop mechanism is generally invoked in response to a consumer
calling one of its methods. While the operation that method
requested is occuring, the state machine stays active, possibly
over multiple asynchronous operations and state transitions. When
that operation is complete, the state machine transitions to
`STATE_NONE` (by a `DoLoop()` callee not setting `next_state_`) or
explicitly to `STATE_DONE` (indicating that the operation is
complete *and* the state machine is not amenable to further
driving). At this point the consumer is notified of the completion
of the operation (by synchronous return or asynchronous callback).
Note that this implies that when `DoLoop()` returns, one of two
things will be true:
* The return value will be `net::ERR_IO_PENDING`, indicating that the
caller should take no action and instead wait for asynchronous
notification.
* The state of the machine will be either `STATE_DONE` or `STATE_NONE`,
indicating that the operation that first initiated the `DoLoop()` has
completed.
This invariant reflects and enforces the single-threaded (though
possibly asynchronous) nature of the driven state machine--the
machine is always executing one requested operation.
* `DoLoop()` is called from two places: a) methods exposed to the consumer
for specific operations (e.g. `ReadHeaders()`), and b) an IO completion
callbacks called asynchronously by spawned IO operations.
In the first case, the return value from `DoLoop()` is returned directly
to the caller; if the operation completed synchronously, that will
contain the operation result, and if it completed asynchronously, it
will be `net::ERR_IO_PENDING`. For example (from
`HttpStreamParser`, abridged for clarity):
int HttpStreamParser::ReadResponseHeaders(
const CompletionCallback& callback) {
DCHECK(io_state_ == STATE_NONE || io_state_ == STATE_DONE);
DCHECK(callback_.is_null());
DCHECK(!callback.is_null());
int result = OK;
io_state_ = STATE_READ_HEADERS;
result = DoLoop(result);
if (result == ERR_IO_PENDING)
callback_ = callback;
return result > 0 ? OK : result;
}
In the second case, the IO completion callback will examine the
return value from `DoLoop()`. If it is `net::ERR_IO_PENDING`, no
further action will be taken, and the IO completion callback will be
called again at some future point. If it is not
`net::ERR_IO_PENDING`, that is a signal that the operation has
completed, and the IO completion callback will call the appropriate
consumer callback to notify the consumer that the operation has
completed. Note that it is important that this callback be done
from the IO completion callback and not from `DoLoop()` or a
`DoLoop()` callee, both to support the sync/async error return
(DoLoop and its callees don't know the difference) and to avoid
consumer callbacks deleting the object out from under `DoLoop()`.
Example:
void HttpStreamParser::OnIOComplete(int result) {
result = DoLoop(result);
if (result != ERR_IO_PENDING && !callback_.is_null())
base::ResetAndReturn(&callback_).Run(result);
}
* The DoLoop pattern has no concept of different events arriving for
a single state; each state, if waiting, is waiting for one
particular event, and when `DoLoop()` is invoked when the machine is
in that state, it will handle that event. This reflects the
single-threaded model for operations spawned by the state machine.
Public class methods generally have very little processing, primarily wrapping
`DoLoop()`. For `DoLoop()` entry this involves setting the `next_state_`
variable, and possibly making copies of arguments into class members. For
`DoLoop()` exit, it involves inspecting the return and passing it back to
the caller, and in the asynchronous case, saving any passed completion callback
for executing by a future subsidiary IO completion (see above example).
This idiom allows synchronous and asynchronous logic to be written in
the same fashion; it's all just state transition handling. For mostly
linear state diagrams, the handling code can be very easy to
comprehend, as such code is usually written linearly (in different
handling functions) in the order it's executed.
For examples of this idiom, see
* [HttpStreamParser::DoLoop](https://code.google.com/p/chromium/codesearch#chromium/src/net/http/http_stream_parser.cc&q=HttpStreamParser::DoLoop&sq=package:chromium).
* [HttpNetworkTransaction::DoLoop](https://code.google.com/p/chromium/codesearch#chromium/src/net/http/http_network_transaction.cc&q=HttpNetworkTransaction::DoLoop&sq=package:chromium)
[net_error_list.h]: https://chromium.googlesource.com/chromium/src/+/master/net/base/net_error_list.h#1