Flow-IPC 3.0.0
Flow-IPC project: Full implementation reference.
protocol_negotiator.hpp
Go to the documentation of this file.
1/* Flow-IPC: Core
2 * Copyright 2023 Akamai Technologies, Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the
5 * "License"); you may not use this file except in
6 * compliance with the License. You may obtain a copy
7 * of the License at
8 *
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in
12 * writing, software distributed under the License is
13 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
14 * CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing
16 * permissions and limitations under the License. */
17
18/// @file
19#pragma once
20
21#include "ipc/common.hpp"
22#include "ipc/util/util_fwd.hpp"
23#include <flow/log/log.hpp>
24#include <cstdint>
25#include <string>
26
27namespace ipc::transport
28{
29
30/**
31 * A simple state machine that, assuming the opposing side of a comm pathway uses an equivalent state machine,
32 * helps negotiate the protocol version to speak over that pathway, given each side being capable of speaking
33 * a range of protocol versions and reporting the highest such version to the other side. By *comm pathway*
34 * we mean a bidirectional communication channel of some sort with two mutually-opposing endpoints (it need
35 * not be full-duplex).
36 *
37 * It is copyable and movable, so that the containing object can be copyable and movable too.
38 * A moved-from `*this` becomes as-if it was just constructed. (More precisely: a moved-from `*this` is `.reset()`,
39 * which keeps all the initial config but resets any dynamic changes since construction.)
40 *
41 * The algorithm followed is quite straightforward and is exposed completely in the contract of this simple API:
42 * The impetus behind this class is not for it to be able to perform some complex `private` operations but rather
43 * to provide a reusable component that both internal and (optionally) user code can reliably count on to apply
44 * the same consistent rules. So while it does some minimal work, its main value is in presenting a common
45 * algorithm that (1) achieves protocol negotiation and (2) doesn't shoot ourselves in the foot in the face
46 * of future protocol changes.
47 *
48 * ### The algorithm ###
49 * A Protocol_negotiator `*this` assumes it is used by a single open comm pathway's local endpoint, and that a
50 * logically-equivalent Protocol_negotiator (or equivalent software) is used in symmetrical fashion by the opposing
51 * side's endpoint. (For example, a Native_socket_stream in PEER state uses a Protocol_negotiator internally,
52 * and it therefore assumes the opposing Native_socket_stream does the same.)
53 *
54 * Further we assume that there is a range of protocols for the comm pathway, such that at least version 1
55 * (the initial version) exists, and versions 2, 3, ... may be developed (or have already been developed).
56 * There is understood to be no ambiguity as to what a version X of the protocol means: so, if certain software
57 * knows about version X at all, then what it understands protocol version X to be is exactly equal to what any
58 * other software (that also knows of version X) understands about protocol version X.
59 * - We (the local endpoint) can speak a range of versions of the protocol: [L, H] with H >= L >= 1.
60 * Obviously we know L and H: i.e., the code instantiating `*this` knows L and H (in fact it gives them
61 * to our ctor).
62 * - Note: Informally, one can think of H being the *preferred* version for us: We want to speak it if possible.
63 * However, if necessary, we can invoke alternative code paths to speak a lower version for compatibility.
64 * - They (the opposing endpoint) can similarly speak a range [Lp, Hp], with Hp >= Lp >= 1.
65 * However we do not, at first at least, know Lp nor Hp.
66 * - Each side shall speak the *highest* possible version of the protocol such that:
67 * - It is in range [L, H].
68 * - It is in range [Lp, Hp].
69 * - Therefore there are two possibilities:
70 * - If there is *no* such version, then the comm pathway cannot proceed: they have no protocol version in common
71 * they can both speak. The pathway should close ASAP upon either side realizing this.
72 * - If there is such a version, then the comm pathway can proceed. All we need is for *each side* to determine
73 * what that version V is. Naturally each side must come to the same answer.
74 *
75 * While there are various ways to achieve this, including a back-and-forth negotiation, we opt for something quite
76 * simple and symmetrical. (Recall that the opposing side is assumed to have a Protocol_negotiator (equivalent)
77 * following the same logic, and neither side shall be chosen (in our context) to be different from the other.
78 * E.g., there's no client and server dichotomy -- even if in the subsequently negotiated protocol there is; that's
79 * none of our business.) The procedure:
80 * - We send H to them, ASAP.
81 * - Similarly they send Hp to us.
82 * - Having received their Hp, we choose V = min(H, Hp).
83 * - Similarly having received our H, they choose V = min(H, Hp). Important: This value V *is* the same on each
84 * side. However the next computation will potentially differ between the 2 sides.
85 * - On our side: If V < L (which is possible only if V = Hp, meaning Hp > H, meaning we are more advanced than
86 * they are), we are insufficiently backwards-compatible. Therefore V = UNSUPPORTED. We should
87 * close the comm pathway ASAP.
88 * - Similarly, on their side, if V < Lp -- they are more advanced than we are, and they don't speak enough
89 * older versions to accommodate us -- then they will detect that V = UNSUPPORTED and should close the comm
90 * pathway ASAP.
91 * - On our side: Otherwise (V >= L), speak V from this point on.
92 * - Similarly, on their side, if V >= Lp, they shall speak V from this point on.
93 *
94 * The role of Protocol_negotiator is simple:
95 * - It memorizes the local L and H as passed to its ctor, and it starts with V = UNKNOWN. negotiated_proto_ver()
96 * always simply returns V which is the chief output of a `*this`.
97 * - Upon receiving Hp, user gives it to compute_negotiated_proto_ver(); this computes V based on the above
98 * algorithm; namely: `(H <= Hp) ? H : ((Hp >= L) ? Hp : UNSUPPORTED)`. So negotiated_proto_ver() shall return
99 * that value (one of H, Hp, #S_VER_UNSUPPORTED) from then on.
100 * - compute_negotiated_proto_ver() shall not be called again. One can check
101 * `negotiated_proto_ver() == S_VER_UNKNOWN`, if one would rather not independently keep track of whether
102 * compute_negotiated_proto_ver() has been called yet or not.
103 * - local_max_proto_ver_for_sending() shall return H once; after that ALREADY_SENT. This is to encourage/help
104 * the sending-out of our H exactly once, no more.
105 *
106 * Couple notes:
107 * - If H = Hp, then everyone will agree on everything and just speak H. This is perhaps most typical.
108 * Usually then L = Lp also, but it's conceivable they're not equal (e.g. one side has a backwards-compatibility
109 * patch but not the other). It doesn't matter really.
110 * - Otherwise, though, there are 2 possibilities:
111 * - Both sides agree on the same V, with no V = UNSUPPORTED. This occurs, if in fact there is overlap between
112 * [L, H] and [Lp, Hp], just Hp > H or vice versa: One gets to speak its "preferred" protocol, while the other
113 * has to speak an earlier version.
114 * - One side detects V = UNSUPPORTED (the other does not). This occurs, if one side is so much newer than the
115 * other, it doesn't even have backwards-compatibility support for the other side. (And/or perhaps the
116 * protocol does not attempt to ever be backwards-compatible; so H = L and Hp = Lp always.)
117 * - In this case one side will try to proceed; but it won't get far, as the other (more advanced) side
118 * will close the comm pathway instead of either sending or receiving anything beyond its H or Hp.
119 *
120 * We could have avoided the latter asymmetric situation by sending over both L and H (and they both Lp and
121 * Hp); then both sides would do exactly the same computation. However it seems an unnecessary complication
122 * and payload.
123 *
124 * ### Key tip: Coding for version-1 versus one version versus multiple versions ###
125 * Using a `*this` is in and of itself extremely simple; just look at the API and/or read the above. What is somewhat
126 * more subtle is how to organize your comm pathway's behavior around the start, when the negotiation occurs.
127 * That part is also mostly straightforward:
128 * - Before you send out your first stuff, or possibly together with it, send an encoding of
129 * local_max_proto_ver_for_sending().
130 * - When reading your first stuff, read the similar encoding from the opposing side.
131 * Now compute_negotiated_proto_ver() will determine negotiated_proto_ver(); call it V. From this point on:
132 * - Anything you receive should be interpreted per protocol version V.
133 * - This is straightforward: since the stuff that makes it possible to determine V comes first, anytime
134 * you need to know how to interpret anything else, you'll know V already.
135 * - Anything you send should be according to protocol version V.
136 *
137 * That last part is the only subtle part. The potential challenge is that if you want to send something, you may
138 * not have gotten the protocol-negotiation in-message yet; in which case you do *not* know V. If it is necessary
139 * to know it at that point, then there's no choice but to enter a would-block state of sorts, during which time
140 * any out-messages would need to be internally queued or otherwise deferred, until you do know V (have received
141 * the first in-message and called compute_negotiated_proto_ver()). Protocol_negotiator offers no help as such.
142 *
143 * However there are several common situations where this concern is entirely mitigated away. If it applies to you,
144 * then you will have no problem.
145 * - If `local_min_proto_ver == local_max_proto_ver` (to ctor, a/k/a L = H in the above algorithm sketch),
146 * meaning locally you support only one protocol version, then there is no ambiguity in any case.
147 * You know which protocol you'll speak. The only ways there'd be a problem are: (1) compute_negotiated_proto_ver()
148 * determines incompatibility -- by which point the supposed problem is moot; and (2) the other side
149 * does the same -- in which case it will close the comm pathway (not our problem, by design).
150 * - In particular, if L = H = 1 -- in the first release of the protocol -- this is of course the case.
151 * - If you do not maintain backwards compatibility (e.g., L = H = 2, then next time L = H = 3, then... etc.),
152 * then this is also the case.
153 * - If L does not equal H, in many (most?) cases the actual negotiated version V does not affect many (most)
154 * messages sent. E.g., if you have a "ping" message, perhaps it will never change in any version of the
155 * protocol.
156 * - So the problem only occurs when there's actual ambiguity about what to send. Your code may still need to be
157 * somewhat more complex than the L=H case, but perf/responsiveness at least is less likely to be affected.
158 * - If your protocol has a built-in handshake/log-in/etc. phase (where one side is expected to send
159 * a SYN-like thing, and the other side is supposed to reply with an ACK of some kind in response), then you
160 * can specifically:
161 * - Not add any version-dependent payload to the SYN-like (opening) message; or at least defer any such
162 * version-dependent interpretation thereof, until V is known shortly.
163 * - But, do include the version in the SYN-like (opening) message and to its ACK-like response, specifically.
164 * By the time the hand-shake completes, both sides know V and can proceed.
165 *
166 * @note In general, for the case of initial-protocol-release -- version 1 -- the usefulness of Protocol_negotiator
167 * is minimal, but it does exist. It is minimal, because *assuming* both sides promise to follow this
168 * algorithm, then *all* the code actually *needs* to do is: (1) send `H = 1` in some fashion that will never
169 * change in the future; (2) receive Hp in some fashion that will never change in the future either; and
170 * (3) explode, if Hp is not present or is not 1. This is very much doable without Protocol_negotiator.
171 * However it is almost equally easy with Protocol_negotiator, too; and it *is a good practice* to leverage it,
172 * so that if there is a version 2 later, the conventions in force will be unambiguous. Only then it *might*
173 * be necessary to worry about the subtle situation above, where we want to send something out whose expression
174 * depends on V, but we haven't received the protocol-negotiating in-message yet and hence don't know V yet.
175 *
176 * ### Safety, trust assumption ###
177 * We emphasize that this is not authentication. There is the implicit trust that the opposing side will be using
178 * a very specific *protocol for determining the rest of the protocol* and trust us to do the same. Furthermore
179 * there's implicit trust that the protocol versions being referred-to mean the same thing on both sides.
180 *
181 * To establish this trust is well beyond the mission of `*this` class; and explaining how to do so is well beyond
182 * this doc header.
183 *
184 * ### Thread safety ###
185 * For the same `*this`, the standard default assumptions apply (mutating access disallowed concurrently with
186 * any other access) with the following potentially important exception: the outgoing-direction work
187 * (local_max_proto_ver_for_sending()) and the incoming-direction work
188 * (compute_negotiated_proto_ver(), negotiated_proto_ver()) can be safely performed independently/concurrently
189 * w/r/t each other. (This does not extend to reset() or the assignment operators: those touch both
190 * directions' state.)
191 */
193 public flow::log::Log_context
194{
195public:
196 // Types.
197
198 /**
199 * Type sufficient to store a protocol version; positive values identify newer versions of a protocol;
200 * while non-positive values `S_VER_UNKNOWN` and `S_VER_UNSUPPORTED` are special values.
201 */
202 using proto_ver_t = int16_t;
203
204 // Constants.
205
206 /**
207 * A #proto_ver_t value, namely zero, which is a reserved value indicating "unsupported version"; it is not
208 * a valid version number identifying a protocol that can actually be spoken by relevant software. Its specific
209 * meaning is identified specifically where it might be returned or taken by the API.
210 */
211 static constexpr proto_ver_t S_VER_UNSUPPORTED = 0;
212
213 /**
214 * A #proto_ver_t value, namely a negative one, which is a reserved value indicating "unknown version"; it is not
215 * a valid version number identifying a protocol that can actually be spoken by relevant software. Its specific
216 * meaning is identified specifically where it might be returned or taken by the API.
217 */
218 static constexpr proto_ver_t S_VER_UNKNOWN = -1;
219
220 /**
221 * An #proto_ver_t value, namely a negative one, which is a reserved value specifically for return by
222 * local_max_proto_ver_for_sending() indicating: that method has already returned the true `local_max_proto_ver`
223 * earlier; hence one shall not send it to the opposing side again.
224 *
225 * @note An earlier, compatible, version of Protocol_negotiator lacked `S_VER_ALREADY_SENT` and reused
226 * #S_VER_UNKNOWN instead. Then we added this distinct name for clarity at call sites; but for compatibility
227 * the two constants are equal/interchangeable value-wise.
228 */
230
231 // Constructors/destructor.
232
233 /**
234 * Constructs a comm pathway's negotiator object in initial state wherein: (1) negotiated_proto_ver()
235 * returns #S_VER_UNKNOWN (not yet negotiated with opposing Protocol_negotiator); and (2) we've not yet
236 * sent `local_max_proto_ver` to opposing side via first being queried using local_max_proto_ver_for_sending().
237 *
238 * @param logger_ptr
239 * Logger to use for logging subsequently.
240 * @param nickname
241 * String to use subsequently in logging to identify `*this`.
242 * @param local_max_proto_ver
243 * The highest version of the protocol the comm pathway can speak; a/k/a the preferred version.
244 * Positive or undefined behavior (assertion may trip).
245 * @param local_min_proto_ver
246 * The lowest version of the protocol the comm pathway can speak; so either `local_max_proto_ver`
247 * (if our side has no backwards compatibility) or the oldest version with which we are backwards-compatible.
248 * Positive and at most `local_max_proto_ver` or undefined behavior (assertion may trip).
249 */
250 explicit Protocol_negotiator(flow::log::Logger* logger_ptr, util::String_view nickname,
251 proto_ver_t local_max_proto_ver, proto_ver_t local_min_proto_ver);
252
253 /**
254 * Copy-constructs `*this` to be equal to `src` object.
255 *
256 * @param src
257 * Source object.
258 */
260
261 /**
262 * Move-constructs `*this` to be equal to `src`, while `src` becomes as-if just-cted.
263 * More precisely: `src` is `.reset()`, which keeps all the initial config but resets any post-construction
264 * changes.
265 *
266 * @param src
267 * Moved-from object that becomes as-if just-cted (`src.reset()`).
268 */
270
271 // Methods.
272
273 /**
274 * Copy-assigns `*this` to be equal to `src`.
275 *
276 * @param src
277 * Source object.
278 * @return `*this`.
279 */
281
282 /**
283 * Acts identically to the move ctor; or no-op if `&src == this`.
284 *
285 * @param src
286 * Moved-from object that becomes as-if just-cted (`src.reset()`), unless it is `*this`.
287 * @return `*this`.
288 */
290
291 /**
292 * Returns `S_VER_UNKNOWN` before compute_negotiated_proto_ver(); then either the positive version of the protocol
293 * we shall speak subsequently, or `S_VER_UNSUPPORTED` if the two sides are incompatible.
294 *
295 * @return See above.
296 */
298
299 /**
300 * Based on the presumably-just-received-from-opposing-side value of their `local_max_proto_ver`, passed-in as
301 * the arg, calculates the protocol version we shall speak over the comm pathway, so it will be returned by
302 * negotiated_proto_ver() subsequently. Returns `true`; unless it has already been called -- in which case
303 * it is a no-op (outside of logging); returns `false` (unless exception thrown; see next paragraph).
304 *
305 * If it is not a no-op, and the result is that the two sides lack a protocol version they can both speak,
306 * a suggested truthy `Error_code` is emitted using standard Flow error-emission convention.
307 *
308 * Tip: It's a reasonable tactic to potentially call this for every in-message, if you only do so after ensuring
309 * `negotiated_proto_ver() == S_VER_UNKNOWN`. That will effectively ensure the negotiation occurs ASAP and at most
310 * once.
311 *
312 * More formally:
313 * - If `negotiated_proto_ver() != S_VER_UNKNOWN`: does nothing except possibly logging; returns `false`.
314 * Informal tip: If you don't know whether that's the case, check for it via negotiated_proto_ver() and
315 * (if the case) neither try to parse `opposing_max_proto_ver` from your in-message, nor call the present method.
316 * - If `negotiated_proto_ver() == S_VER_UNKNOWN` (the case right after construction): Upon return
317 * `negotiated_proto_ver() != S_VER_UNKNOWN` and:
318 * - If `err_code != nullptr`: `*err_code` is set to falsy on success; else to the truthy suggested
319 * error to emit. Returns `true`.
320 * - If `err_code == nullptr`: a truthy suggested error is emitted via exception.
321 *
322 * @param opposing_max_proto_ver
323 * Value that the opposing Protocol_negotiator (or equivalent) sent to us over pathway:
324 * their `local_max_proto_ver`. Any value is allowed (we will check that it's positive and
325 * report negotiation failure if not). Informal advice: if you were unable to parse
326 * it from your in-message, you should pass `S_VER_UNKNOWN`.
327 * @param err_code
328 * See `flow::Error_code` docs for error reporting semantics. #Error_code generated:
329 * error::Code::S_PROTOCOL_NEGOTIATION_OPPOSING_VER_TOO_OLD (incompatible protocol version -- we're more
330 * advanced and lack backwards-compatibility for their preferred version),
331 * error::Code::S_PROTOCOL_NEGOTIATION_OPPOSING_VER_INVALID (`opposing_max_proto_ver` is invalid: not
332 * positive).
333 * @return `false` if pre-condition was `negotiated_proto_ver() != S_VER_UNKNOWN`, so we no-oped;
334 * `true` otherwise (unless exception thrown, only if `err_code == nullptr`).
335 */
336 bool compute_negotiated_proto_ver(proto_ver_t opposing_max_proto_ver, Error_code* err_code = nullptr);
337
338 /**
339 * Returns `local_max_proto_ver` from ctor the first time it is called and
340 * #S_VER_ALREADY_SENT subsequently.
341 *
342 * Tip: It's a reasonable tactic to call this when about to send any out-message; if it returns
343 * #S_VER_ALREADY_SENT, then you've already sent it and don't need to do so now; otherwise encode this value
344 * before/with the out-message and send it.
345 *
346 * @return See above. Either a positive version number or #S_VER_ALREADY_SENT.
347 */
349
350 /**
351 * Resets the negotiation state, meaning back to the state as-if just after ctor invoked. Hence:
352 * negotiated_proto_ver() yields #S_VER_UNKNOWN, while
353 * local_max_proto_ver_for_sending() would yield not-`S_VER_ALREADY_SENT`.
354 */
355 void reset();
356
357private:
358 // Data.
359
360 /// The `nickname` from ctor. Not `const` so as to support assignment.
361 std::string m_nickname;
362
363 /// `local_max_proto_ver` from ctor. Not `const` so as to support assignment.
365
366 /// `local_min_proto_ver` from ctor. Not `const` so as to support assignment.
368
369 /// Init value `false` indicating local_max_proto_ver_for_sending() has not been called; subsequently `true`.
371
372 /// See negotiated_proto_ver().
374}; // class Protocol_negotiator
375
376} // namespace ipc::transport
A simple state machine that, assuming the opposing side of a comm pathway uses an equivalent state ma...
static constexpr proto_ver_t S_VER_UNSUPPORTED
A proto_ver_t value, namely zero, which is a reserved value indicating "unsupported version"; it is n...
void reset()
Resets the negotiation state, meaning back to the state as-if just after ctor invoked.
std::string m_nickname
The nickname from ctor. Not const so as to support assignment.
int16_t proto_ver_t
Type sufficient to store a protocol version; positive values identify newer versions of a protocol; w...
Protocol_negotiator(flow::log::Logger *logger_ptr, util::String_view nickname, proto_ver_t local_max_proto_ver, proto_ver_t local_min_proto_ver)
Constructs a comm pathway's negotiator object in initial state wherein: (1) negotiated_proto_ver() re...
proto_ver_t local_max_proto_ver_for_sending()
Returns local_max_proto_ver from ctor the first time it is called and S_VER_ALREADY_SENT subsequently...
proto_ver_t m_local_max_proto_ver
local_max_proto_ver from ctor. Not const so as to support assignment.
bool compute_negotiated_proto_ver(proto_ver_t opposing_max_proto_ver, Error_code *err_code=nullptr)
Based on the presumably-just-received-from-opposing-side value of their local_max_proto_ver,...
proto_ver_t negotiated_proto_ver() const
Returns S_VER_UNKNOWN before compute_negotiated_proto_ver(); then either the positive version of the ...
static constexpr proto_ver_t S_VER_UNKNOWN
A proto_ver_t value, namely a negative one, which is a reserved value indicating "unknown version"; i...
static constexpr proto_ver_t S_VER_ALREADY_SENT
An proto_ver_t value, namely a negative one, which is a reserved value specifically for return by loc...
proto_ver_t m_negotiated_proto_ver
See negotiated_proto_ver().
proto_ver_t m_local_min_proto_ver
local_min_proto_ver from ctor. Not const so as to support assignment.
bool m_local_max_proto_ver_sent
Init value false indicating local_max_proto_ver_for_sending() has not been called; subsequently true.
Protocol_negotiator & operator=(const Protocol_negotiator &src)
Copy-assigns *this to be equal to src.
Protocol_negotiator(const Protocol_negotiator &src)
Copy-constructs *this to be equal to src object.
Flow-IPC module providing transmission of structured messages and/or low-level blobs (and more) betwe...
flow::util::String_view String_view
Short-hand for Flow's String_view.
Definition: util_fwd.hpp:112
flow::Error_code Error_code
Short-hand for flow::Error_code which is very common.
Definition: common.hpp:313