109 : _url{::morph::net::detail::parseWsUrl(serverUrl)},
111 _core{std::make_shared<Core>(loop, _url, cfg)} {
122 : _url{::morph::net::detail::parseWsUrl(serverUrl)},
123 _ownedLoop{std::make_unique<::morph::exec::IoLoop>()},
124 _loop{_ownedLoop.get()},
125 _core{std::make_shared<Core>(*_ownedLoop, _url, cfg)} {
143 _loop->runAndWait([core = _core] { core->close(); });
156 bool waitForConnected(std::chrono::milliseconds timeout = std::chrono::milliseconds{5000}) {
157 if (_core->connected.load() || _loop->
runningHere()) {
158 return _core->connected.load();
160 auto waiter = std::make_shared<std::promise<void>>();
161 std::future<void>
const released = waiter->get_future();
162 _loop->
post([core = _core, waiter] { core->addConnectWaiter(waiter); });
163 static_cast<void>(released.wait_for(timeout));
164 return _core->connected.load();
179 const std::string& typeId,
180 std::function<std::unique_ptr<::morph::model::detail::IModelHolder>()> factory)
override {
197 const std::string& typeId, std::function<std::unique_ptr<::morph::model::detail::IModelHolder>()> factory,
198 std::string_view contextKey)
override {
200 return bindModel(::morph::backend::detail::BindRequest{.typeId = typeId,
201 .factory = std::move(factory),
202 .contextKey = std::string{contextKey},
213 void assignPrimary(::morph::exec::detail::ModelId mid,
const std::string& typeId,
214 std::string_view primary)
override {
215 if (primary.empty() || mid.v == 0U) {
220 ::morph::backend::detail::PromoteRequest{
221 .mid = mid, .typeId = typeId, .primary = std::string{primary}},
269 return sendControlAsync(::morph::wire::makeRegister(
request.typeId,
request.contextKey),
"register",
270 std::nullopt, cbExec, BeforeConnect::Queue);
273 return sendControlAsync(
275 "attach", std::nullopt, cbExec);
277 return sendControlAsync(::morph::wire::makeRegisterShared(
request.typeId,
request.primary,
request.contextKey),
278 "register", std::nullopt, cbExec);
298 auto state = std::make_shared<::morph::async::detail::CompletionState<::morph::exec::detail::ModelId>>();
303 return sendControlAsync(::morph::wire::makeAssign(
request.typeId,
request.primary,
request.mid.v),
"assign",
313 std::vector<std::string>
listInstances(
const std::string& typeId)
override {
315 auto state = std::make_shared<::morph::async::detail::CompletionState<std::string>>();
317 _loop->post([core = _core, env = ::morph::wire::makeInstances(typeId),
318 pending = PendingControl{
319 .state =
nullptr, .what =
"instances", .echo = std::nullopt, .body = state}]()
mutable {
320 core->fileControl(std::move(env), std::move(pending));
324 std::vector<std::string> keys;
325 if (
auto errCode = glz::read_json(keys, body)) {
326 throw std::runtime_error(
"instances decode failed: " + glz::format_error(errCode, body));
344 auto state = std::make_shared<::morph::async::detail::CompletionState<::morph::wire::Envelope>>();
346 std::string what = envelope.
kind;
347 _loop->post([core = _core, env = std::move(envelope),
348 pending = PendingControl{.state =
nullptr,
349 .what = std::move(what),
350 .echo = std::nullopt,
352 .beforeConnect = BeforeConnect::Queue,
353 .reply = std::move(state)}]()
mutable {
354 core->fileControl(std::move(env), std::move(pending));
374 using Result = ::morph::wire::ProtocolNegotiationResult;
375 auto const state = std::make_shared<::morph::async::detail::CompletionState<Result>>();
377 _loop->post([core = _core, state] {
378 core->note(
"SocketBackend::negotiateProtocolVersion");
379 core->negotiate =
true;
380 core->fileHello(state);
395 if (!_core->connected.load()) {
398 _loop->post([core = _core, env = ::morph::wire::makeDeregister(mid.v)]()
mutable {
399 core->sendDeregister(std::move(env));
413 ::morph::backend::detail::ActionCall call,
415 auto compState = std::make_shared<::morph::async::detail::CompletionState<std::shared_ptr<void>>>();
417 if (!_core->connected.load()) {
426 env.kind =
"execute";
428 env.modelType = call.modelTypeId;
429 env.actionType = call.actionTypeId;
430 env.body = call.serializeBody();
431 env.session = std::move(call.session);
433 PendingExecute pending{.state = compState, .deserialize = call.deserializeResult};
434 if (call.stopSource) {
437 pending.stop = call.stopSource;
438 pending.session = env.session;
440 _loop->post([core = _core, env, pending = std::move(pending)]
mutable {
441 core->fileExecute(std::move(env), std::move(pending));
460 _loop->post([core = _core, exc] {
461 ::morph::exec::detail::noteOwner(
"SocketBackend::cancelPending", core->loop.loop(),
462 core->loop.runningHere());
463 core->cancelAll(exc, CancelOnServer::Yes);
478 _loop->post([core = _core, handler = std::move(handler), exec]()
mutable {
479 core->note(
"SocketBackend::setReconnectHandler");
480 core->reconnectHandler = std::move(handler);
481 core->reconnectExec = exec;
494 _loop->post([core = _core, handler] {
495 core->note(
"SocketBackend::setConnectHandler");
496 core->connectHandler = handler;
513 _loop->post([core = _core, handler = std::move(handler), exec]()
mutable {
514 core->note(
"SocketBackend::setEventHandler");
518 core->eventExec = exec;
531 _loop->post([core = _core, session = std::move(session)]()
mutable {
532 core->note(
"SocketBackend::setSession");
533 core->session = std::move(session);
552 template <
typename T,
typename Ask>
553 T awaitReply(std::string_view what, Ask ask) {
555 throw std::runtime_error(std::string{what} +
556 " failed: a synchronous call cannot wait on the I/O loop's own thread");
560 std::optional<T> value;
561 std::exception_ptr error;
563 waiter.
post([&completion, &value, &error, &done] {
565 .then([&value, &done](
const T& settled) {
566 value.emplace(settled);
569 .onError([&error, &done](
const std::exception_ptr& failure) {
578 _loop->runAndWait([&alive] { alive =
true; });
580 throw std::runtime_error(std::string{what} +
" failed: the I/O loop has stopped");
586 waiter.
runFor(std::chrono::milliseconds{1});
591 std::rethrow_exception(error);
592 }
catch (const ::morph::backend::DisconnectedError&) {
593 throw std::runtime_error(std::string{what} +
" failed: disconnected");
596 return std::move(*value);
602 enum class CancelOnServer : std::uint8_t { No, Yes };
607 struct CancelOnStop {
608 std::weak_ptr<Core> core;
609 ::morph::exec::IoLoop* loop{
nullptr};
610 std::uint64_t callId{0};
611 void operator()() const noexcept {
613 loop->post([weak = core, target = callId] {
614 if (
auto const live = weak.lock()) {
615 live->requestCancel(target);
625 struct PendingExecute {
626 std::shared_ptr<::morph::async::detail::CompletionState<std::shared_ptr<void>>> state;
627 std::shared_ptr<void> (*deserialize)(std::string_view json){
nullptr};
629 std::shared_ptr<::core::async::StopSource> stop;
631 ::morph::session::Context session;
635 std::shared_ptr<::core::async::StopCallback<CancelOnStop>> watch;
639 enum class BeforeConnect : std::uint8_t {
651 struct PendingControl {
654 std::shared_ptr<::morph::async::detail::CompletionState<::morph::exec::detail::ModelId>> state;
660 std::optional<::morph::exec::detail::ModelId> echo;
663 std::shared_ptr<::morph::async::detail::CompletionState<std::string>> body;
669 std::shared_ptr<::morph::async::detail::CompletionState<::morph::wire::ProtocolNegotiationResult>> negotiation;
671 BeforeConnect beforeConnect = BeforeConnect::Reject;
674 std::shared_ptr<::morph::async::detail::CompletionState<::morph::wire::Envelope>> reply;
678 void reject(
const std::exception_ptr& failure)
const {
680 state->setException(failure);
683 body->setException(failure);
686 negotiation->setException(failure);
689 reply->setException(failure);
703 ::morph::async::Completion<::morph::exec::detail::ModelId> sendControlAsync(
704 ::morph::wire::Envelope env, std::string_view what, std::optional<::morph::exec::detail::ModelId> echo,
705 ::morph::exec::IExecutor& cbExec, BeforeConnect beforeConnect = BeforeConnect::Reject) {
706 auto state = std::make_shared<::morph::async::detail::CompletionState<::morph::exec::detail::ModelId>>();
707 ::morph::async::Completion<::morph::exec::detail::ModelId> comp{state, &cbExec};
708 _loop->post([core = _core, env,
709 pending = PendingControl{.state = state,
710 .what = std::string{what},
713 .beforeConnect = beforeConnect}]()
mutable {
714 core->fileControl(std::move(env), std::move(pending));
722 static void settleControl(
const PendingControl& pending, const ::morph::wire::Envelope& reply) {
726 if (pending.negotiation) {
728 pending.negotiation->setValue(::morph::wire::interpretHelloReply(reply));
730 pending.negotiation->setException(std::current_exception());
737 pending.reply->setValue(reply);
740 if (reply.kind !=
"ok") {
741 pending.reject(std::make_exception_ptr(std::runtime_error(pending.what +
" failed: " + reply.message)));
745 pending.state->setValue(pending.echo.value_or(::morph::exec::detail::ModelId{reply.modelId}));
748 pending.body->setValue(reply.body);
753 _loop->post([core = _core] { core->startAttempt(); });
761 struct Core : std::enable_shared_from_this<Core> {
762 Core(::morph::exec::IoLoop& ioLoop, ::morph::net::detail::ParsedWsUrl target,
Config config)
763 : loop{ioLoop}, url{std::move(target)}, cfg{config}, reconnectDelay{config.initialReconnectDelay} {}
766 void note(
char const* site)
const noexcept {
767 ::morph::exec::detail::noteOwner(site, loop.loop(), loop.runningHere());
770 void startAttempt() {
772 loop.loop().spawn(attemptFlow(shared_from_this()));
778 bool send(std::string frame) {
779 note(
"SocketBackend::send");
783 return ::morph::net::detail::enqueueFrame(conn, std::move(frame));
787 void fileExecute(::morph::wire::Envelope env, PendingExecute pending) {
790 note(
"SocketBackend::execute");
791 if (!connected.load() || !conn) {
792 pending.state->setException(std::make_exception_ptr(::morph::backend::DisconnectedError{}));
795 std::uint64_t
const callId = executes.nextCallId();
799 frame = ::morph::net::detail::encodeWsFrame(::morph::net::detail::WsOpcode::kText,
800 ::morph::wire::encode(env),
true);
802 pending.state->setException(std::current_exception());
805 auto const stop = pending.stop;
806 executes.insert(callId, std::move(pending));
809 static_cast<void>(send(std::move(frame)));
814 auto watch = std::make_shared<::core::async::StopCallback<CancelOnStop>>(
815 stop->get_token(), CancelOnStop{.core = weak_from_this(), .loop = &loop, .callId = callId});
816 if (
auto entry = executes.take(callId)) {
817 entry->watch = std::move(watch);
818 executes.insert(callId, std::move(*entry));
826 void requestCancel(std::uint64_t callId) {
827 note(
"SocketBackend::requestCancel");
828 if (!cancelAdvertised) {
833 auto entry = executes.take(callId);
837 sendCancel(callId, entry->stop ? entry->session : session);
838 executes.insert(callId, std::move(*entry));
843 void sendCancel(std::uint64_t target, const ::morph::session::Context& callerSession) {
844 auto env = ::morph::wire::makeCancel(target);
845 env.callId = executes.nextCallId();
846 env.session = callerSession;
848 static_cast<void>(send(::morph::net::detail::encodeWsFrame(
849 ::morph::net::detail::WsOpcode::kText, ::morph::wire::encode(env),
true)));
850 }
catch (
const std::exception&) {
859 std::shared_ptr<::morph::async::detail::CompletionState<::morph::wire::ProtocolNegotiationResult>>
861 fileControl(::morph::wire::makeHello(), PendingControl{.state =
nullptr,
863 .echo = std::nullopt,
866 .negotiation = std::move(negotiation)});
870 void fileControl(::morph::wire::Envelope env, PendingControl pending) {
872 note(
"SocketBackend::bindModel");
873 if (!connected.load() || !conn) {
874 if (pending.beforeConnect == BeforeConnect::Queue && !closed && !attemptsOver) {
876 queuedControls.emplace_back(std::move(env), std::move(pending));
879 pending.reject(std::make_exception_ptr(::morph::backend::DisconnectedError{}));
882 std::uint64_t
const callId = executes.nextCallId();
884 if (!pending.reply) {
885 env.session = session;
889 frame = ::morph::net::detail::encodeWsFrame(::morph::net::detail::WsOpcode::kText,
890 ::morph::wire::encode(env),
true);
891 }
catch (
const std::exception& exc) {
892 pending.reject(std::make_exception_ptr(std::runtime_error(pending.what +
" failed: " + exc.what())));
895 controls.insert(callId, std::move(pending));
896 static_cast<void>(send(std::move(frame)));
899 void sendDeregister(::morph::wire::Envelope env) {
900 note(
"SocketBackend::deregisterModel");
901 if (!connected.load()) {
904 env.callId = executes.nextCallId();
905 env.session = session;
907 static_cast<void>(send(::morph::net::detail::encodeWsFrame(
908 ::morph::net::detail::WsOpcode::kText, ::morph::wire::encode(env),
true)));
909 }
catch (
const std::exception&) {
916 void addConnectWaiter(
const std::shared_ptr<std::promise<void>>& waiter) {
917 note(
"SocketBackend::waitForConnected");
918 if (connected.load()) {
923 connectWaiters.push_back(waiter);
931 void cancelAll(
const std::exception_ptr& exc, CancelOnServer onServer = CancelOnServer::No) {
932 auto drained = executes.drain();
933 auto drainedControl = controls.drain();
934 auto drainedQueue = std::exchange(queuedControls, {});
935 if (onServer == CancelOnServer::Yes && cancelAdvertised) {
936 for (
auto const& [callId, pending] : drained) {
937 sendCancel(callId, pending.stop ? pending.session : session);
940 for (
auto& entry : drained) {
941 if (entry.second.state) {
942 entry.second.state->setException(exc);
945 for (
auto& entry : drainedControl) {
946 entry.second.reject(exc);
948 for (
auto& entry : drainedQueue) {
949 entry.second.reject(exc);
954 bool const isReconnect = everConnected;
955 everConnected =
true;
956 connected.store(
true);
957 reconnectDelay = cfg.initialReconnectDelay;
958 for (
auto const& waiter : std::exchange(connectWaiters, {})) {
968 for (
auto& [env, pending] : std::exchange(queuedControls, {})) {
969 fileControl(std::move(env), std::move(pending));
971 if (connectHandler) {
974 }
catch (
const std::exception& exc) {
975 ::morph::log::logWarn(
"[SocketBackend] connect handler threw: {}", exc.what());
977 ::morph::log::logWarn(
"[SocketBackend] connect handler threw a non-standard exception");
980 if (isReconnect && reconnectHandler && reconnectExec !=
nullptr) {
983 reconnectExec->post(reconnectHandler);
987 void onDisconnected() {
988 connected.store(
false);
989 cancelAdvertised =
false;
991 ::morph::net::detail::closeAfterFlush(conn);
994 cancelAll(std::make_exception_ptr(::morph::backend::DisconnectedError{}));
998 void scheduleReconnect() {
999 note(
"SocketBackend::reconnect");
1000 backoffTimer = loop.loop().addTimer(loop.loop().clock().now() + reconnectDelay, &Core::onBackoff,
this);
1001 auto const nextDelayMs =
static_cast<std::chrono::milliseconds::rep
>(
1002 static_cast<double>(reconnectDelay.count()) * cfg.backoffMultiplier);
1003 reconnectDelay = std::min(std::chrono::milliseconds{nextDelayMs}, cfg.maxReconnectDelay);
1008 static void onBackoff(
void* corePtr) {
1009 auto&
self = *
static_cast<Core*
>(corePtr);
1010 self.backoffTimer = {};
1011 self.startAttempt();
1017 note(
"SocketBackend::close");
1022 connected.store(
false);
1023 static_cast<void>(loop.loop().cancelTimer(backoffTimer));
1031 ::morph::net::detail::closeAfterFlush(conn);
1034 connectWaiters.clear();
1035 reconnectHandler =
nullptr;
1036 connectHandler =
nullptr;
1037 eventHandler =
nullptr;
1038 cancelAll(std::make_exception_ptr(::morph::backend::DisconnectedError{}));
1041 void dispatchIncomingEnvelope(
const std::string& payload) {
1042 MORPH_ZONE(
"SocketBackend::dispatchIncomingEnvelope");
1043 ::morph::wire::Envelope
env;
1045 env = ::morph::wire::decode(payload);
1046 }
catch (
const std::exception&) {
1054 cancelAll(std::make_exception_ptr(
1055 std::runtime_error(
"protocol error: server sent a message that is not a valid envelope")));
1058 note(
"SocketBackend::reply");
1061 if (::morph::backend::detail::routeEvent(env, [
this](::morph::wire::EventBody event) {
1062 if (eventHandler && eventExec !=
nullptr) {
1063 eventExec->post([handler = eventHandler, event = std::move(event)]()
mutable {
1064 (*handler)(std::move(event));
1070 auto pending = executes.take(
env.callId);
1074 if (
auto control = controls.take(
env.callId)) {
1075 if (control->hello) {
1076 cancelAdvertised = ::morph::wire::helloAdvertises(env, ::morph::wire::kCapabilityCancel);
1078 settleControl(*control, env);
1088 switch (::morph::backend::detail::classifyExecuteReply(env)) {
1089 case ::morph::backend::detail::ExecuteReplyKind::Value:
1091 if (pending->deserialize ==
nullptr) {
1092 throw std::runtime_error{
"ActionCall::deserializeResult is null"};
1094 pending->state->setValue(pending->deserialize(
env.body));
1096 pending->state->setException(std::current_exception());
1099 case ::morph::backend::detail::ExecuteReplyKind::Timeout:
1100 pending->state->setException(std::make_exception_ptr(::morph::backend::TimeoutError{}));
1102 case ::morph::backend::detail::ExecuteReplyKind::Error:
1107 pending->state->setException(std::make_exception_ptr(std::runtime_error(
env.message)));
1115 bool drainFrames(
const std::shared_ptr<::morph::net::detail::LoopConnection>& connection,
1116 ::morph::net::detail::WsFrameReader& reader) {
1118 using ::morph::net::detail::WsOpcode;
1120 std::optional<::morph::net::detail::WsFrame> frame;
1122 frame = reader.tryExtractFrame();
1123 }
catch (
const std::exception&) {
1129 if (frame->opcode == WsOpcode::kClose) {
1130 static_cast<void>(::morph::net::detail::enqueueFrame(
1131 connection, ::morph::net::detail::encodeWsFrame(WsOpcode::kClose, frame->payload,
true)));
1134 if (frame->opcode == WsOpcode::kPing) {
1135 static_cast<void>(::morph::net::detail::enqueueFrame(
1136 connection, ::morph::net::detail::encodeWsFrame(WsOpcode::kPong, frame->payload,
true)));
1139 if (frame->opcode == WsOpcode::kText) {
1140 dispatchIncomingEnvelope(frame->payload);
1152 static ::core::async::Task<void> attemptFlow(std::shared_ptr<Core> self) {
1153 bool reachedServer =
false;
1155 ::core::net::DialOptions options;
1156 options.connectTimeout =
self->cfg.connectTimeout;
1157 auto dialed = co_await ::core::net::connect(&
self->loop.loop(),
self->url.host,
self->url.port,
1158 &::core::net::defaultAsyncResolver(), options);
1163 auto connection = std::make_shared<::morph::net::detail::LoopConnection>(
1164 self->loop.loop(), std::move(*dialed),
self->cfg.sendTimeout);
1165 self->conn = connection;
1166 std::string
const key = ::morph::net::detail::generateClientKey();
1167 static_cast<void>(::morph::net::detail::enqueueFrame(
1168 connection, ::morph::net::detail::buildClientHandshakeRequest(
self->url, key)));
1170 co_await ::morph::net::detail::readHeaderBlockAsync(connection,
self->cfg.handshakeTimeout);
1174 bool verified =
false;
1177 ::morph::net::detail::verifyServerHandshakeResponse(header->header, key);
1179 }
catch (
const std::exception&) {
1184 reachedServer =
true;
1185 self->onConnected();
1189 ::morph::net::detail::WsFrameReader reader{
false};
1190 reader.feed(header->leftover);
1191 std::array<std::byte, 4096> buf{};
1192 while (
self->drainFrames(connection, reader) && !connection->closed) {
1193 auto const got =
co_await connection->socket->read(buf);
1194 if (
self->closed || !got || *got == 0) {
1198 reader.feed(std::string_view{
reinterpret_cast<char const*
>(buf.data()), *got});
1206 self->onDisconnected();
1207 if (!reachedServer && !
self->everConnected) {
1211 self->attemptsOver =
true;
1214 self->attemptsOver = !
self->cfg.reconnectEnabled;
1215 if (!
self->attemptsOver) {
1216 self->scheduleReconnect();
1220 ::morph::exec::IoLoop& loop;
1221 ::morph::net::detail::ParsedWsUrl url;
1225 std::atomic<bool> connected{
false};
1227 ::morph::session::Context session;
1229 std::function<void()> reconnectHandler;
1230 ::morph::exec::IExecutor* reconnectExec{
nullptr};
1232 std::function<void()> connectHandler;
1235 std::shared_ptr<std::function<void(::morph::wire::EventBody)>> eventHandler;
1236 ::morph::exec::IExecutor* eventExec{
nullptr};
1237 bool everConnected{
false};
1241 bool attemptsOver{
false};
1243 bool negotiate{
false};
1246 bool cancelAdvertised{
false};
1247 std::chrono::milliseconds reconnectDelay;
1248 ::core::net::TimerId backoffTimer{};
1249 std::shared_ptr<::morph::net::detail::LoopConnection> conn;
1250 ::morph::backend::detail::PendingCallTable<PendingExecute> executes;
1254 ::morph::backend::detail::PendingCallTable<PendingControl> controls;
1257 std::vector<std::pair<::morph::wire::Envelope, PendingControl>> queuedControls;
1258 std::vector<std::shared_ptr<std::promise<void>>> connectWaiters;
1263 ::morph::net::detail::ParsedWsUrl _url;
1266 std::unique_ptr<::morph::exec::IoLoop> _ownedLoop;
1267 ::morph::exec::IoLoop* _loop;
1268 std::shared_ptr<Core> _core;