CIRCT 24.0.0git
Loading...
Searching...
No Matches
RpcServer.cpp
Go to the documentation of this file.
1//===- RpcServer.cpp - Run a cosim server ---------------------------------===//
2//
3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4// See https://llvm.org/LICENSE.txt for license information.
5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6//
7//===----------------------------------------------------------------------===//
8//
9// Implementation of the cosim RPC server over the WebSocket + JSON protocol.
10// The wire protocol is documented in cosim-protocol.md.
11//
12//===----------------------------------------------------------------------===//
13
15#include "esi/Context.h"
16#include "esi/Utils.h"
17#include "esi/backends/RpcClient.h" // for ChannelDirection
18
19#include "RpcWire.h"
20
21#include <ixwebsocket/IXBase64.h>
22#include <ixwebsocket/IXNetSystem.h>
23#include <ixwebsocket/IXWebSocket.h>
24#include <ixwebsocket/IXWebSocketServer.h>
25#include <nlohmann/json.hpp>
26
27#include <algorithm>
28#include <atomic>
29#include <cassert>
30#include <condition_variable>
31#include <cstdint>
32#include <cstdio>
33#include <cstring>
34#include <deque>
35#include <format>
36#include <map>
37#include <mutex>
38#include <optional>
39#include <string>
40#include <thread>
41#include <unordered_map>
42#include <unordered_set>
43#include <utility>
44#include <vector>
45
46#ifdef _WIN32
47#include <winsock2.h>
48#include <ws2tcpip.h>
49#else
50#include <arpa/inet.h>
51#include <netinet/in.h>
52#include <sys/socket.h>
53#include <unistd.h>
54#endif
55
56using namespace esi;
57using namespace esi::cosim;
58using json = nlohmann::json;
59
60namespace {
61
62//===----------------------------------------------------------------------===//
63// Helpers
64//===----------------------------------------------------------------------===//
65
66/// Write the bound port number to a file so callers (typically esi-cosim) can
67/// discover it. Necessary when the OS picks the port, since the simulator's
68/// stdout/stderr buffering is undefined.
69static void writePortFile(uint16_t port) {
70 FILE *fd = fopen("cosim.cfg", "w");
71 if (!fd)
72 return;
73 fprintf(fd, "port: %u\n", static_cast<unsigned int>(port));
74 fclose(fd);
75}
76
77/// Pre-bind a temporary loopback socket to port 0 to discover an OS-assigned
78/// ephemeral port. IXWebSocket exposes the port a user passed in but does not
79/// query `getsockname` after binding, so for the "let the OS pick" path we have
80/// to find a free port ourselves and hand it to IX. SO_REUSEADDR + immediate
81/// close keep the race window minimal in practice.
82static int pickEphemeralPort() {
83#ifdef _WIN32
84 SOCKET fd = socket(AF_INET, SOCK_STREAM, 0);
85 if (fd == INVALID_SOCKET)
86 return -1;
87#else
88 int fd = socket(AF_INET, SOCK_STREAM, 0);
89 if (fd < 0)
90 return -1;
91#endif
92 int enable = 1;
93 setsockopt(fd, SOL_SOCKET, SO_REUSEADDR,
94 reinterpret_cast<const char *>(&enable), sizeof(enable));
95 sockaddr_in addr{};
96 addr.sin_family = AF_INET;
97 addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
98 addr.sin_port = 0;
99 if (bind(fd, reinterpret_cast<sockaddr *>(&addr), sizeof(addr)) < 0) {
100#ifdef _WIN32
101 closesocket(fd);
102#else
103 close(fd);
104#endif
105 return -1;
106 }
107 sockaddr_in bound{};
108 socklen_t len = sizeof(bound);
109 int rc = getsockname(fd, reinterpret_cast<sockaddr *>(&bound), &len);
110#ifdef _WIN32
111 closesocket(fd);
112#else
113 close(fd);
114#endif
115 if (rc < 0)
116 return -1;
117 return ntohs(bound.sin_port);
118}
119
120class RpcServerReadPort;
121class RpcServerWritePort;
122
123} // namespace
124
125//===----------------------------------------------------------------------===//
126// RpcServer::Impl - private implementation
127//===----------------------------------------------------------------------===//
128
130public:
131 Impl(Context &ctxt, int port);
132 ~Impl();
133
134 Context &getContext() { return ctxt; }
135
136 void setManifest(int esiVersion,
137 const std::vector<uint8_t> &compressedManifest);
138
139 ReadChannelPort &registerReadPort(const std::string &name,
140 const std::string &type);
141 WriteChannelPort &registerWritePort(const std::string &name,
142 const std::string &type);
144
145 void stop(uint32_t timeoutMS);
146 int getPort() const { return boundPort; }
147
148 // Dirty-set doorbell for the transport thread. Public so each port's
149 // TSQueue can pass `[&impl, id]{ impl.readyIds.markReady(id); }` as its
150 // push-notifier; also called directly from `handleSubscribe` to flush
151 // anything queued before the subscription arrived.
153
154 /// Log that a `size`-byte frame for channel `channelId` was dropped since
155 /// the simulator-side consumer refused it.
156 void logRefusedFrame(uint64_t channelId, size_t size);
157
158private:
159 // Reuse the public direction enum from the client side rather than
160 // duplicating it; both ends of the cosim transport agree on the same two
161 // values.
163 struct ChannelInfo {
164 uint64_t id;
165 std::string name;
166 std::string typeId;
168 RpcServerReadPort *readPort = nullptr;
169 RpcServerWritePort *writePort = nullptr;
170 };
171
172 // Locking rules. IXWebSocket may invoke the close callback (`onClose`)
173 // synchronously from inside a send on a dead socket, on whichever thread is
174 // sending. So:
175 // - Never send while holding `channelsMutex`, `sessionMutex` or a port's
176 // `connectMutex`; `onClose` takes the first two.
177 // - Sending while holding a session's `subscribedMutex` is allowed (it
178 // orders data frames against the unsubscribe-ack), so never wait for
179 // `subscribedMutex` while holding any other lock.
180 // - Senders hold a `shared_ptr` to the session, which owns the socket,
181 // so `onClose` never has to wait for them. Since `onClose` can't take
182 // `subscribedMutex` either, per-port client state is tagged with the
183 // session's generation, and `onClose` retires the generation so late
184 // updates from in-progress requests are ignored.
185 // - Within a port, only the write queue's and the doorbell's locks are
186 // taken while holding its `stateMutex`.
188 std::shared_ptr<ix::WebSocket> ws;
189 // The set of `to_client` channel ids the client subscribed to. Guarded by
190 // `subscribedMutex`.
191 std::mutex subscribedMutex;
192 std::unordered_set<uint64_t> subscribed;
193 // True once `hello` has been answered.
194 std::atomic<bool> helloDone = false;
195 // Set once the client has disconnected.
196 std::atomic<bool> closed = false;
197 // Distinguishes this session from earlier ones, so state belonging to an
198 // earlier client (e.g. credits for frames it sent) isn't given to this
199 // one.
200 uint64_t generation = 0;
201 };
202
204
205 // Manifest state. setManifest() flips manifestReady and broadcasts the CV;
206 // any in-flight `hello` handler blocks on this until it is set.
207 std::mutex manifestMutex;
208 std::condition_variable manifestReadyCV;
209 int esiVersion = -1;
210 std::vector<uint8_t> compressedManifest;
211 bool manifestReady = false;
212
213 // Channel table is keyed by name in the public API and by id on the wire.
214 // Ports are owned here; ChannelInfo holds non-owning pointers.
215 std::mutex channelsMutex;
216 std::map<std::string, std::unique_ptr<RpcServerReadPort>> readPorts;
217 std::map<std::string, std::unique_ptr<RpcServerWritePort>> writePorts;
218 std::unordered_map<uint64_t, ChannelInfo> channelById;
219 std::unordered_map<std::string, uint64_t> idByName;
220 uint64_t nextChannelId = 0;
221
222 // Session state. v4 of the protocol allows a single concurrent client.
223 std::mutex sessionMutex;
224 std::shared_ptr<ClientSession> session;
226
227 /// The active session, if any. The returned pointer stays valid after the
228 /// client disconnects; sends on it then just fail.
229 std::shared_ptr<ClientSession> getSession() {
230 std::lock_guard<std::mutex> lock(sessionMutex);
231 return session;
232 }
233 /// The active session if `ws` is its socket, else null. Requests are
234 /// handled through this, so one still being handled when its client
235 /// disconnects can't act on a replacement client's session.
236 std::shared_ptr<ClientSession> sessionFor(const ix::WebSocket &ws) {
237 std::shared_ptr<ClientSession> s = getSession();
238 if (s && s->ws.get() != &ws)
239 return nullptr;
240 return s;
241 }
242
243 // Transport thread; drains `readyIds` and dispatches per channel id.
244 std::thread transportThread;
245
246 // The IXWebSocket server.
247 std::unique_ptr<ix::WebSocketServer> server;
248 int boundPort = -1;
249
250 // Connection callbacks.
251 void onClientMessage(const std::weak_ptr<ix::WebSocket> &wsWeak,
252 ix::WebSocket &ws, const ix::WebSocketMessagePtr &msg);
253 void onOpen(const std::weak_ptr<ix::WebSocket> &wsWeak);
254 void onClose(ix::WebSocket &ws);
255 void handleBinaryFrame(uint64_t generation, const std::string &data);
256 void handleControlFrame(ix::WebSocket &ws, const std::string &text);
257 void handleEvent(uint64_t generation, const json &frame);
258 void handleHello(ix::WebSocket &ws, uint64_t requestId, const json &params);
259 void handleSubscribe(ix::WebSocket &ws, uint64_t requestId,
260 const json &params);
261 void handleUnsubscribe(ix::WebSocket &ws, uint64_t requestId,
262 const json &params);
263 /// Look up a channel. If it doesn't exist or doesn't have direction `dir`,
264 /// reply with an error and return nullopt.
265 std::optional<ChannelInfo> findChannel(ix::WebSocket &ws, uint64_t requestId,
266 uint64_t channelId,
267 ChannelDirection dir);
268 void sendResult(ix::WebSocket &ws, uint64_t requestId, const json &result);
269 void sendError(ix::WebSocket &ws, uint64_t requestId, const std::string &code,
270 const std::string &message);
271
272 void transportLoop();
273 /// Send a batched `consumed` event for any to-server messages the
274 /// simulation has consumed since the last call. Transport thread only.
275 void sendConsumedEvent(const std::unordered_set<uint64_t> &ids);
276 /// Forget all per-channel state belonging to client session `generation`,
277 /// which has disconnected.
278 void retireSession(uint64_t generation);
279
280 // Cross-thread fault propagation out of the IX network thread. See
281 // FaultStash docs in RpcWire.h.
283};
284
286
287//===----------------------------------------------------------------------===//
288// Port implementations
289//
290// Read and write ports are simple queues; the RPC server pushes/pops as
291// appropriate. These mirror the previous gRPC implementation: they are
292// transport-agnostic except that write port writes ring the sender doorbell.
293//===----------------------------------------------------------------------===//
294
295namespace {
296/// Read port for "to server" channels.
297///
298/// The cosim transport hands inbound frames synchronously from the IX
299/// network thread, which is shared across every channel on a connection.
300/// Any back-pressure on a per-port consumer would stall that thread and risk
301/// a cross-channel deadlock when an accelerator's flow control requires
302/// ordering across channels. So we *force* polling-mode `connect` to use an
303/// unbounded internal queue regardless of what the caller passes --
304/// `ReadChannelPort::pollingState` then acts as our buffer and
305/// `invokeCallback` is guaranteed non-blocking. The unbounded queue mirrors
306/// the existing `to_client` write queue and is acceptable for a cosim
307/// driver. The client bounds how many messages it sends ahead of the
308/// simulation using credits which this port returns via `markConsumed()`.
309///
310/// Frames which arrive while the port is not connected are parked here, in
311/// order, and handed over when it connects. They stay in flight until the
312/// simulation consumes them, so the client is backpressured rather than
313/// having its frames dropped.
314///
315/// Each frame's credit belongs to the client session which sent it. Frames
316/// outlive their session (in the base class's queue or parked here), so they
317/// are tracked by session, and credits for a session which has gone are
318/// dropped rather than handed to the next client: a new client starts with
319/// nothing in flight.
320class RpcServerReadPort : public ReadChannelPort {
321public:
322 RpcServerReadPort(Type *type, Impl &impl, uint64_t channelId)
323 : ReadChannelPort(type), impl(impl), channelId(channelId) {}
324
325 /// Polling-mode connect: force the internal queue to be unbounded
326 /// (`bufferSize == 0`) regardless of what the caller asked for.
327 void connect(const ConnectOptions &options = {}) override {
328 ConnectOptions forced = options;
329 forced.bufferSize = 0;
331 markConnected();
332 }
333 void connect(ReadCallback callback,
334 const ConnectOptions &options = {}) override {
335 ReadChannelPort::connect(std::move(callback), options);
336 markConnected();
337 }
339
340 /// Client session `generation` closed: its credits are dropped.
341 void clientDisconnected(uint64_t generation) {
342 std::lock_guard<std::recursive_mutex> lock(creditMutex);
343 retiredGeneration = (std::max)(retiredGeneration, generation);
344 if (unreportedGeneration <= generation)
345 unreported = 0;
346 }
347
348 void disconnect() override {
349 {
350 std::lock_guard<std::mutex> lock(connectMutex);
351 connected = false;
352 }
353 // Held across the base class's disconnect so no frame is handed over and
354 // counted in `held` after its queue is torn down (see `handOver()`).
355 std::lock_guard<std::recursive_mutex> lock(creditMutex);
357 // The base class discarded whatever it had queued for the simulation.
358 // Return those frames' credits, like the server stops counting a client's
359 // unconsumed frames on `unsubscribe`.
360 for (auto [generation, count] : held)
361 credit(generation, count);
362 held.clear();
363 }
364
365 /// Hand one inbound frame from client session `generation` to the
366 /// simulation, or park it if the port isn't connected (or earlier frames
367 /// are still parked). Returns `false` only if a connected callback-mode
368 /// consumer refused the frame, in which case the caller should drop it and
369 /// call `dropped()`.
370 bool deliver(uint64_t generation, const MessageData &data) {
371 {
372 std::lock_guard<std::mutex> lock(connectMutex);
373 if (!connected || !parked.empty()) {
374 parked.emplace_back(generation, data);
375 return true;
376 }
377 }
378 if (handOver(generation, data))
379 return true;
380 // Lost a race with `disconnect()`: park it at the front, since it arrived
381 // before anything parked since.
382 std::lock_guard<std::mutex> lock(connectMutex);
383 if (connected)
384 return false;
385 parked.emplace_front(generation, data);
386 return true;
387 }
388
389 /// A frame from client session `generation` was dropped rather than handed
390 /// to the simulation. Return its credit.
391 void dropped(uint64_t generation) {
392 std::lock_guard<std::recursive_mutex> lock(creditMutex);
393 credit(generation, 1);
394 }
395
396 /// The simulation consumed the oldest frame handed to it. Return its credit
397 /// to the session which sent it.
398 void markConsumed() {
399 std::lock_guard<std::recursive_mutex> lock(creditMutex);
400 // Empty if its credit was already returned by `disconnect()`.
401 if (held.empty())
402 return;
403 auto &[generation, count] = held.front();
404 credit(generation, 1);
405 if (--count == 0)
406 held.pop_front();
407 }
408
409 /// Take (and zero) the credits not yet returned to client session
410 /// `generation`.
411 uint64_t takeConsumed(uint64_t generation) {
412 std::lock_guard<std::recursive_mutex> lock(creditMutex);
413 if (generation != unreportedGeneration)
414 return 0;
415 return std::exchange(unreported, 0);
416 }
417
418private:
419 /// Pass one frame from session `generation` to the base class's callback /
420 /// polling queue, counting it in `held` if that worked.
421 bool handOver(uint64_t generation, const MessageData &data) {
422 std::unique_ptr<SegmentedMessageData> msg =
423 std::make_unique<MessageData>(data);
424 // Held throughout, so `disconnect()` sees the frame either counted in
425 // `held` or not handed over at all. Counted first, since a callback-mode
426 // consumer may report it consumed from within the callback.
427 std::lock_guard<std::recursive_mutex> lock(creditMutex);
428 if (!held.empty() && held.back().first == generation)
429 ++held.back().second;
430 else
431 held.emplace_back(generation, 1);
432 bool ok = false;
433 try {
434 ok = invokeCallback(msg);
435 } catch (...) {
436 unhold();
437 throw;
438 }
439 if (!ok)
440 unhold();
441 return ok;
442 }
443
444 /// Undo the count of the frame `handOver()` failed to hand over.
445 void unhold() {
446 if (held.empty())
447 return;
448 if (--held.back().second == 0)
449 held.pop_back();
450 }
451
452 /// Return `count` credits to session `generation`, unless it has gone.
453 /// Caller holds `creditMutex`.
454 void credit(uint64_t generation, uint64_t count) {
455 if (generation <= retiredGeneration || generation < unreportedGeneration)
456 return;
457 if (generation != unreportedGeneration) {
458 unreportedGeneration = generation;
459 unreported = 0;
460 }
461 unreported += count;
462 impl.readyIds.markReady(channelId);
463 }
464
465 /// Called after the base class has finished connecting (so inbound frames
466 /// will be accepted). Flushes parked frames in order.
467 void markConnected() {
468 std::unique_lock<std::mutex> lock(connectMutex);
469 connected = true;
470 // `deliver()` keeps appending to `parked` (rather than delivering
471 // directly) until it's empty, which preserves ordering while we flush
472 // without holding the lock across the callback.
473 while (!parked.empty()) {
474 auto [generation, front] = parked.front();
475 lock.unlock();
476 bool ok = handOver(generation, front);
477 lock.lock();
478 if (!ok) {
479 if (!connected)
480 break; // Disconnected again; leave it parked.
481 // Refused by a callback-mode consumer: drop it, like a frame which
482 // arrives while connected (see `deliver()`), rather than leave it
483 // blocking everything after it.
484 impl.logRefusedFrame(channelId, front.getSize());
485 dropped(generation);
486 }
487 parked.pop_front();
488 }
489 }
490
491 Impl &impl;
492 uint64_t channelId;
493
494 // Guards the credit state below. Also held while a frame is handed over
495 // (see `handOver()`), and recursive since a callback-mode consumer may
496 // report a frame consumed from within the callback.
497 std::recursive_mutex creditMutex;
498 // Frames handed to the base class and not yet consumed, oldest first, as
499 // runs of (client session generation, count).
500 std::deque<std::pair<uint64_t, uint64_t>> held;
501 // Credits not yet returned to session `unreportedGeneration`.
502 uint64_t unreported = 0;
503 uint64_t unreportedGeneration = 0;
504 // Sessions up to this one have closed; their credits are dropped.
505 uint64_t retiredGeneration = 0;
506
507 std::mutex connectMutex;
508 bool connected = false;
509 // Frames received while not connected, oldest first, with the generation
510 // of the session which sent them.
511 std::deque<std::pair<uint64_t, MessageData>> parked;
512};
513
514/// Write queue for "to client" channels. Writes go into the per-port TSQueue;
515/// the queue's notifier hook rings the transport doorbell so the transport
516/// thread wakes up and drains across the WebSocket. DPI threads must never
517/// block on I/O, so this path is strictly non-blocking.
518///
519/// Flow control: the port's in-flight count is the number of messages queued
520/// here plus the number sent to the client but not yet reported consumed.
521/// `tryWrite()` refuses new messages while that count is at the in-flight
522/// limit. The limit is set by the client on `subscribe` (0 means unbounded)
523/// and reverts to the default when there is no subscriber.
524class RpcServerWritePort : public WriteChannelPort {
525public:
527
528 RpcServerWritePort(Type *type, Impl &impl, uint64_t channelId)
529 : WriteChannelPort(type), channelId(channelId), impl(impl),
530 writeQueue([&impl, channelId] { impl.readyIds.markReady(channelId); }) {
531 }
532
533 uint64_t getChannelId() const { return channelId; }
534 uint64_t channelId;
536
537 /// Pop the next queued message for sending to client session
538 /// `generation`, accounting it as sent but unacknowledged. Returns nullopt
539 /// once that session is retired, so nothing is sent (and counted) for a
540 /// client which has gone away.
541 std::optional<MessageData> popForSend(uint64_t generation) {
542 std::lock_guard<std::mutex> lock(stateMutex);
543 if (generation <= retiredGeneration || generation != ownerGeneration)
544 return std::nullopt;
545 std::optional<MessageData> msg = writeQueue.pop();
546 if (!msg)
547 return std::nullopt;
548 // Increment `unacked` before decrementing `queued` so a concurrent
549 // `hasSpace()` never under-counts the in-flight total.
550 unacked.fetch_add(1);
551 queued.fetch_sub(1);
552 return msg;
553 }
554
555 /// Client session `generation` reported `count` messages consumed. Ignored
556 /// unless it owns the flow-control state (see `subscribed()`).
557 void ack(uint64_t count, uint64_t generation) {
558 std::lock_guard<std::mutex> lock(stateMutex);
559 if (generation <= retiredGeneration || generation != ownerGeneration)
560 return;
561 // Clamp at zero, just in case.
562 unacked.store(unacked.load() - (std::min)(unacked.load(), count));
563 spaceCv.notify_all();
564 }
565
566 /// Client session `generation` subscribed with in-flight limit `limit`. It
567 /// now owns the flow-control state; anything an earlier session had in
568 /// flight is gone. Ignored if that session is already retired.
569 void subscribed(uint64_t limit, uint64_t generation) {
570 std::lock_guard<std::mutex> lock(stateMutex);
571 if (generation <= retiredGeneration)
572 return;
573 if (generation != ownerGeneration) {
574 ownerGeneration = generation;
575 unacked.store(0);
576 }
577 maxInFlight.store(limit);
578 spaceCv.notify_all();
579 }
580
581 /// Client session `generation` unsubscribed: messages it had not
582 /// acknowledged are gone, so stop counting them.
583 void unsubscribed(uint64_t generation) {
584 std::lock_guard<std::mutex> lock(stateMutex);
585 if (generation == ownerGeneration)
586 resetFlowControlLocked();
587 }
588
589 /// Client session `generation` closed: forget its flow-control state (if a
590 /// later session hasn't already taken it over) and ignore anything else
591 /// which arrives on its behalf.
592 void retireClient(uint64_t generation) {
593 std::lock_guard<std::mutex> lock(stateMutex);
594 retiredGeneration = (std::max)(retiredGeneration, generation);
595 if (ownerGeneration <= generation)
596 resetFlowControlLocked();
597 }
598
599 /// Wakes any `write()` blocked at the in-flight limit, which then throws.
600 void disconnect() override {
601 {
602 std::lock_guard<std::mutex> lock(stateMutex);
603 disconnected = true;
604 }
605 spaceCv.notify_all();
607 }
608
609protected:
610 void connectImpl(const ConnectOptions &) override {
611 std::lock_guard<std::mutex> lock(stateMutex);
612 disconnected = false;
613 }
614
615 /// Blocks while the in-flight limit is reached. The simulation must not
616 /// call this, since the client may need it to make progress before it
617 /// consumes anything; it should use `tryWrite()`. Throws if the port is
618 /// disconnected meanwhile.
619 void writeImpl(const MessageData &data) override {
620 std::unique_lock<std::mutex> lock(stateMutex);
621 spaceCv.wait(lock, [&] { return disconnected || hasSpace(); });
622 if (disconnected)
623 throw std::runtime_error("Channel id " + std::to_string(channelId) +
624 " disconnected while writing");
625 pushLocked(data);
626 }
627 bool tryWriteImpl(const MessageData &data) override {
628 std::lock_guard<std::mutex> lock(stateMutex);
629 if (!hasSpace())
630 return false;
631 pushLocked(data);
632 return true;
633 }
634
635private:
636 void pushLocked(const MessageData &data) {
637 queued.fetch_add(1);
638 writeQueue.push(data);
639 }
640
641 void resetFlowControlLocked() {
642 unacked.store(0);
643 maxInFlight.store(RpcClient::DefaultToClientMaxInFlight);
644 spaceCv.notify_all();
645 }
646
647 bool hasSpace() const {
648 uint64_t limit = maxInFlight.load();
649 return limit == 0 || queued.load() + unacked.load() < limit;
650 }
651
652 Impl &impl;
653 std::atomic<uint64_t> queued{0};
654 std::atomic<uint64_t> unacked{0};
655 std::atomic<uint64_t> maxInFlight{RpcClient::DefaultToClientMaxInFlight};
656 // Guards the generations and `disconnected`, and orders flow-control
657 // updates against them. Only the write queue's and doorbell's locks are
658 // taken while holding it.
659 std::mutex stateMutex;
660 // Notified whenever room may have appeared, or the port disconnected.
661 std::condition_variable spaceCv;
662 bool disconnected = false;
663 uint64_t retiredGeneration = 0;
664 // The session which subscribed last, and so owns the flow-control state.
665 uint64_t ownerGeneration = 0;
666};
667} // namespace
668
669//===----------------------------------------------------------------------===//
670// Impl - server lifecycle
671//===----------------------------------------------------------------------===//
672
674 // On Windows, `ix::initNetSystem()` calls `WSAStartup` and returns false if
675 // that fails. On other platforms it's a no-op that always returns true.
676 if (!ix::initNetSystem())
677 throw std::runtime_error(
678 "RpcServer: ix::initNetSystem() failed (WSAStartup)");
679
680 // Resolve port 0 / negative request to an OS-assigned ephemeral port,
681 // since IXWebSocket does not expose the bound port after the fact.
682 int requestedPort = port;
683 if (requestedPort <= 0) {
684 requestedPort = pickEphemeralPort();
685 if (requestedPort <= 0)
686 throw std::runtime_error(
687 "RpcServer: failed to obtain an ephemeral TCP port");
688 }
689
690 const std::string host = "127.0.0.1";
691 server = std::make_unique<ix::WebSocketServer>(requestedPort, host);
692 server->disablePerMessageDeflate();
693
694 // Use the connection callback (rather than the simpler client-message one)
695 // because it hands us an owning reference to the socket, which the session
696 // holds so senders on other threads can't outlive it.
697 server->setOnConnectionCallback(
698 [this](std::weak_ptr<ix::WebSocket> wsWeak,
699 std::shared_ptr<ix::ConnectionState> /*state*/) {
700 std::shared_ptr<ix::WebSocket> ws = wsWeak.lock();
701 if (!ws)
702 return;
703 ix::WebSocket *raw = ws.get();
704 ws->setOnMessageCallback(
705 [this, wsWeak, raw](const ix::WebSocketMessagePtr &msg) {
706 onClientMessage(wsWeak, *raw, msg);
707 });
708 });
709
710 auto res = server->listen();
711 if (!res.first)
712 throw std::runtime_error("RpcServer: listen failed: " + res.second);
713
714 server->start();
715 boundPort = requestedPort;
716 writePortFile(static_cast<uint16_t>(boundPort));
717 ctxt.getLogger().info("cosim", std::format("Server listening on {}:{}", host,
718 static_cast<unsigned>(boundPort)));
719
720 // Start the always-on transport thread that drains every port's queue. It
721 // lives the entire lifetime of the server and sleeps on `readyIds`'s
722 // internal CV when there is nothing to do regardless of whether a client
723 // is connected.
724 transportThread = std::thread([this] { transportLoop(); });
725}
726
728 if (server) {
729 // A pending fault from the network thread is interesting, but throwing
730 // out of a destructor is worse than swallowing it. The user can still
731 // observe the fault by calling stop() explicitly before letting the
732 // server destruct; if they didn't, log and continue.
733 try {
734 stop(0);
735 } catch (const std::exception &e) {
736 ctxt.getLogger().error(
737 "cosim",
738 std::string("Suppressed exception during ~RpcServer::Impl: ") +
739 e.what());
740 } catch (...) {
741 ctxt.getLogger().error(
742 "cosim", "Suppressed non-std::exception during ~RpcServer::Impl");
743 }
744 }
745}
746
748 const std::vector<uint8_t> &compressedManifest) {
749 faultStash.check();
750 {
751 std::lock_guard<std::mutex> lock(manifestMutex);
752 this->esiVersion = esiVersion;
753 this->compressedManifest = compressedManifest;
754 manifestReady = true;
755 }
756 manifestReadyCV.notify_all();
757}
758
760 const std::string &type) {
761 faultStash.check();
762 std::lock_guard<std::mutex> lock(channelsMutex);
763 uint64_t id = nextChannelId++;
764 auto port = std::make_unique<RpcServerReadPort>(new Type(type), *this, id);
765 RpcServerReadPort *raw = port.get();
766 readPorts.emplace(name, std::move(port));
767
768 ChannelInfo info{id, name, type, ChannelDirection::ToServer, raw, nullptr};
769 channelById.emplace(id, std::move(info));
770 idByName.emplace(name, id);
771 return *raw;
772}
773
775 // Only ports created by `registerReadPort` are handed out, so this cast is
776 // safe for any well-behaved caller.
777 static_cast<RpcServerReadPort &>(port).markConsumed();
778}
779
781 const std::string &type) {
782 faultStash.check();
783 std::lock_guard<std::mutex> lock(channelsMutex);
784 uint64_t id = nextChannelId++;
785 auto port = std::make_unique<RpcServerWritePort>(new Type(type), *this, id);
786 RpcServerWritePort *raw = port.get();
787 writePorts.emplace(name, std::move(port));
788
789 ChannelInfo info{id, name, type, ChannelDirection::ToClient, nullptr, raw};
790 channelById.emplace(id, std::move(info));
791 idByName.emplace(name, id);
792 return *raw;
793}
794
795void Impl::stop(uint32_t /*timeoutMS*/) {
796 // Disconnect ports first so any in-flight DPI writes see a closed channel.
797 {
798 std::lock_guard<std::mutex> lock(channelsMutex);
799 for (auto &[name, port] : readPorts)
800 port->disconnect();
801 for (auto &[name, port] : writePorts)
802 port->disconnect();
803 }
804
805 // Retire the transport thread.
806 if (transportThread.joinable()) {
807 readyIds.requestShutdown();
808 transportThread.join();
809 // NB: there's no explicit "clear readyIds" step: each port's queue
810 // contents and the corresponding dirty markers belong to the ports,
811 // not to the transport thread, and the server doesn't restart in-place.
812 }
813
814 if (server) {
815 server->stop();
816 server.reset();
817 }
818
819 std::shared_ptr<ClientSession> old;
820 {
821 std::lock_guard<std::mutex> lock(sessionMutex);
822 old.swap(session);
823 }
824
825 // Surface any fault the IX network thread caught while we were running.
826 // Done last so the rest of the shutdown sequence completes regardless.
827 faultStash.check();
828}
829
830//===----------------------------------------------------------------------===//
831// Impl - WebSocket message dispatch
832//===----------------------------------------------------------------------===//
833
834void Impl::onClientMessage(const std::weak_ptr<ix::WebSocket> &wsWeak,
835 ix::WebSocket &ws,
836 const ix::WebSocketMessagePtr &msg) {
837 switch (msg->type) {
838 case ix::WebSocketMessageType::Open:
839 onOpen(wsWeak);
840 return;
841 case ix::WebSocketMessageType::Close:
842 case ix::WebSocketMessageType::Error: {
843 ctxt.getLogger().debug(
844 "cosim", std::format("Client disconnected: {}",
845 msg->type == ix::WebSocketMessageType::Error
846 ? msg->errorInfo.reason
847 : msg->closeInfo.reason));
848 onClose(ws);
849 return;
850 }
851 case ix::WebSocketMessageType::Message:
852 // An exception escaping this callback would kill IX's network thread.
853 // Stash the first one so the next public RpcServer method rethrows it
854 // on the user's thread.
855 try {
856 if (msg->binary) {
857 // Frames from a rejected additional client are ignored.
858 if (std::shared_ptr<ClientSession> s = sessionFor(ws))
859 handleBinaryFrame(s->generation, msg->str);
860 } else {
861 handleControlFrame(ws, msg->str);
862 }
863 } catch (...) {
864 faultStash.record(std::current_exception());
865 }
866 return;
867 default:
868 return;
869 }
870}
871
872void Impl::onOpen(const std::weak_ptr<ix::WebSocket> &wsWeak) {
873 std::shared_ptr<ix::WebSocket> ws = wsWeak.lock();
874 if (!ws)
875 return;
876 {
877 std::lock_guard<std::mutex> lock(sessionMutex);
878 if (!session) {
879 session = std::make_shared<ClientSession>();
880 session->ws = ws;
881 session->generation = nextSessionGeneration++;
882 ctxt.getLogger().debug("cosim", "Client connected");
883 return;
884 }
885 }
886 // Single-client model: a session is already active. Send an unsolicited
887 // JSON error frame so the new client gets an actionable, application-level
888 // reason, then close with 1013 ("Try Again Later") rather than 1011
889 // ("Internal Error") -- the latter falsely implies a server-side bug.
890 ctxt.getLogger().warning(
891 "cosim", "Rejecting additional client; one already connected");
892 json err;
893 err["type"] = "error";
894 err["error"] = {{"code", "server_busy"},
895 {"message", "cosim server allows only one client at a "
896 "time; another client is already connected"}};
897 ws->sendUtf8Text(err.dump());
898 // 1013 = Try Again Later (RFC 6455 §7.4).
899 ws->close(1013, "cosim server busy: another client is already connected");
900}
901
902void Impl::onClose(ix::WebSocket &ws) {
903 std::shared_ptr<ClientSession> old;
904 {
905 std::lock_guard<std::mutex> lock(sessionMutex);
906 // A rejected second client closing must not tear down the active one.
907 if (!session || session->ws.get() != &ws)
908 return;
909 old.swap(session);
910 }
911 old->closed.store(true);
912 // Must not wait for in-progress sends: this may be running inside one (see
913 // the locking rules on ClientSession). Senders hold their own reference.
914 // Requests from this session still being handled are fenced off by
915 // generation instead.
916 retireSession(old->generation);
917}
918
919void Impl::retireSession(uint64_t generation) {
920 std::lock_guard<std::mutex> lock(channelsMutex);
921 for (auto &[name, port] : writePorts)
922 port->retireClient(generation);
923 for (auto &[name, port] : readPorts)
924 port->clientDisconnected(generation);
925}
926
927void Impl::handleBinaryFrame(uint64_t generation, const std::string &data) {
928 uint64_t channelId;
929 const uint8_t *payloadBytes;
930 size_t payloadSize;
931 if (!parseDataFrame(data, channelId, payloadBytes, payloadSize)) {
932 ctxt.getLogger().error("cosim",
933 "Received binary frame shorter than 8-byte header");
934 return;
935 }
936
937 RpcServerReadPort *port = nullptr;
938 {
939 std::lock_guard<std::mutex> lock(channelsMutex);
940 auto it = channelById.find(channelId);
941 if (it == channelById.end() ||
942 it->second.direction != ChannelDirection::ToServer)
943 port = nullptr;
944 else
945 port = it->second.readPort;
946 }
947 if (!port) {
948 ctxt.getLogger().error(
949 "cosim", std::format("Binary frame for unknown to-server channel id {}",
950 channelId));
951 return;
952 }
953
954 MessageData payload(payloadBytes, payloadSize);
955 // RpcServerReadPort always has an unbounded internal queue so `deliver` is
956 // non-blocking: it enqueues into `ReadChannelPort`'s internal polling buffer
957 // (or parks the frame until the port connects) and returns. A `false`
958 // return means a callback-mode consumer refused it; drop the frame but
959 // still return the credit so the client's in-flight count doesn't leak.
960 if (!port->deliver(generation, payload)) {
961 logRefusedFrame(channelId, payload.getSize());
962 port->dropped(generation);
963 }
964}
965
966void Impl::logRefusedFrame(uint64_t channelId, size_t size) {
967 ctxt.getLogger().warning(
968 "cosim", std::format("Dropped {} bytes for channel id {}: refused by "
969 "the simulator-side consumer",
970 size, channelId));
971}
972
973void Impl::handleControlFrame(ix::WebSocket &ws, const std::string &text) {
974 json req;
975 try {
976 req = json::parse(text);
977 } catch (const std::exception &e) {
978 ctxt.getLogger().error(
979 "cosim", std::format("Failed to parse control frame: {}", e.what()));
980 sendError(ws, 0, "protocol_error",
981 std::string("Failed to parse JSON: ") + e.what());
982 faultStash.record(std::current_exception());
983 return;
984 }
985
986 auto typeIt = req.find("type");
987 if (typeIt != req.end() && typeIt->is_string() &&
988 typeIt->get<std::string>() == "event") {
989 // Events from a rejected additional client are ignored.
990 if (std::shared_ptr<ClientSession> s = sessionFor(ws))
991 handleEvent(s->generation, req);
992 return;
993 }
994 if (typeIt == req.end() || !typeIt->is_string() ||
995 typeIt->get<std::string>() != "request") {
996 sendError(ws, 0, "protocol_error",
997 "Control frame missing \"type\":\"request\"");
998 return;
999 }
1000 uint64_t requestId = 0;
1001 if (auto idIt = req.find("request_id"); idIt != req.end()) {
1002 // Accept only unsigned integers.
1003 if (!idIt->is_number_unsigned()) {
1004 sendError(ws, 0, "protocol_error",
1005 "\"request_id\" must be an unsigned integer");
1006 return;
1007 }
1008 try {
1009 requestId = idIt->get<uint64_t>();
1010 } catch (const std::exception &e) {
1011 sendError(ws, 0, "protocol_error",
1012 std::string("Invalid \"request_id\": ") + e.what());
1013 return;
1014 }
1015 }
1016 auto methodIt = req.find("method");
1017 if (methodIt == req.end() || !methodIt->is_string()) {
1018 sendError(ws, requestId, "protocol_error", "Missing \"method\"");
1019 return;
1020 }
1021 std::string method = methodIt->get<std::string>();
1022 json params = req.value("params", json::object());
1023
1024 if (method == "hello")
1025 handleHello(ws, requestId, params);
1026 else if (method == "subscribe")
1027 handleSubscribe(ws, requestId, params);
1028 else if (method == "unsubscribe")
1029 handleUnsubscribe(ws, requestId, params);
1030 else
1031 sendError(ws, requestId, "protocol_error", "Unknown method: " + method);
1032}
1033
1034//===----------------------------------------------------------------------===//
1035// Impl - control methods
1036//===----------------------------------------------------------------------===//
1037
1038/// Parse the `channels` list of a `consumed` event into (channel_id, count)
1039/// pairs. Returns false (with `err` set) on a malformed event.
1040static bool parseConsumedEvent(const json &frame,
1041 std::vector<std::pair<uint64_t, uint64_t>> &out,
1042 std::string &err) {
1043 auto paramsIt = frame.find("params");
1044 if (paramsIt == frame.end() || !paramsIt->is_object()) {
1045 err = "missing \"params\" object";
1046 return false;
1047 }
1048 auto chIt = paramsIt->find("channels");
1049 if (chIt == paramsIt->end() || !chIt->is_array()) {
1050 err = "missing \"channels\" array";
1051 return false;
1052 }
1053 for (const json &c : *chIt) {
1054 auto idIt = c.find("channel_id");
1055 auto countIt = c.find("count");
1056 if (idIt == c.end() || !idIt->is_number_unsigned() || countIt == c.end() ||
1057 !countIt->is_number_unsigned()) {
1058 err = "entries need unsigned \"channel_id\" and \"count\"";
1059 return false;
1060 }
1061 out.emplace_back(idIt->get<uint64_t>(), countIt->get<uint64_t>());
1062 }
1063 return true;
1064}
1065
1066void Impl::handleEvent(uint64_t generation, const json &frame) {
1067 std::string event = frame.value("event", std::string());
1068 if (event != "consumed") {
1069 // Events carry no request_id, so there's nobody to reply to. Ignore
1070 // unknown events per the protocol's additive-evolution rule.
1071 ctxt.getLogger().debug("cosim",
1072 "Ignoring unknown client event \"" + event + "\"");
1073 return;
1074 }
1075 std::vector<std::pair<uint64_t, uint64_t>> acks;
1076 std::string err;
1077 if (!parseConsumedEvent(frame, acks, err)) {
1078 ctxt.getLogger().warning("cosim", "Malformed consumed event: " + err);
1079 return;
1080 }
1081 std::lock_guard<std::mutex> lock(channelsMutex);
1082 for (auto [id, count] : acks) {
1083 auto it = channelById.find(id);
1084 if (it == channelById.end() || !it->second.writePort) {
1085 ctxt.getLogger().warning(
1086 "cosim",
1087 std::format("consumed event for unknown to-client channel id {}",
1088 id));
1089 continue;
1090 }
1091 it->second.writePort->ack(count, generation);
1092 }
1093}
1094
1095void Impl::handleHello(ix::WebSocket &ws, uint64_t requestId,
1096 const json &params) {
1097 auto verIt = params.find("client_protocol_version");
1098 if (verIt == params.end() || !verIt->is_number_unsigned() ||
1099 verIt->get<uint64_t>() != kProtocolVersion) {
1100 std::string got = verIt == params.end() ? "none" : verIt->dump();
1101 sendError(ws, requestId, "unsupported_version",
1102 std::format("Server speaks cosim protocol v{}; client requested "
1103 "{}. Update the ESI runtime on both sides.",
1104 kProtocolVersion, got));
1105 return;
1106 }
1107
1108 // Block until the manifest has been set. This replaces the gRPC-era poll
1109 // loop on the client side.
1110 {
1111 std::unique_lock<std::mutex> lock(manifestMutex);
1112 manifestReadyCV.wait(lock, [&] { return manifestReady; });
1113 }
1114
1115 json result;
1116 result["protocol_version"] = kProtocolVersion;
1117 {
1118 std::lock_guard<std::mutex> lock(manifestMutex);
1119 result["esi_version"] = esiVersion;
1120 result["compressed_manifest_b64"] = macaron::Base64::Encode(
1121 std::string(reinterpret_cast<const char *>(compressedManifest.data()),
1122 compressedManifest.size()));
1123 }
1124
1125 json channelsJson = json::array();
1126 {
1127 std::lock_guard<std::mutex> lock(channelsMutex);
1128 // Emit in id order for deterministic output.
1129 for (uint64_t i = 0; i < nextChannelId; ++i) {
1130 auto it = channelById.find(i);
1131 if (it == channelById.end())
1132 continue;
1133 const ChannelInfo &info = it->second;
1134 json c;
1135 c["channel_id"] = info.id;
1136 c["name"] = info.name;
1137 c["type"] = info.typeId;
1138 c["direction"] = info.direction == ChannelDirection::ToServer
1139 ? "to_server"
1140 : "to_client";
1141 channelsJson.push_back(std::move(c));
1142 }
1143 }
1144 result["channels"] = std::move(channelsJson);
1145
1146 if (std::shared_ptr<ClientSession> s = sessionFor(ws))
1147 s->helloDone = true;
1148
1149 sendResult(ws, requestId, result);
1150}
1151
1152void Impl::handleSubscribe(ix::WebSocket &ws, uint64_t requestId,
1153 const json &params) {
1154 auto chIdIt = params.find("channel_id");
1155 if (chIdIt == params.end() || !chIdIt->is_number_unsigned()) {
1156 sendError(ws, requestId, "protocol_error",
1157 "subscribe requires unsigned \"channel_id\"");
1158 return;
1159 }
1160 uint64_t channelId = chIdIt->get<uint64_t>();
1162 if (auto limitIt = params.find("max_in_flight"); limitIt != params.end()) {
1163 if (!limitIt->is_number_unsigned()) {
1164 sendError(ws, requestId, "protocol_error",
1165 "\"max_in_flight\" must be an unsigned integer");
1166 return;
1167 }
1168 maxInFlight = limitIt->get<uint64_t>();
1169 }
1170
1171 std::optional<ChannelInfo> info =
1172 findChannel(ws, requestId, channelId, ChannelDirection::ToClient);
1173 if (!info)
1174 return;
1175 std::shared_ptr<ClientSession> s = sessionFor(ws);
1176 if (!s) {
1177 sendError(ws, requestId, "internal", "No active session");
1178 return;
1179 }
1180 {
1181 std::lock_guard<std::mutex> subLock(s->subscribedMutex);
1182 if (s->subscribed.count(channelId)) {
1183 sendError(ws, requestId, "protocol_error",
1184 std::format("Channel id {} is already subscribed", channelId));
1185 return;
1186 }
1187 s->subscribed.insert(channelId);
1188 info->writePort->subscribed(maxInFlight, s->generation);
1189 // Send the subscribe-ack BEFORE waking the transport thread. IXWebSocket
1190 // queues sends in FIFO order on a given connection, so as long as the ack
1191 // is enqueued first, any data frames the transport thread emits next will
1192 // arrive after it. Holding `subscribedMutex` keeps the transport thread
1193 // from sending on this channel before the ack, too.
1194 sendResult(ws, requestId, json::object());
1195 }
1196 // Kick the transport thread: if the port already has queued data (typical
1197 // for accelerator-startup writes that landed before the client
1198 // subscribed), this is what flushes it.
1199 readyIds.markReady(channelId);
1200}
1201
1202void Impl::handleUnsubscribe(ix::WebSocket &ws, uint64_t requestId,
1203 const json &params) {
1204 auto chIdIt = params.find("channel_id");
1205 if (chIdIt == params.end() || !chIdIt->is_number_unsigned()) {
1206 sendError(ws, requestId, "protocol_error",
1207 "unsubscribe requires unsigned \"channel_id\"");
1208 return;
1209 }
1210 uint64_t channelId = chIdIt->get<uint64_t>();
1211
1212 RpcServerWritePort *writePort = nullptr;
1213 {
1214 std::lock_guard<std::mutex> chLock(channelsMutex);
1215 auto it = channelById.find(channelId);
1216 if (it != channelById.end())
1217 writePort = it->second.writePort;
1218 }
1219
1220 std::shared_ptr<ClientSession> s = sessionFor(ws);
1221 if (!s) {
1222 sendError(ws, requestId, "internal", "No active session");
1223 return;
1224 }
1225 // Holding `subscribedMutex` across the ack keeps the transport thread from
1226 // sending another data frame on this channel before it.
1227 {
1228 std::lock_guard<std::mutex> subLock(s->subscribedMutex);
1229 auto removed = s->subscribed.erase(channelId);
1230 if (!removed) {
1231 sendError(ws, requestId, "not_subscribed",
1232 std::format("Channel id {} is not subscribed", channelId));
1233 return;
1234 }
1235 // The client drops anything it hasn't consumed once it unsubscribes.
1236 // Reset under the same lock so the transport thread can't send (and
1237 // count) another frame on this channel in between.
1238 if (writePort)
1239 writePort->unsubscribed(s->generation);
1240 sendResult(ws, requestId, json::object());
1241 }
1242}
1243
1244std::optional<Impl::ChannelInfo> Impl::findChannel(ix::WebSocket &ws,
1245 uint64_t requestId,
1246 uint64_t channelId,
1247 ChannelDirection dir) {
1248 std::optional<ChannelInfo> info;
1249 {
1250 std::lock_guard<std::mutex> chLock(channelsMutex);
1251 auto it = channelById.find(channelId);
1252 if (it != channelById.end())
1253 info = it->second;
1254 }
1255 if (!info) {
1256 sendError(ws, requestId, "unknown_channel",
1257 std::format("No channel with id {}", channelId));
1258 return std::nullopt;
1259 }
1260 if (info->direction != dir) {
1261 sendError(ws, requestId, "wrong_direction",
1262 std::format("Channel id {} is not a {} channel", channelId,
1263 dir == ChannelDirection::ToServer ? "to-server"
1264 : "to-client"));
1265 return std::nullopt;
1266 }
1267 return info;
1268}
1269
1270void Impl::sendResult(ix::WebSocket &ws, uint64_t requestId,
1271 const json &result) {
1272 json resp;
1273 resp["type"] = "response";
1274 resp["request_id"] = requestId;
1275 resp["result"] = result;
1276 // IXWebSocket serializes concurrent send calls internally. Callers must
1277 // follow the locking rules on ClientSession.
1278 ws.sendUtf8Text(resp.dump());
1279}
1280
1281void Impl::sendError(ix::WebSocket &ws, uint64_t requestId,
1282 const std::string &code, const std::string &message) {
1283 json resp;
1284 resp["type"] = "response";
1285 resp["request_id"] = requestId;
1286 resp["error"] = {{"code", code}, {"message", message}};
1287 ws.sendUtf8Text(resp.dump());
1288}
1289
1290//===----------------------------------------------------------------------===//
1291// Impl - transport thread
1292//
1293// Drains `to_client` write queues onto the WebSocket and returns credits for
1294// consumed `to_server` messages.
1295//===----------------------------------------------------------------------===//
1296
1298 while (true) {
1299 std::unordered_set<uint64_t> ids;
1300 if (!readyIds.waitDrain(ids))
1301 return;
1302 sendConsumedEvent(ids);
1303 for (uint64_t id : ids) {
1304 RpcServerWritePort *writePort = nullptr;
1305 {
1306 std::lock_guard<std::mutex> chLock(channelsMutex);
1307 auto it = channelById.find(id);
1308 if (it == channelById.end() ||
1309 it->second.direction != ChannelDirection::ToClient)
1310 continue;
1311 writePort = it->second.writePort;
1312 }
1313 if (!writePort)
1314 continue;
1315
1316 // Only drain if a client is subscribed; otherwise data stays in the
1317 // queue until subscription (handleSubscribe fires markReady). We
1318 // re-acquire `subscribedMutex` on every iteration so
1319 // `handleUnsubscribe` can interpose between frames: once it erases the
1320 // subscription and sends the unsubscribe-ack, the next iteration here
1321 // sees the channel as not subscribed and breaks, guaranteeing the ack
1322 // is the last `to_client` frame on that channel (as the protocol spec
1323 // requires). Our reference to the session keeps its socket alive
1324 // against a concurrent `onClose()`.
1325 std::shared_ptr<ClientSession> s = getSession();
1326 if (!s)
1327 continue;
1328 while (true) {
1329 if (readyIds.isShutdown())
1330 return;
1331 if (s->closed.load())
1332 break;
1333 std::lock_guard<std::mutex> subLock(s->subscribedMutex);
1334 if (!s->subscribed.count(id))
1335 break;
1336 std::optional<MessageData> msg = writePort->popForSend(s->generation);
1337 if (!msg)
1338 break;
1339 std::string frame = buildDataFrame(id, msg->getBytes(), msg->getSize());
1340 // IXWebSocket serializes concurrent send calls internally and queues
1341 // them in FIFO order on the WS.
1342 s->ws->sendBinary(frame);
1343 }
1344 }
1345 }
1346}
1347
1348void Impl::sendConsumedEvent(const std::unordered_set<uint64_t> &ids) {
1349 // Credits belong to the session which sent the frames, so are only
1350 // returned to that one. With no client there's nobody holding any.
1351 std::shared_ptr<ClientSession> s = getSession();
1352 if (!s || !s->helloDone)
1353 return;
1354 std::vector<std::pair<uint64_t, RpcServerReadPort *>> ports;
1355 {
1356 std::lock_guard<std::mutex> chLock(channelsMutex);
1357 for (uint64_t id : ids) {
1358 auto it = channelById.find(id);
1359 if (it != channelById.end() && it->second.readPort)
1360 ports.emplace_back(id, it->second.readPort);
1361 }
1362 }
1363 json channels = json::array();
1364 for (auto [id, port] : ports)
1365 if (uint64_t count = port->takeConsumed(s->generation))
1366 channels.push_back({{"channel_id", id}, {"count", count}});
1367 if (channels.empty())
1368 return;
1369
1370 json event;
1371 event["type"] = "event";
1372 event["event"] = "consumed";
1373 event["params"] = {{"channels", std::move(channels)}};
1374 s->ws->sendUtf8Text(event.dump());
1375}
1376
1377//===----------------------------------------------------------------------===//
1378// RpcServer pass-throughs
1379//===----------------------------------------------------------------------===//
1380
1382RpcServer::~RpcServer() = default;
1383
1384void RpcServer::setManifest(int esiVersion,
1385 const std::vector<uint8_t> &compressedManifest) {
1386 if (!impl)
1387 throw std::runtime_error("Server not running");
1388 impl->setManifest(esiVersion, compressedManifest);
1389}
1390
1392 const std::string &type) {
1393 if (!impl)
1394 throw std::runtime_error("Server not running");
1395 return impl->registerReadPort(name, type);
1396}
1397
1399 const std::string &type) {
1400 if (!impl)
1401 throw std::runtime_error("Server not running");
1402 return impl->registerWritePort(name, type);
1403}
1404
1406 if (!impl)
1407 throw std::runtime_error("Server not running");
1408 impl->messageConsumed(port);
1409}
1410
1411void RpcServer::run(int port) {
1412 if (impl)
1413 throw std::runtime_error("Server already running");
1414 impl = std::make_unique<Impl>(ctxt, port);
1415}
1416
1417void RpcServer::stop(uint32_t timeoutMS) {
1418 if (!impl)
1419 throw std::runtime_error("Server not running");
1420 impl->stop(timeoutMS);
1421}
1422
1424 if (!impl)
1425 throw std::runtime_error("Server not running");
1426 return impl->getPort();
1427}
esi::cosim::RpcServer::Impl Impl
static bool parseConsumedEvent(const json &frame, std::vector< std::pair< uint64_t, uint64_t > > &out, std::string &err)
Parse the channels list of a consumed event into (channel_id, count) pairs.
A client for the cosim RPC server.
Definition RpcClient.h:34
static constexpr unsigned DefaultToClientMaxInFlight
Default maximum number of frames in flight (queued in the simulator or sent but not yet consumed by t...
Definition RpcClient.h:71
AcceleratorConnections, Accelerators, and Manifests must all share a context.
Definition Context.h:34
A concrete flat message backed by a single vector of bytes.
Definition Common.h:197
size_t getSize() const
Get the size of the data in bytes.
Definition Common.h:222
A ChannelPort which reads data from the accelerator.
Definition Ports.h:479
virtual void connect(ReadCallback callback, const ConnectOptions &options={})
Definition Ports.cpp:140
virtual void disconnect() override
Disconnect the channel.
Definition Ports.cpp:70
Root class of the ESI type system.
Definition Types.h:36
A ChannelPort which sends data to the accelerator.
Definition Ports.h:308
virtual void disconnect() override
Definition Ports.h:322
A client for the cosim RPC server.
Definition RpcClient.h:34
ChannelDirection
Channel direction as reported by the server.
Definition RpcClient.h:50
static constexpr unsigned DefaultToClientMaxInFlight
Default maximum number of frames in flight (queued in the simulator or sent but not yet consumed by t...
Definition RpcClient.h:71
Cross-thread error channel for the IXWebSocket network thread.
Definition RpcWire.h:73
ReadChannelPort & registerReadPort(const std::string &name, const std::string &type)
std::unique_ptr< ix::WebSocketServer > server
void onClose(ix::WebSocket &ws)
void logRefusedFrame(uint64_t channelId, size_t size)
Log that a size-byte frame for channel channelId was dropped since the simulator-side consumer refuse...
void handleBinaryFrame(uint64_t generation, const std::string &data)
std::vector< uint8_t > compressedManifest
void handleUnsubscribe(ix::WebSocket &ws, uint64_t requestId, const json &params)
std::optional< ChannelInfo > findChannel(ix::WebSocket &ws, uint64_t requestId, uint64_t channelId, ChannelDirection dir)
Look up a channel.
std::unordered_map< std::string, uint64_t > idByName
void sendResult(ix::WebSocket &ws, uint64_t requestId, const json &result)
void handleHello(ix::WebSocket &ws, uint64_t requestId, const json &params)
::esi::cosim::FaultStash faultStash
void sendConsumedEvent(const std::unordered_set< uint64_t > &ids)
Send a batched consumed event for any to-server messages the simulation has consumed since the last c...
void handleControlFrame(ix::WebSocket &ws, const std::string &text)
std::shared_ptr< ClientSession > session
void sendError(ix::WebSocket &ws, uint64_t requestId, const std::string &code, const std::string &message)
Impl(Context &ctxt, int port)
void stop(uint32_t timeoutMS)
std::condition_variable manifestReadyCV
std::map< std::string, std::unique_ptr< RpcServerWritePort > > writePorts
std::shared_ptr< ClientSession > sessionFor(const ix::WebSocket &ws)
The active session if ws is its socket, else null.
void handleEvent(uint64_t generation, const json &frame)
WriteChannelPort & registerWritePort(const std::string &name, const std::string &type)
std::unordered_map< uint64_t, ChannelInfo > channelById
std::map< std::string, std::unique_ptr< RpcServerReadPort > > readPorts
backends::cosim::RpcClient::ChannelDirection ChannelDirection
void handleSubscribe(ix::WebSocket &ws, uint64_t requestId, const json &params)
void onOpen(const std::weak_ptr< ix::WebSocket > &wsWeak)
void onClientMessage(const std::weak_ptr< ix::WebSocket > &wsWeak, ix::WebSocket &ws, const ix::WebSocketMessagePtr &msg)
utils::ReadyIdSet< uint64_t > readyIds
void retireSession(uint64_t generation)
Forget all per-channel state belonging to client session generation, which has disconnected.
std::shared_ptr< ClientSession > getSession()
The active session, if any.
void messageConsumed(ReadChannelPort &port)
void setManifest(int esiVersion, const std::vector< uint8_t > &compressedManifest)
RpcServer(Context &ctxt)
void messageConsumed(ReadChannelPort &port)
Report that the simulation has consumed one message read from port (which must have been returned by ...
ReadChannelPort & registerReadPort(const std::string &name, const std::string &type)
Register a read or write port which communicates over RPC.
void run(int port=-1)
void stop(uint32_t timeoutMS=0)
void setManifest(int esiVersion, const std::vector< uint8_t > &compressedManifest)
Set the manifest and version.
std::unique_ptr< Impl > impl
Definition RpcServer.h:79
WriteChannelPort & registerWritePort(const std::string &name, const std::string &type)
Multi-producer / single-consumer dirty-set of channel ids, with CV-style blocking drain semantics.
Definition Utils.h:177
Thread safe queue.
Definition Utils.h:48
void push(E... t)
Push onto the queue.
Definition Utils.h:86
std::optional< T > pop()
Pop something off the queue but return nullopt if the queue is empty.
Definition Utils.h:98
std::string buildDataFrame(uint64_t channelId, const uint8_t *bytes, size_t size)
Pack a cosim binary data frame: [u64 LE channel_id][payload].
Definition RpcWire.h:33
constexpr uint64_t kProtocolVersion
Wire protocol version.
Definition RpcWire.h:30
bool parseDataFrame(const std::string &data, uint64_t &channelId, const uint8_t *&payload, size_t &payloadSize)
Parse a cosim binary data frame.
Definition RpcWire.h:52
Definition esi.py:1
std::shared_ptr< ix::WebSocket > ws
std::unordered_set< uint64_t > subscribed