multiplexer::SyncClient in multiplexer/client.h
is the class a program on one thread connects with. It is an alias of the
class multiplexer::Client, its name before 2.4.0, which compiler messages
show and a forward declaration must name (class Client;), since an alias
cannot be forward-declared. multiplexer::backend::BaseMultiplexerServer in
multiplexer/backend/base_multiplexer_server.h
is one class a backend can be built on. The constants generated from the
rules file are in multiplexer/multiplexer.constants.h, as
multiplexer::peers::* and multiplexer::types::*, both std::uint32_t.
Bazel targets: @mx//multiplexer:client,
@mx//multiplexer/backend:base_multiplexer_server and
@mx//multiplexer:multiplexer_cc_constants. The library uses standalone Asio and
protocol buffers; a consuming workspace gets both through mx_dependencies()
and mx_setup(), see examples/README.md. The
complete example is examples/echo.
multiplexer::MultiplexerMessage is the generated protocol buffer class from
Multiplexer.proto. The fields that matter
are the same as in Python: type, message (a std::string of bytes),
id, sender, to, references, workflow (a trace or request id,
UTF-8 text, as key is). Setters are set_type(),
set_message() and so on. id is drawn by the library per attempt: a query's
request sent again, to the backend its search found, carries a new id,
so a backend that must not do the same work twice keys on the payload, not
on id(). A message you pass to a query is never changed, since the library
sends copies: both clients draw an id for every attempt, the first
included, and a reply's references() names the attempt it answers, so
one message may be queried again and again as it is. set_key() gives a
message the key that peers' key filters are matched against
(keys): the whole-message forms of every send and query take
it, the payload forms of query() none; a key over MAX_KEY_SIZE, 256
bytes, is refused at the call, with std::length_error. A key is UTF-8
text, as protobuf's string is: a Rust peer refuses a frame whose key is
not, closing the connection it came on, so a binary key, a digest say,
goes as hex or base64.
Every timeout, and every other length of time a call takes, is seconds
as a float, in every client and server class: on a connect, a receive,
a query, a send, flush(), flush_all(), shutdown() and close(),
serve_forever()'s poll and drain_seconds,
ThreadedServerOptions::connect_timeout, and Delivery's max_delay,
retry_after and timeout (delivery). A negative timeout
sets no deadline, exactly as an infinite one does: the call waits as
long as it takes, and a message sent with it waits for a connection or
for room as long as that takes. So shutdown(-1) and close(-1) write
everything sent before them first, however long that takes; with a
poll of -1 a BaseMultiplexerServer waits for a message before it
calls periodic_task() or sees working cleared, and a
BaseThreadedMultiplexerServer until a drain, stop() or close()
wakes it; and a drain_seconds of -1 gives a drain no cap, so only the
multiplexers' confirmation ends it, or a stop.
0 and NaN mean "don't wait", in every class: a call does what it can at
once and gives up, a receive taking what has arrived, a connect starting
the attempt. A message sent or scheduled with one, by queue(),
schedule_*(), ThreadedClient::send_serialized() and the like, is
placed now on a connection with room, each copy for ALL likewise, and
otherwise dropped and reported at once, never held: NO_ROOM on a full
connection, its tracker reading is_lost() and schedule_all()
counting the connection still, and, with no connection live, NO_CONNECTION
for the calls that would have held it, queue() and the ThreadedClient
sends. schedule_*() place only on a live connection: with none,
schedule_one() returns a null tracker and schedule_all() 0, and
nothing is reported. A
flushing send's message waits, as every flushing send's does, its call's
deadline and ROOM_GRACE_SECONDS more, so that the call ends as a
timeout first. So a flushing send given 0 reports its message not
written, though a connection may write it a moment later: to send
without waiting and learn how the message ended, use
queue(msg, timeout, lane, done) or ThreadedClient's
send(msg, lane, done). shutdown(0) and close(0) drop what is unwritten at
once, a drain_seconds of 0 or NaN ends a drain as it begins, and a
poll of 0 or NaN is a loop that never waits.
On a SyncClient with nothing connected and nothing on its way, never
connected, made with no address or shut down, a wait for a write gives
up at once, whatever its timeout: flush_all() returns false when a
message is held for a connection that cannot come, flush() returns
with the tracker reading in_queue(), and a flushing send() or a query
throws NotConnected, the send's message dropped. With nothing waiting,
after shutdown() for instance, flush_all() returns true, as on
ThreadedClient. A receive with no deadline throws NotConnected then;
one with a deadline waits it out and throws OperationTimedOut, and so
does schedule_one(mxmsg, wrapper, timeout)'s wait for a connection,
NotConnected at its end. A ThreadedClient waits on, as another
thread may connect it meanwhile.
Defaults says what infinity does, and the
default of each.
#include "multiplexer/client.h"
#include "multiplexer/multiplexer.constants.h"
multiplexer::SyncClient client(multiplexer::peers::ECHO_CLIENT);
client.connect("10.0.0.1", 1980);
client.connect("10.0.0.2", 1980);
multiplexer::IncomingMessage reply = client.query("hello", multiplexer::types::ECHO_REQUEST, 10);
std::cout << reply.third->message() << "\n";
client.shutdown();set_drop_observer()anddropped()are under Messages the library gives up on,set_delivery()andclear_delivery()under Delivery.- The constructor takes the peer type and creates its own
io_service; the overloads taking anasio::io_serviceshare yours. The client runs that service only inside its calls, so the peer type must beis_passivein the rules file; this is the one class that needs the mark, every other runs the loop all the time. A call runs whatever handler of the service is ready, another client's too, so the clients that share one service are all driven from one thread, the thread that owns them. connect(host, port, timeout = 10)connects, performs the handshake and returns aConnectionWrapper.hostis an address, IPv4 or IPv6 without brackets (::1), or a name; a name is resolved inside the library, on every attempt, and each address it has is tried in turn, for 5 s at most (CONNECT_ATTEMPT_SECONDS), so that one that drops the attempt holds up the others no longer, and a multiplexer that moved is found at the next reconnect. It does not throw when the multiplexer is unreachable or the name does not resolve yet; the connection is retried every 3 s while the library runs. A multiplexer the client has a connection to, live or on its way, keeps it: connecting to it again returns that connection.async_connect(host, port)returns at once;wait_for_connection(wrapper, timeout)waits for it. The wrapper'starget()is the host and port given,endpoint()the address in use; for a host name, the wrapperasync_connect()returns, made before the name resolved, has none, andconnect()'s, once connected, has it.connect(endpoint, timeout)andasync_connect(endpoint)take anasio::ip::tcp::endpoint.disconnect(host, port), ordisconnect(endpoint), drops a multiplexer given toconnect()orasync_connect(), the same host and port, an address in any spelling: its reconnect stops, and nothing connects to it again unlessconnect()is called again. A connection to it on its way is abandoned; a live one is closed the polite way, asshutdown()closes each: what it had not written goes, in order, to one other connection, or is held for the next, as a lost connection's does (semantics), and what it wrote still arrives. It returns at once whether the client had that multiplexer, a connection to it or a reconnect armed, and throwsNotConnectedaftershutdown()andUsedAfterForkin a forked child, asconnect()does. It is for a program that keeps its own list of multiplexers, when one leaves the list: the library would retry an address nobody serves any more for good, every 3 s, until another deployment's multiplexer gets that address, as pod addresses are reused, and the client joins it.query(payload, type, timeout = 10, lane = nullptr, received = nullptr)andquery(mxmsg, timeout, lane, received)send a request and return anIncomingMessage, a triple whosethirdis ashared_ptr<MultiplexerMessage>with the reply and whosesecondis the connection it came on. The algorithm is the one in how a query is answered: one connection first, then, after a delivery error, a timeout or a lost connection, a search on all of them, then the request again to the backend found, each stage with its owntimeout; the request goes out at most twice, and the query never goes back a stage.mxmsgis the request itself, its emptysenderfilled in, each attempt with an id of its own, ids belonging to attempts, so that one message may be queried again and again; the request sent again is a copy of it, every field kept. A message withtoset is an addressed query, with onetimeoutfor its stages, see below. When the search finds nobody, or the backend it found is gone by the direct request, the query fails at once if nobody took the request, and otherwise, after a timeout or a lost connection, waits out the stage for a late reply from a backend that did (every way a stage ends). ThrowsSyncClient::OperationFailedwhen no backend can be found,SyncClient::OperationTimedOutwhen a stage runs out of time,SyncClient::NotConnectedwhen no connection is live. All three derive fromSyncClient::MxClientError, which derives fromstd::exception. While it waits it reads whatever arrives, and a message that is not its reply, aDELIVERY_ERRORfor an earlier event say, is logged and dropped.receivedhears which backend acknowledged the request, see Knowing a backend took the request.queue(mxmsg, timeout, lane = nullptr, done)andqueue_all(mxmsg, timeout, done)send an event the way every client sends,ThreadedClient'ssend(msg)included, and return at once: the event is queued on one live connection, round robin or the lane's, or on every one, a multiplexer whose first connection is still in its handshake getting its copy at the welcome, waits for room on a full connection, or, with no connection live, is held until one comes up,timeoutseconds at most each way, and is written as a later call runs the loop. A connection that dies with it unwritten hands it, with the rest it had not written, in order, to one other, or has it held for the next; aqueue_allcopy is held too when no connection is live, and dropped while one is. One the client gives up on is reported (Messages the library gives up on).done, when given, hears how the event ended, once, inside a later call that runs the loop: 1 once it was written, the first copy for ALL, 0 once it was given up on orshutdown()came first, as Python'ssend_message(callback=)does. Since it runs inside whichever call runs the loop, a query or a receive among them, it must not receive or query through the client, which would take that call's reply. They return the event's tracker, the first copy's for ALL, null,donenever called, only when nothing may take it: a pinned lane whose connection is gone, or the client shut down.send(mxmsg, timeout, lane = nullptr)writes an event through one connection and returns the one that wrote it, withintimeout: sent asqueue()sends, then waited for, so a connection that dies under the write, or a client with no live connection, costs the reconnect delay, not the event: the way to send an event from a client that may have lost its connection since the last call. A connection that dies with the event still queued hands it to another, which is the one returned and the one a reply comes back through. It throwsNotConnectedwhen nothing wrote it with no connection live, or through a pinned lane whose connection is gone, its message lost with it, andOperationTimedOutotherwise, as the Python clients raise; the C++ThreadedClient's flushingsendreturns 0 instead.send(mxmsg, connection, timeout)prefers that connection.schedule_one(mxmsg, timeout)queues an event on one connection and returns aScheduledMessageTracker, null when no connection is live, since it holds nothing.schedule_one(mxmsg, wrapper, timeout)queues it on that connection while it is live, waiting there for room, and on another once it is gone; with none live it runs the loop up totimeoutfor one to come up and throwsNotConnectedif none did, so it never returns null;queue(mxmsg, timeout, std::make_shared<Lane>(wrapper))is the form that holds the message instead.schedule_all(mxmsg, timeout, used = NULL)queues it on every connection and returns how many, 0 when none is live, and puts those connections in*usedwhen given. A message a full connection cannot take (1024 queued) waits for its room, in order,timeoutseconds at most, as inThreadedClient; it goes in while a later call runs the loop, and is dropped and reported after its timeout. On an idle connection a message is written inside the call that queues it, and the ones behind it while a later call runs the loop:flush(tracker, timeout)runs the loop until that message was written or dropped, ortimeoutpassed, the tracker saying which;flush_all(timeout)until everything sent before the call was written or given up on, what waits for room included, true only when every one was written. A tracker answersin_queue();is_sent(), written to the socket, which the kernel then holds and the multiplexer may not have yet; andis_lost(), dropped: its connection ended before writing it, or it waited past its timeout.receive_message(timeout = -1, end_on_acknowledgement = false)waits for the next message, with no deadline by default, and returns a pair of the message and its connection. With no deadline it throwsNotConnectedat once when nothing could arrive: no connection and none on its way, on a client never connected, made with no address, or shut down; with one, it waits it out then and throwsOperationTimedOut. It runs theacknowledgedcallbacks whose time came (below); withend_on_acknowledgement, a change of the routing or the key filters acknowledged ends the wait too, throwingOperationTimedOutas the time running out would, and so does the last message held for a connection going (held_for_a_connection()): what a loop of receives acts on at once, the server classes' loops among them. A change whose last confirmation came while messages routed before it were still to be received is acknowledged by the receive that takes the last of them, and the next wait ends at it, its callbacks run first.set_routing(const Routing&, acknowledged = {})androuting_acknowledged(): which of a multiplexer's routing paths reach this peer,any,allandlast_resort, told to every multiplexer and carried in every welcome from then on, and whether every one has it in effect, with everything they routed before received; what a backend's drain uses, see How a backend leaves. OnThreadedClienttoo:set_routing()from any thread,routing_acknowledged()from any but the io thread, where it throwsstd::logic_error, as every call that waits for the io thread does.acknowledged, astd::function<void()>when given, is called once the change, or a later one, is acknowledged so: onSyncClientinside areceive_message(), the one after the last message routed before was received, or the one waiting when the acknowledgement came, and it must not receive or query through the client; onThreadedClienton the io thread, as its other callbacks, afteron_messagehad everything routed before, and never aftershutdown(). What it throws is logged.set_key_filters(const BasicClient::KeyFilters&, acknowledged = {})andkey_filters(): which keys of each message type reach this peer by the rules, astd::mapof message type to its patterns, RE2 regular expressions of which one must match a key whole; a type left out takes every key, one with no pattern none, and a message without a key arrives whatever its type. Replaces the filters set before, leaves the routing as it is, and goes asset_routing()does, acknowledged byrouting_acknowledged()andacknowledgedas the routing is: set before connecting to be in effect from the first message.std::invalid_argumentfor a protocol message type or a pattern overMAX_KEY_PATTERN_SIZE, on the calling thread,ThreadedClient's included; a pattern a multiplexer refuses is logged by it, and by the client as an error. The search of a keyed query carries the request's key. See Keys.refuse_arrivals()andrefuse_unread(): how the server classes leave. From the first on, a request that arrives is refused at once with theDELIVERY_ERRORa multiplexer sends for a peer that is gone, so that its sender retries elsewhere, and a reply is dropped, but one that answers a query under way, which reads it while what was read before waits aside; what was read before stays to be received. The second refuses what was read and not received yet.held_for_a_connection(): how many messages wait for a connection to come up, sent while none was live or left by one that died with none to take them; each goes out in order once one is registered, or is given up on at its own deadline. The server classes' drains wait for them, since the close would drop them. OnThreadedClienttoo, from any but the io thread.dropped_while_closing(): the messages the client's connections read after they began closing, which they could only drop, each connection's logged as aWARNINGwhen it ends. The protocol's own answers to what the client sent, aDELIVERY_ERROR,REQUEST_RECEIVED,BACKEND_ERROR, a control frame's status or aPINGthat answers, are not counted: only the client waited for them.instance_id(),client_type(),connections_count(),random64(), andshutdown(timeout = CLOSE_FLUSH_SECONDS), also run by the destructor: it first writes what was sent before it, running the loop asflush_all(timeout)does, and what the loop sent meanwhile, a server's refusals of what it read, a second in all by default, and drops and reports what is still unwritten then, at once with 0, as every client and server class ends; then it runs the loop until every multiplexer has closed its side of the connection too, a round trip,CLOSE_READ_SECONDSat most, so that what was written arrives (semantics). After itconnect(),async_connect()anddisconnect()throwNotConnected, asThreadedClient::connect()does, and nothing is sent or placed. The destructor then waits for a name lookup in progress, for an address given by name, which runs on asio's resolver thread and ends with the client'sio_service: nothing cuts a lookup short, so against a slow or unreachable DNS server it waits up to the resolver's own timeout. A client given addresses never waits so.
A message built by hand that has no id or sender gets them where it is
sent: a fresh id, and the client's instance id as sender. The receiving
library drops a message without an id. SyncClient's queue(),
queue_all() and schedule_*() also take a frame, a
std::shared_ptr<const RawMessage>, sent as it is:
RawMessage::FromMessage(msg), or, for a message serialized already,
new RawMessage(&serialized, msg.id(), msg.type()), the id and type a
drop is reported by.
The same on SyncClient and ThreadedClient; the reasoning is in
the Python API.
- An addressed query is a
query(mxmsg, ...)whose message hasset_to(instance_id): only that instance ever gets it. When a multiplexer reports the instance is not behind it, or the connection dies under the wait, the client locates it with aPINGaddressed to it on every connection and repeats the request through the connection that found it, once; an instance nobody has isOperationFailed(FAILED) at once, unless the request may still be with it, its connection lost under it, when the query waits for that late reply until its timeout; onetimeoutcovers the stages. ThePINGreaches the instance whatever its routing, as every addressed message does, and the server classes andThreadedClientall answer it whatever their search policy, so it finds a backend that declines searches as well as a peer that serves no requests;SyncClientdoes not answer it. The library setsreport_delivery_erroron the request. The old behaviour ofSyncClient::querywithto, a search by type and the request to whichever backend answered, is gone. - A lane,
multiplexer::Laneinmultiplexer/basic_client.h, held asLanePtr(astd::shared_ptr<Lane>), is a soft, late pin, or, made pinned, the hard one; it keeps a stream on one connection:SyncClient'squeue(msg, timeout, lane)andsend(msg, timeout, lane),ThreadedClient'ssend(msg, lane)andsend(msg, lane, timeout), andquery(..., lane)on both go through the lane's connection, the first message taking the connection the library chose, a query leaving the lane on the connection the reply came through. While the lane's connection lives, a message it has no room for waits there for room, so the stream keeps its order. A lane that is not pinned takes another connection when its own dies: the one that took, in order, what the dead one had not written, through every failover since, so the stream has one gap or reorder per failover.Lane(true)is pinned: once it isclosed()the lane refuses:SyncClient::sendandThreadedClient'ssend(msg, lane)throwNotConnected,queue()returns a null tracker, a flushingThreadedClientsendreturns 0, andqueryendsNOT_CONNECTED(NotConnectedonSyncClient); aThreadedClientsend whose lane's connection goes after the call is dropped and reported (CONNECTION_LOST). A pinned message a dying connection had not written is reported lost, never handed to another connection (RawMessage::pinned). A pinned lane's send that runs out of time while its connection lives is a timeout,OperationTimedOutonSyncClient, as on any lane.Lane(connection, pinned)seeds a lane.connection(),holds_connection(),connected(),closed(),pinned()read it. A lane holds its connection weakly and the library keeps no registry, so it lives as long as yourLanePtr; a query in flight holds it until it ends. A lane, once it took a connection, and aConnectionWrapperbelong to the client they came from: given to another client, a send, a queue or a query throwsstd::invalid_argumenton the calling thread. Safe to share with the io thread. - A connection, given to
send(msg, connection, timeout), which flushes on both, toThreadedClient's queueingsend(msg, connection), or toquery(msg, connection, ...), is preferred for that message and replaced when gone, asBaseMultiplexerServer::send_messagereplies the way the request came.Result::reply.secondandIncomingMessage::secondare where a connection comes from.
The same on SyncClient and ThreadedClient, and on the Python clients,
where the reasoning is.
Every query() overload takes a last, optional ReceivedCallback received, a std::function<void(std::uint64_t backend)> from
multiplexer/basic_client.h, called with the instance id of each backend
that acknowledges the request with REQUEST_RECEIVED, what
notify_start() sends, as soon as the acknowledgement arrives:
multiplexer::IncomingMessage reply = client.query(
"hello", multiplexer::types::ECHO_REQUEST, 60, multiplexer::LanePtr(),
[](std::uint64_t backend) { std::cerr << "backend " << backend << " is working on it\n"; });Normally once; again, with the backend's id, when a retry reached a
backend, the same or another, which says the request may be running
twice. Nothing else
about the query changes, and a query without one costs nothing more. On
SyncClient it runs on the calling thread inside query(), while the
query waits, so it must not query or receive through that client, which
could take the reply; on ThreadedClient on the io thread, so it must be
quick.
One that throws a std::exception is logged, and the query goes on.
#include "multiplexer/backend/base_multiplexer_server.h"
#include "multiplexer/multiplexer.constants.h"
using multiplexer::backend::BaseMultiplexerServer;
using mx::util::kwargs::Kwargs;
class Echo : public BaseMultiplexerServer {
public:
Echo(const multiplexer::backend::MultiplexerAddresses &addresses)
: BaseMultiplexerServer(addresses, multiplexer::peers::ECHO_BACKEND) {}
protected:
void handle_message(multiplexer::MultiplexerMessage &mxmsg) override {
std::string payload = mxmsg.message();
// ... work ...
send_message(Kwargs().set("message", payload).set("type", multiplexer::types::ECHO_RESPONSE));
}
};The constructor takes a vector of (host, port) pairs and makes the
instance id; it must be called from a subclass because it is protected.
connect() connects to every address, once: it starts every connection
at once and returns when each has its handshake done or has failed, 10 s
at most in all, so a multiplexer that drops the connect or never answers
holds up the others' requests that long once, not once for every address
after it. It throws nothing for an address it could not reach, which the
library goes on trying. serve_forever() starts the connections itself
unless connect() did, waiting for none: its loop finishes the
handshakes, so the backend serves what one multiplexer routes to it while
another has not welcomed it yet, a multiplexer that never answers holds
up nothing, and no multiplexer knows the backend before it can serve. A
program that only constructs and serves never calls connect(). Call it yourself
when something waits for a line you print before it sends, a test that
reads ready from your stdout or a notebook that greps your log, so that
the line means reachable: the echo backend does. Call it before you
start serve_forever on a thread of your own and send at once, as a
test does: until that thread has connected, the first request finds no
backend and fails. Call it too when you
drive loop_iter yourself instead of serve_forever, and in a test that
wants a backend connected without a thread serving it. A second call does
nothing. The second constructor takes a SyncClient* you created and
connected, for backends that also act as clients; close(), and so the
end of serve_forever(), shuts it down as it would its own, refusing what
it had not read, so it neither sends nor receives after serving.
serve_forever(poll = 1.0f, drain_seconds = 0.0f) runs the loop:
each iteration waits up to poll seconds for a message, handles it if one
came, then calls the virtual periodic_task(), message or not, so anything
checked there takes effect within one poll, unless poll sets no deadline
(timeouts), and a change of the routing or the key filters
that every multiplexer has acknowledged, a drain's included, ends the
wait too, so that periodic_task() and drained() see it at once; a liveness probe that must see the loop answers
from a time written down there (check a backend's
health). It returns, with the
connections closed, when the public working flag is cleared or a drain
is over. loop_iter(timeout)
does one step and throws SyncClient::OperationTimedOut after timeout
seconds. The thread that calls
serve_forever becomes the backend's thread, whichever thread built it;
in debug builds a later call from another thread fails an assertion. A
program driving loop_iter itself from another thread calls
SyncClient::bind_to_current_thread() and then connect() first.
send_message(Kwargs) takes named arguments, because the message has many
optional fields. Keys and their exact types:
| Key | Type | Default while handling a request |
|---|---|---|
message |
std::string, const std::string *, or const MultiplexerMessage * for a message you built yourself |
required |
type |
std::uint32_t |
required unless message is a whole message |
to |
std::uint64_t |
the requester's instance id |
references |
std::uint64_t |
the request's id |
workflow |
std::string or const std::string * |
the request's workflow |
multiplexer |
int BaseMultiplexerServer::ONE or ALL, or a ConnectionWrapper |
the connection the request arrived on |
A whole MultiplexerMessage is sent as it is: beside one, only
multiplexer may be given, and type, to, references or workflow
throws std::invalid_argument. While a request is handled, its empty to,
references and workflow are filled in from the request, as
Request::reply fills a threaded server's. Outside a handler, from
periodic_task() say, there are no such defaults: to and references
are 0 and the message goes through one connection, routed by its type like
any client's. The message is sent as
SyncClient::queue() sends: placed, or held while no connection is live,
and reported if given up on, so a reply through a connection that is gone
waits for another rather than holding the loop. The std::any it returns
holds the message's ScheduledMessageTracker, the first copy's for ALL.
Kwargs stores each value by its static type, so pass exactly the type in
the table: the generated constants already are std::uint32_t, but a
literal or an int needs a cast. A wrong type fails an assertion in a debug
build.
Leaving gracefully. From periodic_task(), call start_draining()
when asked to leave: the backend tells every multiplexer to route it
nothing new by the rules, its drain routing, so no request and no search
is sent to it, while it serves what was already on its way.
serve_forever returns once the virtual drained() says so, having
served what it had read by then and refused what arrived later, however
fast it comes: by default once every multiplexer confirmed, so nothing
more is coming, or drain_seconds after the drain started at the latest; override it to
wait for a condition of your own. set_key_filters(filters, acknowledged = {}), as on SyncClient, declares which keys of each
message type reach the backend: before serve_forever(), from the first
request on, and a drain keeps them (keys); acknowledged
runs on the loop's thread, between two messages, once every multiplexer
has the change and every request routed before it has been handled. set_drain_routing(routing) before the
drain changes what it asks for, by default
multiplexer::backend::direct_only_routing(), every path off: all
kept keeps events coming, last_resort keeps a lone backend serving
through its drain, and either makes the drain last its drain_seconds.
Start from it, Routing routing = direct_only_routing(); routing.set_last_resort(true);, as examples/echo/backend.cc
does: a default-constructed multiplexer::Routing has any and all
on, a drain that turns nothing off. How a backend
leaves draws the phases and what each costs; a request the
multiplexer routed to the backend in the moment it closed goes
unanswered, counted by dropped_while_closing(), kept past close(), and
logged as a WARNING. Overriding
should_respond_to_backend_for_packet_search() puts a condition of your
own behind the search; a drain needs none. The library installs no signal handlers; a handler of
your own must only set a sig_atomic_t that periodic_task() reads, as
examples/echo/backend.cc does. A Python
backend may do the same with a handler that only sets a flag, which the
main thread sees at its next return from C++, within one poll
(api_python.md, the FAQ).
A handler that throws is reported to the requester with BACKEND_ERROR,
unless the message answers another, one with references set, a reply or
someone's BACKEND_ERROR: nobody waits for an answer to it, and two
backends whose handlers throw on what they do not expect would answer each
other's reports for good. Then the virtual
on_handler_exception(const std::exception &) decides:
true, the default, keeps serving; false lets the exception propagate out
of serve_forever, an OperationTimedOut of the handler's own too: only
poll running out is the loop's timeout.
no_response() marks a message as needing no reply; notify_start() sends
REQUEST_RECEIVED to the requester at once, which a query's received
hears of; parse_message<SomeProto>(mxmsg)
parses the payload; report_error(message) answers the request with
BACKEND_ERROR. An exception escaping handle_message is logged and, if no
reply went out yet, reported the same way, so the requester fails at once
rather than timing out; a reply that threw, or that no connection took,
did not go out. A report that fails is logged, the requester waiting out
its timeout, and on_handler_exception() decides all the same; the
backend keeps serving. A Python requester sees
BackendError; a C++ requester's query() returns the BACKEND_ERROR
message itself, so check reply.third->type() when the backend may fail.
close(timeout = CLOSE_FLUSH_SECONDS) ends the connections as
SyncClient::shutdown(timeout) does, what is still queued, the last
replies, written first; a request that arrives meanwhile, or was read and
will not be handled, is refused with DELIVERY_ERROR, so that its sender
retries elsewhere at once. It then destroys its client, which waits for a
name lookup in progress, up to the resolver's own timeout, as
SyncClient's destructor does; so does the server's destructor when
close() did not run.
multiplexer::backend::BaseThreadedMultiplexerServer in
multiplexer/backend/base_threaded_multiplexer_server.h
is the server class whose handlers run on worker threads behind a
heartbeating io thread; target @mx//multiplexer/backend:base_threaded_multiplexer_server.
When to use it rather than BaseMultiplexerServer is in
the Python API: a request
that may take longer than the multiplexer's drop interval, several handled
at once, or a handler that blocks on a query of its own.
#include "multiplexer/backend/base_threaded_multiplexer_server.h"
using multiplexer::backend::BaseThreadedMultiplexerServer;
using multiplexer::backend::RequestPtr;
class Echo : public BaseThreadedMultiplexerServer {
public:
Echo(const multiplexer::backend::MultiplexerAddresses &addresses, const Options &options)
: BaseThreadedMultiplexerServer(addresses, multiplexer::peers::ECHO_BACKEND, options) {}
protected:
void handle_message(const RequestPtr &request) override {
request->reply(upper(request->mxmsg().message()), multiplexer::types::ECHO_RESPONSE);
}
};
multiplexer::backend::ThreadedServerOptions options;
options.workers = 4;
Echo(addresses, options).serve_forever();ThreadedServerOptions:workers(1),queue_size(1024, the requests waiting for a worker; beyond it a request is dropped with no answer, logged at the drop lines' rate, about two lines a second at most, its requester waiting out its first stage's timeout before the search, and the backend keeps its round-robin share, since the multiplexer skips a peer only for its own full queue; one arriving while the server is leaving, routed before the multiplexer applied the drain routing, is refused withDELIVERY_ERROR, so its requester retries at once, and one that answers another is dropped, since refusing a reply could start a loop, how a backend leaves; the largeststd::size_tis no limit),decline_searches_when_full(false: searches are answered while the backend serves, none onceclose()began; true leaves them unanswered while every worker is busy and requests wait),connect_timeoutanddrain_routing(whatstart_draining()tells the multiplexers;anyandalloff by default).- The constructor only makes the instance id;
set_key_filters(filters, acknowledged = {}), as onBaseMultiplexerServer, goes beforeconnect(),acknowledgedrunning onserve_forever()'s thread, beforeperiodic_task(), once every multiplexer has the change and the workers have finished every request routed before it;connect()starts the workers and connects, to every address at once against oneconnect_timeout, asBaseMultiplexerServerdoes, once, andserve_forever()calls it first, so nothing reacheshandle_message()before your constructor has finished, and no multiplexer knows the backend until it can serve.instance_id()is valid from construction. When to callconnect()yourself is as forBaseMultiplexerServerabove: something waits for a line you print before it sends, a test wants the backend connected without serving it, or you startserve_forever()on a thread of your own and send at once; such a backend closes in its own destructor, seeclose()below. handle_message(const RequestPtr &)runs on a worker, with every message that is not the protocol's own and with theDELIVERY_ERRORs for messages the server sent that were not queries, which a handler tells by the type. TheRequest, held byshared_ptrso a handler may keep it and answer from another thread later, hasmxmsg(),connection(),reply(payload, type),reply(MultiplexerMessage)(id, from, to, references and workflow filled in when empty),no_response(),report_error(message),notify_start()andparse_message<T>(); a request destroyed without a reply orno_response()logs a warning. The reply goes the way the request came, or another way when that connection is gone. One reply per request:reply()setsreferences, which a requester built onThreadedClientuses to drop late replies, so a follow-up that is not the reply goes throughclient().send()withtoset and noreferences. A request kept for later no longer counts inpending()once its handler returned, so neitherdrained()norclose()waits for it: a program that answers later overridesdrained()to wait for its own work too. A kept request answers through the server's client: itsreply()afterclose()throwsNotConnected, and after the server is destroyed it would use a freed client, so answer every kept request, or drop it, before the server goes.serve_forever(poll, drain_seconds),stop(),start_draining(),draining(),drained(),periodic_task(),on_handler_exception()andshould_respond_to_backend_for_packet_search()areBaseMultiplexerServer's, with the same meanings,drained()also waiting for the queue to be empty, andserve_forever()woken by the acknowledgement of the server's own changes,set_key_filters()'s and the drain's, and by a drain's last request as well, a change made throughclient().set_routing()seen at the next poll;close(timeout)takes no more messages, lets the workers finish the queue, and ends the connections asThreadedClient::shutdown(timeout)does, the last replies written first, and throwsstd::logic_errorfrom a handler, on a worker, rather than join itself, during another thread'sclose()too,stop()being the call for that; from another thread it makes aserve_forever()running there return, quietly, even while it still connects, and a second call returns once the first is done. The destructor callsclose()when nothing did, but by then your subclass is destroyed, and a request still queued would reach the pure virtualhandle_message(): a server that may still be connected when it is destroyed,connect()called withoutserve_forever(), orserve_forever()running on another thread, callsclose()in its own destructor.pending(),dropped(),instance_id()andclient()for messages that are not replies. A handler that throws gets the requesterBACKEND_ERROR, unless its reply went out, a reply that threw being none, or the message answers another,referencesset, as forBaseMultiplexerServer, and a report that fails is logged;falsefromon_handler_exception()makesserve_forever()return and rethrow. So does an exception out ofon_handler_exception(), or one from a handler not derived fromstd::exception: the worker leaves, and what is still queued once no worker is left is refused, as during a close.
multiplexer::ThreadedClient in
multiplexer/threaded_client.h runs the
connections on a thread of its own, so heartbeats and reconnects happen
without the program calling in, the peer type need not be passive, and any
thread may use it. Target @mx//multiplexer:threaded_client.
set_drop_observer() and dropped() are under Messages the library gives
up on, set_delivery() and
clear_delivery() under Delivery.
multiplexer::ThreadedClient client(multiplexer::peers::MY_SERVICE,
[](const multiplexer::IncomingMessage &m) { handle(*m.third); });
client.connect("10.0.0.1", 1980);
multiplexer::ThreadedClient::Result r = client.query("hello", multiplexer::types::ECHO_REQUEST, 10);
if (r.outcome == multiplexer::ThreadedClient::REPLIED) use(r.reply.third->message());
client.query("hello", multiplexer::types::ECHO_REQUEST, [](const multiplexer::ThreadedClient::Result &r) { ... });
client.send(client.new_message(multiplexer::types::SOME_EVENT, "payload"));
client.shutdown();query(payload, type, timeout, lane, received)blocks and returns aResult:outcomeisREPLIED,TIMED_OUT,FAILED(no backend anywhere, the backend a search found gone, or the addressee gone, with nobody holding the request),NOT_CONNECTEDorSHUT_DOWN, andcheck()returns the reply or throws the exceptionSyncClient::querywould have. The stages areSyncClient's.query(msg, timeout, lane)takes the request as a whole,toincluded, its emptysenderfilled in, each attempt with an id of its own: the addressed form, andquery(msg, connection, ...)prefers a connection, see above.received, the last argument of every overload, is called on the io thread with each backend that acknowledged the request, see above. Any number of threads may call it at once; replies are matched by the ids they reference. Called on the io thread, from a callback, it throwsstd::logic_errorrather than deadlock. The callback form returns at once and runs the callback on the io thread; an empty callback runs the query all the same, for a caller who wants the request delivered and not the reply, its outcome going nowhere. Aftershutdown()it runs the callback at once, on the calling thread, withSHUT_DOWN, so a callback must not query again then, nor take a lock its caller holds. A request that cannot be queued waits the way a message does (below), and the query endsTIMED_OUTwhen a connection was live but had no room in time,NOT_CONNECTEDwhen none was. A query waits for the io thread as a send does (below), and its first stage's timer starts when the io thread takes it up.send(msg)andsend_all(msg)queue a message on one or every connection and return at once; the io thread writes it right after. A message that cannot be queued yet, no connection being live or the connections' queues full (1024 messages each), waits there, behind those sent before it, until a connection comes up or has room, for 10 seconds at most, and is dropped and reported after that (see Messages the library gives up on):send_allgives every live connection its copy, a full one as soon as it has room, and a multiplexer whose first connection is still in its handshake one at its welcome, so that a send just after the start misses none, and a lane waits for room on its own connection while that lives. The io thread takes sends and queries in the order they were made, from a queue of its own that has no bound, all that waits there at once, woken once for as many as came while it was busy; a plain event sent with no lane ordonewhile a delivery is set goes there as its bytes alone (what batching costs). A program that sends faster than the io thread places what it sends holds the difference in memory; a message's time counts from the call, and one the io thread reaches with its time up is placed only where a connection has room for it then, and dropped and reported otherwise.send(msg, lane)andsend(msg, connection)choose the connection.send(msg, lane, done),send(msg, connection, done)andsend_all(msg, done)calldone(written)once on the io thread, 1 when the message was written, the first copy forsend_all, 0 when it was given up on orshutdown()came first;send(msg, nullptr, done)for a message with no lane. All are safe from callbacks.send(msg, timeout),send(msg, lane, timeout),send(msg, connection, timeout)andsend_all(msg, timeout)are the flushing forms: they wait until the message reached the socket, forsend_alluntil one copy did, the others going out from their connections' queues, a connection that dies under it handing it to another or having it held, and return 1 once a copy is written, 0 when none was bytimeoutor the message was given up on, a pinned lane's connection being gone for instance; not from callbacks. Asend_allcopy whose connection dies is held only when no connection is live and dropped otherwise, so a flushingsend_allwhose copies all went with their connections returns 0, even with a connection that came up since. Aftershutdown()every send throwsNotConnected. The serialized forms, what the Python binding uses, take the message's id and type after its bytes, which a drop is reported by, and a callback instead of the wait:send_serialized(serialized, id, type, lane, timeout, done)andsend_all_serialized(serialized, id, type, timeout, done)return at once and calldone(written)once on the io thread, 1 when the message was written, the first copy for every connection, 0 when it was given up on, and reported, orshutdown()came first;send_serialized_with_callback(serialized, id, type, all, timeout, done, lane)is the flushing send so,donehearing 1 once a copy is written, 0 when the message was given up on,timeoutpassed orshutdown()came first.send_serialized_and_wait(serialized, id, type, all, timeout, lane, ¬_connected)andsend_serialized_and_notify(serialized, id, type, all, timeout, done, lane), withdone(written, not_connected), also say why a flushing send wrote nothing, as the synchronous client tells it at its deadline: its message given up on, a pinned lane's connection gone or a shutdown, or no connection live whentimeoutpassed, elsetimeoutpassed with one live, the message then waitingROOM_GRACE_SECONDSmore before it is dropped; the Python clients raiseNotConnectedfor the one andOperationTimedOutfor the other on it, and an asyncio layer awaits the second. All are safe from callbacks. A message overMAX_MESSAGE_SIZEis refused where it is sent or queried, withstd::length_error, a query's request measured as the query may send it again.new_message()fills in id and sender, and every send fills them in on a whole message that left them empty: every receiver drops a message without an id.connect(host, port, timeout)connects asSyncClient::connect()does and waits for the handshake: true once the connection is registered, at once for one connected already, which it keeps, false as soon as it failed, or attimeout, the io thread trying again every 3 s on its own.connect_all(addresses, timeout)connects to every(host, port)at once, each waited for against the sametimeout, so a multiplexer that never welcomes costs it once, not once for every address after it, and returns how many are registered.disconnect(host, port)drops the multiplexer asSyncClient::disconnect()does, on the io thread, and returns once it is done, whether the client had it:connections_count()is down by then, a query through the connection closed goes on as after a lost connection, and aconnect()still waiting for that multiplexer returns false. Neither from callbacks, where they throwstd::logic_errorrather than deadlock; aftershutdown()they throwNotConnected.flush_all(timeout)waits until everything sent before the call has been written or given up on, what still waits for a connection or for room included, ortimeoutseconds, and returns, once the callbacks of those sends have run, whether every one was written: false when one was given up on, which the drop observer names, or when the time ran out. What is sent meanwhile is not waited for, so a flush ends however busy the client is, a message a dying connection hands to another included. Aftershutdown()it returns true at once: what is left to write is the shutdown's, which everyshutdown()call waits for. Whatshutdown()does first. Not from callbacks;flush_all_with_callback(timeout, done)is the same withdone(flushed)on the io thread instead of the wait, safe from callbacks: what an asyncio layer awaits. Aftershutdown(),done(true)runs at once on the calling thread, as a query's callback does then, so it must not take a lock its caller holds.- The
MessageSinkgiven to the constructor runs on the io thread with every message that is not a reply to a query: events and requests addressed to this peer, and aDELIVERY_ERRORfor a message that was not a query, an event whose rule reports errors, one from each multiplexer that could not deliver it. Without one such messages are logged and dropped; nothing is queued. A sink says what it drops throughdrop_lines(), on the io thread, at the library's rate for such lines, the first of a kind at once and the rest as a count about once a second (multiplexer/log_summary.h), its kinds numbered fromBasicClient::OWN_LINES: somxcontrol streamlogssays that no log receiver took its chunks. A late reply to a query that already ended,REQUEST_RECEIVEDfor an untracked query, aPING, which the client answers itself, and aBACKEND_FOR_PACKET_SEARCH, answered with aPINGwhen a policy set withset_search_policy()says yes and dropped otherwise, never reach it. The late-reply rule binds the peers that send to this client:referencesmeans "this is the reply", and what references a query of this client that has ended (for twice the longer of its timeout and 10 s after it ended, 131072 queries at most;forget_finished_ids()forgets them at once, for a test that measures the heap) is dropped whatever its type, so a follow-up that is not the reply must not reference the request; it is addressed to this peer withtoand correlated in the payload (semantics). - Callbacks and the sink must return quickly; they may start asynchronous
queries, send, and call
shutdown(), which does not wait there, but not the blockingqueryorflush_all. One that throws, the sink, a query's or a send's callback, or the search policy, is logged with what it was called for, and the client goes on; a search policy that throws answers no. shutdown(timeout = CLOSE_FLUSH_SECONDS), also run by the destructor, fails every query in flight withSHUT_DOWN, writes what was sent before it, and what the io thread sends meanwhile, a server's refusal of what still arrives say,timeoutseconds in all, asSyncClient::shutdown()does; any other send meanwhile throwsNotConnected, then closes the connections and joins the thread, which ends once every multiplexer has closed its side too, a round trip,CLOSE_READ_SECONDSat most, so that what was written arrives (semantics), and once a name lookup in progress, for an address given by name, has returned: nothing cuts a lookup short, so against a slow or unreachable DNS server the thread ends only at the resolver's own timeout. A call that finds the shutdown begun, on another thread or in a callback, the destructor's included, waits as the one that began it does, until the thread has ended. On the io thread itself, from a callback or where the last reference to the client was dropped in one, it cannot wait for its own thread and leaves it to end on its own, the drain included.
Under the hood the io thread owns a BasicClient; other threads reach it
through io_service::post only, and the clang thread-safety analysis
checks that (--config=clang).
Every client sends the same way, through one mechanism in the library
(BasicClient::send), and the server classes send through their client:
| Call | Returns | No connection live | A loss is learned from |
|---|---|---|---|
SyncClient::queue(msg, timeout, lane, done), queue_all(msg, timeout, done) |
at once, a tracker, null when nothing may take it | held until one comes up, timeout at most, then dropped |
the drop observer and dropped(); done(0) when given, done(1) once written; the tracker's is_lost() |
ThreadedClient::send(msg), send(msg, lane, done), send_all(msg, done) |
at once | held, as above | the drop observer and dropped(); done(0) when given, done(1) once written |
send_serialized(serialized, id, type, lane, timeout, done), send_all_serialized(serialized, id, type, timeout, done) |
at once | held | done(0), and the observer; done(1) once written |
SyncClient::send(msg, timeout, lane) |
once written: the connection that wrote it | waits for one, timeout at most |
NotConnected when nothing wrote it with no connection live, else OperationTimedOut |
ThreadedClient::send(msg, timeout), send_all(msg, timeout) |
1 once written, the first copy for send_all; 0 at the timeout |
waits | 0 |
flush_all(timeout) |
true once everything sent before it was written; false once one was given up on, or at the timeout |
waits | false, and the observer for which |
shutdown(timeout), the servers' close(timeout) |
after writing what was sent before it, timeout at most, CLOSE_FLUSH_SECONDS by default |
waits, then drops | the observer, SHUT_DOWN |
schedule_one() and schedule_all() stay below queue(): a null
tracker, or 0, when no connection is live, nothing held. Written means
handed to the kernel on a live connection: only a reply says a message
arrived (semantics).
set_delivery() on SyncClient and ThreadedClient sets how the events
of a message type, or of one key of it, go: several in one message,
which the receiving library splits back into one message each, as if each
had come alone (batch); kept until their consumer's library confirms
them (confirm); or both; and how long a batch has (timeout).
Delivery says what goes in a batch, when it goes, what it
changes and what a confirmed send does.
multiplexer::SyncClient::Delivery delivery;
delivery.batch = true;
delivery.max_delay = 0.005; // seconds
client.set_delivery(multiplexer::types::METRIC_SAMPLE, delivery);
client.queue(sample); // in a batch, which goes once full or at the first call after 5 msset_delivery(type, delivery)sets it for every key oftype, each apart, and its messages without a key apart again;set_delivery(type, key, delivery)for one key, ahead of what is set for its type.Delivery, one struct namedSyncClient::DeliveryandThreadedClient::Deliveryalike, holdsbatch, false by default;max_records(DEFAULT_BATCH_MAX_RECORDS, 1000),max_bytes(DEFAULT_BATCH_MAX_BYTES, 1 MiB, measured asMAX_MESSAGE_SIZEis) andmax_delay, seconds as afloat, 0 by default: no delay, a batch going as soon as a connection has nothing left to write;confirm, false, andretry_after(DEFAULT_RETRY_AFTER, 5 s); andtimeout(DEFAULT_TIMEOUT, 10 s), a batch's from its first message, its sends' own not used, negative for no deadline (the timeout). With neitherbatchnorconfirmthe messages go as any send does, by that timeout.clear_delivery(type)ends what was set fortype, its keys' settings included, andclear_delivery(type, key)one key's, which then goes as its type says. Each lets every batch go first.- They throw
std::invalid_argumentfor a protocol message type, a key overMAX_KEY_SIZE, amax_recordsof 0 or overMAX_BATCH_RECORDS, amax_bytesof 0 or overMAX_MESSAGE_SIZE, a delay negative, overMAX_SECONDS, infinite or NaN, withconfirmaretry_afternot over 0, and atimeoutNaN, or 0 or more but not over the delay (over 0 withoutbatch);ThreadedClient's on the calling thread, the change made on the io thread right after, ahead of every send made after the call. - What goes by a setting is a plain event, no
to, noreferences, no delivery report asked for, nooverride_rrules, sent through no lane bySyncClient::queue()or byThreadedClient'ssend(msg),send(msg, LanePtr(), done)andsend_serialized(), whatever its own timeout. What goes byqueue_all()orsend_all(), a flushing send,schedule_*(), a lane or a connection, and a query's request go alone, after every batch. SyncClientruns its loop only inside its calls: with no delay its messages gather only while the socket is full, so give it one. A batch then goes when it fills or at the first call after its delay, andflush_all(),flush(tracker)andshutdown()let it go at once. Aqueue()that only joins a batch with a delay runs no loop; the one that fills it, or comes after a delay is over, does.ThreadedClientkeeps the delay on its io thread; with no delay its messages gather only while the program sends faster than the io thread writes them or the multiplexer is behind, so a delay of a few milliseconds batches for sure.- A server class sets it through its client: on
BaseMultiplexerServerthe protectedconn, or theSyncClientgiven to its constructor, whosesend_message()of a message that is no reply queues; onBaseThreadedMultiplexerServer,client(). - Each message keeps its tracker and its
done, which hear its batch's end; a batch given up on is counted indropped()and told to the drop observer once for each message in it. - A message that came in a batch has no raw frame until one is read:
receive_message()makes none, andread_raw_message(),receive_raw_message()and aThreadedClient'son_messageget the message serialized again, the same message, not always the same bytes (what the receiver sees).
Delivery::confirm makes a type's messages confirmed sends, in batches
with batch and one by one without: each message kept until its
consumer's library confirms it was processed, sent again until then, at
least once (confirmed sends).
multiplexer::ThreadedClient::Delivery delivery;
delivery.batch = true;
delivery.max_delay = 0.005;
delivery.confirm = true; // retry_after: 5 s, DEFAULT_RETRY_AFTER
delivery.timeout = 60; // a batch's, from its first message
producer.set_delivery(multiplexer::types::ORDER_PLACED, delivery);
producer.send(order, multiplexer::LanePtr(), [](unsigned int processed) {
// 1 once a consumer processed it; 0 once given up on, told to the drop observer
});- The producer: a message's
donehears 1, and its tracker leavesin_queue(), once its consumer confirmed it;flush_all()andflush(tracker)wait for that, andflush_all()returns false when one was given up on,NOT_CONFIRMEDat its batch's deadline orFAILED.Delivery::retry_after, seconds, more than 0, is how long a batch goes without any of its messages confirmed before what is left goes again. ASyncClienthears confirmations, and sends again, inside its calls. - The consumer: a message of a confirmed batch carries its number,
MultiplexerMessage::confirmation(), withsender()the producer.confirm(message, outcome),multiplexer::Confirmed::PROCESSED,FAILEDorREFUSED, andprocessed(message)tell its producer; nothing for a message with no number. AThreadedClientconfirms whaton_messagereturned from,FAILEDwhat it threw on, unlesson_messageconfirmed it itself orconfirm_after_on_message(false)was called before connecting, for a program that confirms its messages itself, from any thread.BaseMultiplexerServerconfirms ashandle_message()returns or throws,BaseThreadedMultiplexerServeras aRequestis let go of,FAILEDwhen its handler threw, andREFUSEDwhat it will not handle as it leaves. ASyncClientconfirms only what its program says. unconfirmed(),sent_again()andconfirmations_owed()say how many messages a client keeps unconfirmed, how many it sent again, and how many confirmations it owes: for tests.
Both clients count the messages the program sent that they gave up on,
dropped(), and tell an observer of each, set_drop_observer(observer)
with a BasicClient::DropObserver, void(std::uint64_t message_id, DropReason reason), each copy of a message sent to ALL once. The
reasons, multiplexer::DropReason in multiplexer/basic_client.h:
NO_ROOM: it waited for room on a full connection, its multiplexer not reading, past its timeout, or, sent with no time to wait, found none.NO_CONNECTION: it waited for a connection to come up past its timeout, or, sent with no time to wait, found none live.CONNECTION_LOST: its connection ended and nothing else could take it: a pinned lane's, or a copy sent toALL.SHUT_DOWN: the client shut down before it went.NOT_CONFIRMED: a confirmed send (delivery) no consumer confirmed by its batch's deadline, the setting's timeout from the batch's first message.FAILED: a confirmed send its consumer's handler failed on.
The drop is counted at once, in dropped(), and the observer hears of it
in the pass that runs the sends' callbacks, ahead of them, once the handler
the drop happened in is done: inside whichever SyncClient call runs the
loop next, and on a ThreadedClient's io thread, where it must return
quickly; the ThreadedClient lets go of it when its io thread ends. So the
observer may send, a retry or a note, or shut the client down; on a
SyncClient it must not receive or query, which would take the reply of
the call it runs in. A flush
returns once the observer heard of every drop before it. Each
drop is also logged, the first of a kind at once, naming the message's id
and type, and the rest counted in a line a second. The id and the type are
those the message's frame was made with, a whole message's own or those
given with its bytes: the library never parses a message for them. A message the library wrote is not dropped, whatever happens
to it next: written means the kernel's buffer (semantics).
A program that reads multiplexer addresses from its command line or its
configuration reads them with multiplexer::parse_endpoint() and writes
them with multiplexer::format_endpoint(), in
multiplexer/endpoint.h, Bazel target
@mx//multiplexer:endpoint: host:port, or [address]:port for an IPv6
address, the way mxcontrol and the Python
library take them. parse_endpoint(text) returns the (host, port) pair
connect() takes, an IPv6 address without its brackets, the host empty
for :1980 for the caller to fill in; parse_endpoint(text, 1980) lets
the port be left out. A malformed text throws std::invalid_argument
saying why: an IPv6 address out of brackets, since ::1:1980 could be
either, brackets around anything else, a port that is not a number from
0 to 65535. format_endpoint(host, port) puts an IPv6 address in
brackets.
Neither SyncClient nor BaseMultiplexerServer is thread-safe. One SyncClient
belongs to one thread, and a BaseMultiplexerServer runs on the thread that
calls serve_forever(); clients that share an io_service belong to one
thread together, since each call runs the others' handlers too. For
parallel clients create one per thread, as the integration test roles do in
tests/roles/cc/client.cc. In builds without
NDEBUG the library asserts this: a call from another thread fails with an
AssertionError naming the wrong-thread call.
Moves, not copies. SyncClient and ThreadedClient are moved, into a
container or out of a function, and never copied: a copy would share the
client, and the destructor of either would shut it down under the other.
A moved-from client is only destroyed, which does nothing; a SyncClient
that borrows an io_service keeps borrowing it where it moves.
Lifetimes. A client or a server ends when it is destroyed, or earlier
with shutdown() or close(): the destructors of Client and
ThreadedClient run shutdown(), writing what was sent first,
CLOSE_FLUSH_SECONDS at most, and waiting for the multiplexers' side of
the close, CLOSE_READ_SECONDS at most, and for a name lookup in
progress, on either client and so in either server class, a
BaseThreadedMultiplexerServer's runs close(), too late for a
subclass's handlers, so a subclass calls it in its own destructor, and a
BaseMultiplexerServer's client goes with it the same way. Nothing the
library holds keeps a C++ client alive: the callbacks it was given are
destroyed when its io thread ends. A process that ends without running
the destructor, by std::exit() with the client on the stack, _exit()
or a fatal signal, loses what the client had not written.
Fork. A client inherited by a forked child is an orphan there: its io
thread does not exist in the child, its locks may have been held at the
fork by threads that do not exist there either, and its sockets are shared
with the parent. Every call that would send, receive, connect,
disconnect, wait or take one of its locks throws UsedAfterFork, a
NotConnected, first, and so does every use of a Lane made before the
fork, or seeded with a ConnectionWrapper from before it, and every send
through such a wrapper, even by a client made in the child, as do the
getters of the connections' state, connections_count(),
has_incoming_messages() and routing_acknowledged(), which would answer
with the parent's;
orphaned() tells without throwing. shutdown() and the destructor close
the child's copies of the descriptors, once, with close(2): of every
socket the client had open at the fork, those of connections still closing
too, from a table the client keeps for this as its sockets open and close,
never the lists of connections its thread may have been changing at the
fork. They leak the rest on purpose, so nothing sends a goodbye or a
shutdown(2) on the parent's connection and asio never sees those
descriptor numbers again. Of
a BaseThreadedMultiplexerServer inherited the same way, close(),
connect(), serve_forever() and pending() throw, stop() only clears
working, and the destructor lets go of the parent's workers without
waiting for them. A pthread_atfork child handler in lib/fork.h bumps a
fork generation that every client and lane compares with the one it was
created under; one load per call, nothing when nobody forks. Create
clients after forking.
Exec. Every socket the clients and the multiplexer open is closed on
exec: a program the process runs, by system(), posix_spawn() or an
exec after a fork, holds none of the connections, which would otherwise
stay open after the client closed them or its process died, the
multiplexer routing to the silent copy until the heartbeats dropped it.
A client's socket is created so; the multiplexer's are marked right after
asio opens or accepts them, which a program embedding the server and
running a program from another thread at that instant could still hand
on.
What a forked child can still wait on. The child of a process with
threads has only the thread that forked; a lock another thread held at
that instant stays held unless something resets it in the child. glibc
resets its allocator and stdio; it does not reset its name resolver. A
child forked while a thread of the parent was inside getaddrinfo can
find a lock of the resolver held for good: every name lookup there waits
forever, and so, in some cases, does a thread's exit. The library looks
names up, on asio's resolver thread, whenever a client connects to a
multiplexer by name and again at every reconnect attempt, every 3 s, while
that multiplexer is unreachable; a child's new client connecting by name
would then never connect, and its destructor would wait for the resolver
thread for good. So a program that forks while clients are alive gives
every client addresses, the parent's too, or resolves the names once
before it makes any; an address in text, connect("10.0.0.1", 1980), is
never looked up. The same goes for the program's own threads that resolve
names, and the dynamic loader's locks are the same kind of hazard: for a
child that calls dlopen while a parent thread was loading a library,
and, with glibc before 2.35 or GCC's unwinder before 12, for a child that
throws, UsedAfterFork included, while a parent thread was throwing.
posix_spawn, or fork followed at once by exec, avoids all of it.
A fork inside a callback. A child forked inside one of the client's
callbacks is a copy of the thread running the loop, in the middle of it:
once the callback returns, the child runs the parent's loop on the
parent's sockets. Such a child calls exec or _exit before the callback
returns.
Backends on threads. BaseMultiplexerServer::stop() clears working
from any thread; serve_forever() notices within one poll. A
BaseThreadedMultiplexerServer is made of threads: its handlers run on
the workers, its Request may be answered from any thread, and
serve_forever() only polls.
mx::HttpServer (lib/http_server.h, @mx//lib:http_server in Bazel)
serves a program's /healthz, or any other path, over HTTP on a thread
of its own, as the multiplexer serves its own probes on it
(health):
#include "lib/http_server.h"
mx::HttpServer health(asio::ip::tcp::endpoint(asio::ip::address_v4::any(), 8080),
[](const std::string& path, std::string& body) {
body = "ok\n";
return path == "/healthz" ? 200 : 404;
});A handler of the form
mx::HttpTypedHandler, int(const std::string& path, std::string& body, std::string& content_type), may set the answer's content type, which
comes as text/plain: /metrics answers Prometheus with
text/plain; version=0.0.4; charset=utf-8. The constructor binds and
starts serving, and throws std::system_error when it cannot bind; port() is the port, the one the system picked for
port 0; stop(), or the destructor, closes the listening socket and every
connection and joins the thread, after a handler's call under way. The
handler gets the request target as sent, a query string included, and
returns the status; its body goes out as text/plain. It runs on the
server's thread, one request at a time, and one that throws, or returns a
status outside 100 to 599, is answered 500. The server takes GET alone,
405 for another method, one request a connection, a head of 8 KiB at
most, 431 beyond, and 16 connections at once, closing one beyond at once;
a connection has the constructor's timeout, 10 s, from its accept for
its request and the answer, so that a client that connects and says
nothing holds up nobody. Check a backend's
health ties the answer to a
backend's serve loop.
A C++ service is tested as it runs: its binary, as the data of a
py_test, against real multiplexers, through what it does over them.
The tools are those of @mx//multiplexer/testing, from Python:
Clusterfor the multiplexers;Processfor the binary, its log kept and its clean exit on SIGTERM checked;FakePeerfor the peers it talks to, andTestClientto ask it.
Test a service binary has the test, its
BUILD and what leaving the cluster checks.
examples/echo/echo_test.py tests the C++
backend so. Code that does not touch the multiplexer gets ordinary gtest
cc_tests.