xref: /aosp_15_r20/external/cronet/net/docs/code-patterns.md (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
1# Chrome Network Stack Common Coding Patterns
2
3## Combined error and byte count into a single value
4
5At many places in the network stack, functions return a value that, if
6positive, indicate a count of bytes that the the function read or
7wrote, and if negative, indicates a network stack error code (see
8[net_error_list.h][]).
9Zero indicates either `net::OK` or zero bytes read (usually EOF)
10depending on the context. This pattern is generally specified by
11an `int` return type.
12
13Many functions also have variables (often named `result` or `rv`) containing
14such a value; this is especially common in the [DoLoop](#DoLoop) pattern
15described below.
16
17## Sync/Async Return
18
19Many network stack routines may return synchronously or
20asynchronously. These functions generally return an int as described
21above. There are three cases:
22
23* If the value is positive or zero, that indicates a synchronous
24  successful return, with a zero return value indicating either zero
25  bytes/EOF or indicating `net::OK`, depending on context. If there
26  is a callback argument, it is not invoked.
27* If the value is negative and != `net::ERR_IO_PENDING`, it is an error
28  code specifying a synchronous failure. If there is a callback argument,
29  it is not invoked.
30* If the return value is the special value `net::ERR_IO_PENDING`, it
31  indicates that the routine will complete asynchronously. A reference to
32  any provided IOBuffer will be retained by the called entity until
33  completion, to be written into or read from as required.
34  If there is a callback argument, that callback will be called upon
35  completion with the return value; if there is no callback argument, it
36  usually means that some known callback mechanism will be employed.
37
38## DoLoop
39
40The DoLoop pattern is used in the network stack to construct simple
41state machines. It is used for cases in which processing is basically
42single-threaded and could be written in a single function, if that
43function could block waiting for input. Generally, initiation of a
44state machine is triggered by some method invocation by a class
45consumer, and that state machine is driven (possibly across
46asynchronous IO initiated by the class) until the operation requested
47by the method invocation completes, at which point the state variable is
48set to `STATE_NONE` and the consumer notified.
49
50Cases which do not fit into this single-threaded, single consumer
51operation model are generally adapted in some way to fit the model,
52either by multiple state machines (e.g. independent state machines for
53reading and writing, if each can be initiated while the other is
54outstanding) or by storing information across consumer invocations and
55returns that can be used to restart the state machine in the proper
56state.
57
58Any class using this pattern will contain an enum listing all states
59of that machine, and define a function, `DoLoop()`, to drive that state
60machine. If a class has multiple state machines (as above) it will
61have multiple methods (e.g. `DoReadLoop()` and `DoWriteLoop()`) to drive
62those different machines.
63
64The characteristics of the DoLoop pattern are:
65
66*   Each state has a corresponding function which is called by `DoLoop()`
67    for handling when the state machine is in that state. Generally the
68    states are named STATE`_<`STATE_NAME`>` (upper case separated by
69    underscores), and the routine is named Do`<`StateName`>` (CamelCase).
70    For example:
71
72         enum State {
73             STATE_NONE,
74             STATE_INIT,
75             STATE_FOO,
76             STATE_FOO_COMPLETE,
77         };
78         int DoInit();
79         int DoFoo();
80         int DoFooComplete(int result);
81
82*   Each state handling function has two basic responsibilities in
83    addition to state specific handling: Setting the data member
84    (named `next_state_` or something similar)
85    to specify the next state, and returning a `net::Error` (or combined
86    error and byte count, as above).
87
88*   On each `DoLoop()` iteration, the function saves the next state to a local
89    variable and resets to a default state (`STATE_NONE`),
90    and then calls the appropriate state handling based on the
91    original value of the next state. This looks like:
92
93           do {
94             State state = io_state_;
95             next_state_ = STATE_NONE;
96             switch (state) {
97               case STATE_INIT:
98                 result = DoInit();
99                 break;
100               ...
101
102    This pattern is followed primarily to ensure that in the event of
103    a bug where the next state isn't set, the loop terminates rather
104    than loops infinitely. It's not a perfect mitigation, but works
105    well as a defensive measure.
106
107*   If a given state may complete asynchronously (for example,
108    writing to an underlying transport socket), then there will often
109    be split states, such as `STATE_WRITE` and
110    `STATE_WRITE_COMPLETE`. The first state is responsible for
111    starting/continuing the original operation, while the second state
112    is responsible for handling completion (e.g. success vs error,
113    complete vs. incomplete writes), and determining the next state to
114    transition to.
115
116*   While the return value from each call is propagated through the loop
117    to the next state, it is expected that for most state transitions the
118    return value will be `net::OK`, and that an error return will also
119    set the state to `STATE_NONE` or fail to override the default
120    assignment to `STATE_DONE` to exit the loop and return that
121    error to the caller. This is often asserted with a DCHECK, e.g.
122
123            case STATE_FOO:
124                DCHECK_EQ(result, OK);
125                result = DoFoo();
126                break;
127
128    The exception to this pattern is split states, where an IO
129    operation has been dispatched, and the second state is handling
130    the result. In that case, the second state's function takes the
131    result code:
132
133            case STATE_FOO_COMPLETE:
134                result = DoFooComplete(result);
135                break;
136
137*   If the return value from the state handling function is
138    `net::ERR_IO_PENDING`, that indicates that the function has arranged
139    for `DoLoop()` to be called at some point in the future, when further
140    progress can be made on the state transitions. The `next_state_` variable
141    will have been set to the proper value for handling that incoming
142    call. In this case, `DoLoop()` will exit. This often occurs between
143    split states, as described above.
144
145*   The DoLoop mechanism is generally invoked in response to a consumer
146    calling one of its methods. While the operation that method
147    requested is occuring, the state machine stays active, possibly
148    over multiple asynchronous operations and state transitions. When
149    that operation is complete, the state machine transitions to
150    `STATE_NONE` (by a `DoLoop()` callee not setting `next_state_`) or
151    explicitly to `STATE_DONE` (indicating that the operation is
152    complete *and* the state machine is not amenable to further
153    driving). At this point the consumer is notified of the completion
154    of the operation (by synchronous return or asynchronous callback).
155
156    Note that this implies that when `DoLoop()` returns, one of two
157    things will be true:
158
159    * The return value will be `net::ERR_IO_PENDING`, indicating that the
160      caller should take no action and instead wait for asynchronous
161      notification.
162    * The state of the machine will be either `STATE_DONE` or `STATE_NONE`,
163      indicating that the operation that first initiated the `DoLoop()` has
164      completed.
165
166    This invariant reflects and enforces the single-threaded (though
167    possibly asynchronous) nature of the driven state machine--the
168    machine is always executing one requested operation.
169
170*   `DoLoop()` is called from two places: a) methods exposed to the consumer
171    for specific operations (e.g. `ReadHeaders()`), and b) an IO completion
172    callbacks called asynchronously by spawned IO operations.
173
174    In the first case, the return value from `DoLoop()` is returned directly
175    to the caller; if the operation completed synchronously, that will
176    contain the operation result, and if it completed asynchronously, it
177    will be `net::ERR_IO_PENDING`. For example (from
178    `HttpStreamParser`, abridged for clarity):
179
180             int HttpStreamParser::ReadResponseHeaders(
181                 CompletionOnceCallback callback) {
182               DCHECK(io_state_ == STATE_NONE || io_state_ == STATE_DONE);
183               DCHECK(callback_.is_null());
184               DCHECK(!callback.is_null());
185
186               int result = OK;
187               io_state_ = STATE_READ_HEADERS;
188
189               result = DoLoop(result);
190
191               if (result == ERR_IO_PENDING)
192                 callback_ = std::move(callback);
193
194               return result > 0 ? OK : result;
195             }
196
197    In the second case, the IO completion callback will examine the
198    return value from `DoLoop()`. If it is `net::ERR_IO_PENDING`, no
199    further action will be taken, and the IO completion callback will be
200    called again at some future point. If it is not
201    `net::ERR_IO_PENDING`, that is a signal that the operation has
202    completed, and the IO completion callback will call the appropriate
203    consumer callback to notify the consumer that the operation has
204    completed. Note that it is important that this callback be done
205    from the IO completion callback and not from `DoLoop()` or a
206    `DoLoop()` callee, both to support the sync/async error return
207    (DoLoop and its callees don't know the difference) and to avoid
208    consumer callbacks deleting the object out from under `DoLoop()`.
209    Example:
210
211             void HttpStreamParser::OnIOComplete(int result) {
212               result = DoLoop(result);
213
214               if (result != ERR_IO_PENDING && !callback_.is_null())
215                 std::move(callback_).Run(result);
216             }
217
218*   The DoLoop pattern has no concept of different events arriving for
219    a single state; each state, if waiting, is waiting for one
220    particular event, and when `DoLoop()` is invoked when the machine is
221    in that state, it will handle that event. This reflects the
222    single-threaded model for operations spawned by the state machine.
223
224Public class methods generally have very little processing, primarily wrapping
225`DoLoop()`. For `DoLoop()` entry this involves setting the `next_state_`
226variable, and possibly making copies of arguments into class members. For
227`DoLoop()` exit, it involves inspecting the return and passing it back to
228the caller, and in the asynchronous case, saving any passed completion callback
229for executing by a future subsidiary IO completion (see above example).
230
231This idiom allows synchronous and asynchronous logic to be written in
232the same fashion; it's all just state transition handling. For mostly
233linear state diagrams, the handling code can be very easy to
234comprehend, as such code is usually written linearly (in different
235handling functions) in the order it's executed.
236
237For examples of this idiom, see
238
239* [HttpStreamParser::DoLoop](https://source.chromium.org/chromium/chromium/src/+/HEAD:net/http/http_stream_parser.cc).
240* [HttpNetworkTransaction::DoLoop](https://source.chromium.org/chromium/chromium/src/+/HEAD:net/http/http_network_transaction.cc)
241
242[net_error_list.h]: https://chromium.googlesource.com/chromium/src/+/main/net/base/net_error_list.h#1
243