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