Merge branch 'dev' into stable

This commit is contained in:
Jason Rhinelander 2020-10-19 23:45:57 -03:00
commit 95d7e0964f
12 changed files with 170 additions and 54 deletions

View File

@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.7)
# Has to be set before `project()`, and ignored on non-macos:
set(CMAKE_OSX_DEPLOYMENT_TARGET 10.14 CACHE STRING "macOS deployment target (Apple clang only)")
set(CMAKE_OSX_DEPLOYMENT_TARGET 10.12 CACHE STRING "macOS deployment target (Apple clang only)")
project(liblokimq CXX C)
@ -9,7 +9,7 @@ include(GNUInstallDirs)
set(LOKIMQ_VERSION_MAJOR 1)
set(LOKIMQ_VERSION_MINOR 2)
set(LOKIMQ_VERSION_PATCH 0)
set(LOKIMQ_VERSION_PATCH 1)
set(LOKIMQ_VERSION "${LOKIMQ_VERSION_MAJOR}.${LOKIMQ_VERSION_MINOR}.${LOKIMQ_VERSION_PATCH}")
message(STATUS "lokimq v${LOKIMQ_VERSION}")
@ -153,6 +153,7 @@ install(
lokimq/lokimq.h
lokimq/message.h
lokimq/string_view.h
lokimq/variant.h
${CMAKE_CURRENT_BINARY_DIR}/lokimq/version.h
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/lokimq
)

View File

@ -35,7 +35,7 @@
#include <ostream>
#include <string>
#include <string_view>
#include <variant>
#include "variant.h"
#include <cstdint>
#include <limits>
#include <stdexcept>
@ -428,8 +428,8 @@ struct bt_deserialize_try_variant_impl<std::enable_if_t<!is_bt_deserializable<T>
// Serialization of a variant; all variant types must be bt-serializable.
template <typename... Ts>
struct bt_serialize<std::variant<Ts...>, std::void_t<bt_serialize<Ts>...>> {
void operator()(std::ostream &os, const std::variant<Ts...>& val) {
std::visit(
void operator()(std::ostream& os, const std::variant<Ts...>& val) {
var::visit(
[&os] (const auto& val) {
using T = std::remove_cv_t<std::remove_reference_t<decltype(val)>>;
bt_serialize<T>{}(os, val);
@ -556,15 +556,14 @@ inline bt_value bt_get(std::string_view s) {
/// auto v = get_int<uint32_t>(val); // throws if the decoded value doesn't fit in a uint32_t
template <typename IntType, std::enable_if_t<std::is_integral_v<IntType>, int> = 0>
IntType get_int(const bt_value &v) {
if (std::holds_alternative<uint64_t>(v)) {
uint64_t value = std::get<uint64_t>(v);
if (auto* value = std::get_if<uint64_t>(&v)) {
if constexpr (!std::is_same_v<IntType, uint64_t>)
if (value > static_cast<uint64_t>(std::numeric_limits<IntType>::max()))
if (*value > static_cast<uint64_t>(std::numeric_limits<IntType>::max()))
throw std::overflow_error("Unable to extract integer value: stored value is too large for the requested type");
return static_cast<IntType>(value);
return static_cast<IntType>(*value);
}
int64_t value = std::get<int64_t>(v);
int64_t value = var::get<int64_t>(v); // throws if no int contained
if constexpr (!std::is_same_v<IntType, int64_t>)
if (value > static_cast<int64_t>(std::numeric_limits<IntType>::max())
|| value < static_cast<int64_t>(std::numeric_limits<IntType>::min()))
@ -589,7 +588,7 @@ Tuple get_tuple(const bt_list& x) {
}
template <typename Tuple>
Tuple get_tuple(const bt_value& x) {
return get_tuple<Tuple>(std::get<bt_list>(static_cast<const bt_variant&>(x)));
return get_tuple<Tuple>(var::get<bt_list>(static_cast<const bt_variant&>(x)));
}
namespace detail {
@ -601,15 +600,15 @@ void get_tuple_impl_one(T& t, It& it) {
} else if constexpr (is_bt_tuple<T>) {
if (std::holds_alternative<bt_list>(v))
throw std::invalid_argument{"Unable to convert tuple: cannot create sub-tuple from non-bt_list"};
t = get_tuple<T>(std::get<bt_list>(v));
t = get_tuple<T>(var::get<bt_list>(v));
} else if constexpr (std::is_same_v<std::string, T> || std::is_same_v<std::string_view, T>) {
// If we request a string/string_view, we might have the other one and need to copy/view it.
if (std::holds_alternative<std::string_view>(v))
t = std::get<std::string_view>(v);
t = var::get<std::string_view>(v);
else
t = std::get<std::string>(v);
t = var::get<std::string>(v);
} else {
t = std::get<T>(v);
t = var::get<T>(v);
}
}
template <typename Tuple, size_t... Is>

View File

@ -378,7 +378,7 @@ LokiMQ::run_info& LokiMQ::run_info::load(pending_command&& pending) {
assert(pending.callback.index() == 0);
return load(&pending.cat, std::move(pending.command), std::move(pending.conn), std::move(pending.access),
std::move(pending.remote), std::move(pending.data_parts), std::get<0>(pending.callback));
std::move(pending.remote), std::move(pending.data_parts), var::get<0>(pending.callback));
}
LokiMQ::run_info& LokiMQ::run_info::load(batch_job&& bj, bool reply_job, int tagged_thread) {

103
lokimq/variant.h Normal file
View File

@ -0,0 +1,103 @@
#pragma once
// Workarounds for macos compatibility. On macOS we aren't allowed to touch anything in
// std::variant that could throw if compiling with a target <10.14 because Apple fails hard at
// properly updating their STL. Thus, if compiling in such a mode, we have to introduce
// workarounds.
//
// This header defines a `var` namespace with `var::get` and `var::visit` implementations. On
// everything except broken backwards macos, this is just an alias to `std`. On broken backwards
// macos, we provide implementations that throw std::runtime_error in failure cases since the
// std::bad_variant_access exception can't be touched.
//
// You also get a BROKEN_APPLE_VARIANT macro defined if targetting a problematic mac architecture.
#include <variant>
#ifdef __APPLE__
# include <AvailabilityVersions.h>
# if defined(__APPLE__) && MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_14
# define BROKEN_APPLE_VARIANT
# endif
#endif
#ifndef BROKEN_APPLE_VARIANT
namespace var = std; // Oh look, actual C++17 support
#else
// Oh look, apple.
namespace var {
// Apple won't let us use std::visit or std::get if targetting some version of macos earlier than
// 10.14 because Apple is awful about not updating their STL. So we have to provide our own, and
// then call these without `std::` -- on crappy macos we'll come here, on everything else we'll ADL
// to the std:: implementation.
template <typename T, typename... Types>
constexpr T& get(std::variant<Types...>& var) {
if (auto* v = std::get_if<T>(&var)) return *v;
throw std::runtime_error{"Bad variant access -- variant does not contain the requested type"};
}
template <typename T, typename... Types>
constexpr const T& get(const std::variant<Types...>& var) {
if (auto* v = std::get_if<T>(&var)) return *v;
throw std::runtime_error{"Bad variant access -- variant does not contain the requested type"};
}
template <typename T, typename... Types>
constexpr const T&& get(const std::variant<Types...>&& var) {
if (auto* v = std::get_if<T>(&var)) return std::move(*v);
throw std::runtime_error{"Bad variant access -- variant does not contain the requested type"};
}
template <typename T, typename... Types>
constexpr T&& get(std::variant<Types...>&& var) {
if (auto* v = std::get_if<T>(&var)) return std::move(*v);
throw std::runtime_error{"Bad variant access -- variant does not contain the requested type"};
}
template <size_t I, typename... Types>
constexpr auto& get(std::variant<Types...>& var) {
if (auto* v = std::get_if<I>(&var)) return *v;
throw std::runtime_error{"Bad variant access -- variant does not contain the requested type"};
}
template <size_t I, typename... Types>
constexpr const auto& get(const std::variant<Types...>& var) {
if (auto* v = std::get_if<I>(&var)) return *v;
throw std::runtime_error{"Bad variant access -- variant does not contain the requested type"};
}
template <size_t I, typename... Types>
constexpr const auto&& get(const std::variant<Types...>&& var) {
if (auto* v = std::get_if<I>(&var)) return std::move(*v);
throw std::runtime_error{"Bad variant access -- variant does not contain the requested type"};
}
template <size_t I, typename... Types>
constexpr auto&& get(std::variant<Types...>&& var) {
if (auto* v = std::get_if<I>(&var)) return std::move(*v);
throw std::runtime_error{"Bad variant access -- variant does not contain the requested type"};
}
template <size_t I, size_t... More, class Visitor, class Variant>
constexpr auto visit_helper(Visitor&& vis, Variant&& var) {
if (var.index() == I)
return vis(var::get<I>(std::forward<Variant>(var)));
else if constexpr (sizeof...(More) > 0)
return visit_helper<More...>(std::forward<Visitor>(vis), std::forward<Variant>(var));
else
throw std::runtime_error{"Bad visit -- variant is valueless"};
}
template <size_t... Is, class Visitor, class Variant>
constexpr auto visit_helper(Visitor&& vis, Variant&& var, std::index_sequence<Is...>) {
return visit_helper<Is...>(std::forward<Visitor>(vis), std::forward<Variant>(var));
}
// Only handle a single variant here because multi-variant invocation is notably harder (and we
// don't need it).
template <class Visitor, class Variant>
constexpr auto visit(Visitor&& vis, Variant&& var) {
return visit_helper(std::forward<Visitor>(vis), std::forward<Variant>(var),
std::make_index_sequence<std::variant_size_v<std::remove_reference_t<Variant>>>{});
}
} // namespace var
#endif

View File

@ -99,7 +99,7 @@ void LokiMQ::worker_thread(unsigned int index, std::optional<std::string> tagged
try {
if (run.is_batch_job) {
auto* batch = std::get<detail::Batch*>(run.to_run);
auto* batch = var::get<detail::Batch*>(run.to_run);
if (run.batch_jobno >= 0) {
LMQ_TRACE("worker thread ", worker_id, " running batch ", batch, "#", run.batch_jobno);
batch->run_job(run.batch_jobno);
@ -108,7 +108,7 @@ void LokiMQ::worker_thread(unsigned int index, std::optional<std::string> tagged
batch->job_completion();
}
} else if (run.is_injected) {
auto& func = std::get<std::function<void()>>(run.to_run);
auto& func = var::get<std::function<void()>>(run.to_run);
LMQ_TRACE("worker thread ", worker_id, " invoking injected command ", run.command);
func();
func = nullptr;
@ -120,7 +120,7 @@ void LokiMQ::worker_thread(unsigned int index, std::optional<std::string> tagged
LMQ_TRACE("Got incoming command from ", message.remote, "/", message.conn, message.conn.route.empty() ? " (outgoing)" : " (incoming)");
auto& [callback, is_request] = *std::get<const std::pair<CommandCallback, bool>*>(run.to_run);
auto& [callback, is_request] = *var::get<const std::pair<CommandCallback, bool>*>(run.to_run);
if (is_request) {
message.reply_tag = {run.data_parts[0].data<char>(), run.data_parts[0].size()};
for (auto it = run.data_parts.begin() + 1; it != run.data_parts.end(); ++it)
@ -137,9 +137,11 @@ void LokiMQ::worker_thread(unsigned int index, std::optional<std::string> tagged
catch (const bt_deserialize_invalid& e) {
LMQ_LOG(warn, worker_id, " deserialization failed: ", e.what(), "; ignoring request");
}
#ifndef BROKEN_APPLE_VARIANT
catch (const std::bad_variant_access& e) {
LMQ_LOG(warn, worker_id, " deserialization failed: found unexpected serialized type (", e.what(), "); ignoring request");
}
#endif
catch (const std::out_of_range& e) {
LMQ_LOG(warn, worker_id, " deserialization failed: invalid data - required field missing (", e.what(), "); ignoring request");
}
@ -208,7 +210,7 @@ void LokiMQ::proxy_worker_message(std::vector<zmq::message_t>& parts) {
active--;
}
bool clear_job = false;
auto* batch = std::get<detail::Batch*>(run.to_run);
auto* batch = var::get<detail::Batch*>(run.to_run);
if (run.batch_jobno == -1) {
// Returned from the completion function
clear_job = true;

View File

@ -6,6 +6,16 @@ using namespace lokimq;
static auto startup = std::chrono::steady_clock::now();
/// Returns a localhost connection string to listen on. It can be considered random, though in
/// practice in the current implementation is sequential starting at 4500.
inline std::string random_localhost() {
static uint16_t last = 4499;
last++;
assert(last); // We should never call this enough to overflow
return "tcp://127.0.0.1:" + std::to_string(last);
}
/// Waits up to 100ms for something to happen.
template <typename Func>
inline void wait_for(Func f) {

View File

@ -125,10 +125,10 @@ TEST_CASE("bt_value serialization", "[bt][serialization][bt_value]") {
TEST_CASE("bt_value deserialization", "[bt][deserialization][bt_value]") {
auto dna1 = bt_deserialize<bt_value>("i42e");
auto dna2 = bt_deserialize<bt_value>("i-42e");
REQUIRE( std::get<uint64_t>(dna1) == 42 );
REQUIRE( std::get<int64_t>(dna2) == -42 );
REQUIRE_THROWS( std::get<int64_t>(dna1) );
REQUIRE_THROWS( std::get<uint64_t>(dna2) );
REQUIRE( var::get<uint64_t>(dna1) == 42 );
REQUIRE( var::get<int64_t>(dna2) == -42 );
REQUIRE_THROWS( var::get<int64_t>(dna1) );
REQUIRE_THROWS( var::get<uint64_t>(dna2) );
REQUIRE( lokimq::get_int<int>(dna1) == 42 );
REQUIRE( lokimq::get_int<int>(dna2) == -42 );
REQUIRE( lokimq::get_int<unsigned>(dna1) == 42 );
@ -136,19 +136,19 @@ TEST_CASE("bt_value deserialization", "[bt][deserialization][bt_value]") {
bt_value x = bt_deserialize<bt_value>("d3:barle3:foold1:ali1ei2ei3ee1:bleed1:cli-5ei4eeeee");
REQUIRE( std::holds_alternative<bt_dict>(x) );
bt_dict& a = std::get<bt_dict>(x);
bt_dict& a = var::get<bt_dict>(x);
REQUIRE( a.count("bar") );
REQUIRE( a.count("foo") );
REQUIRE( a.size() == 2 );
bt_list& foo = std::get<bt_list>(a["foo"]);
bt_list& foo = var::get<bt_list>(a["foo"]);
REQUIRE( foo.size() == 2 );
bt_dict& foo1 = std::get<bt_dict>(foo.front());
bt_dict& foo2 = std::get<bt_dict>(foo.back());
bt_dict& foo1 = var::get<bt_dict>(foo.front());
bt_dict& foo2 = var::get<bt_dict>(foo.back());
REQUIRE( foo1.size() == 2 );
REQUIRE( foo2.size() == 1 );
bt_list& foo1a = std::get<bt_list>(foo1.at("a"));
bt_list& foo1b = std::get<bt_list>(foo1.at("b"));
bt_list& foo2c = std::get<bt_list>(foo2.at("c"));
bt_list& foo1a = var::get<bt_list>(foo1.at("a"));
bt_list& foo1b = var::get<bt_list>(foo1.at("b"));
bt_list& foo2c = var::get<bt_list>(foo2.at("c"));
std::list<int> foo1a_vals, foo1b_vals, foo2c_vals;
for (auto& v : foo1a) foo1a_vals.push_back(lokimq::get_int<int>(v));
for (auto& v : foo1b) foo1b_vals.push_back(lokimq::get_int<int>(v));
@ -157,7 +157,7 @@ TEST_CASE("bt_value deserialization", "[bt][deserialization][bt_value]") {
REQUIRE( foo1b_vals == std::list<int>{} );
REQUIRE( foo2c_vals == std::list{{-5, 4}} );
REQUIRE( std::get<bt_list>(a.at("bar")).empty() );
REQUIRE( var::get<bt_list>(a.at("bar")).empty() );
}
TEST_CASE("bt tuple serialization", "[bt][tuple][serialization]") {

View File

@ -6,7 +6,7 @@
using namespace lokimq;
TEST_CASE("basic commands", "[commands]") {
std::string listen = "tcp://127.0.0.1:4567";
std::string listen = random_localhost();
LokiMQ server{
"", "", // generate ephemeral keys
false, // not a service node
@ -76,7 +76,7 @@ TEST_CASE("basic commands", "[commands]") {
}
TEST_CASE("outgoing auth level", "[commands][auth]") {
std::string listen = "tcp://127.0.0.1:4567";
std::string listen = random_localhost();
LokiMQ server{
"", "", // generate ephemeral keys
false, // not a service node
@ -158,7 +158,7 @@ TEST_CASE("deferred replies on incoming connections", "[commands][hey google]")
// Tests that the ConnectionID from a Message can be stored and reused later to contact the
// original node.
std::string listen = "tcp://127.0.0.1:4567";
std::string listen = random_localhost();
LokiMQ server{
"", "", // generate ephemeral keys
false, // not a service node
@ -270,7 +270,7 @@ TEST_CASE("deferred replies on incoming connections", "[commands][hey google]")
}
TEST_CASE("send failure callbacks", "[commands][queue_full]") {
std::string listen = "tcp://127.0.0.1:4567";
std::string listen = random_localhost();
LokiMQ server{
"", "", // generate ephemeral keys
false, // not a service node
@ -364,7 +364,7 @@ TEST_CASE("send failure callbacks", "[commands][queue_full]") {
}
TEST_CASE("data parts", "[send][data_parts]") {
std::string listen = "tcp://127.0.0.1:4567";
std::string listen = random_localhost();
LokiMQ server{
"", "", // generate ephemeral keys
false, // not a service node

View File

@ -6,7 +6,7 @@ extern "C" {
TEST_CASE("connections with curve authentication", "[curve][connect]") {
std::string listen = "tcp://127.0.0.1:4455";
std::string listen = random_localhost();
LokiMQ server{
"", "", // generate ephemeral keys
false, // not a service node
@ -53,16 +53,17 @@ TEST_CASE("self-connection SN optimization", "[connect][self]") {
pubkey.resize(crypto_box_PUBLICKEYBYTES);
privkey.resize(crypto_box_SECRETKEYBYTES);
REQUIRE(sodium_init() != -1);
auto listen_addr = random_localhost();
crypto_box_keypair(reinterpret_cast<unsigned char*>(&pubkey[0]), reinterpret_cast<unsigned char*>(&privkey[0]));
LokiMQ sn{
pubkey, privkey,
true,
[&](auto pk) { if (pk == pubkey) return "tcp://127.0.0.1:5544"; else return ""; },
[&](auto pk) { if (pk == pubkey) return listen_addr; else return ""s; },
get_logger(""),
LogLevel::trace
};
sn.listen_curve("tcp://127.0.0.1:5544", [&](auto ip, auto pk, auto sn) {
sn.listen_curve(listen_addr, [&](auto ip, auto pk, auto sn) {
auto lock = catch_lock();
REQUIRE(ip == "127.0.0.1");
REQUIRE(sn == (pk == pubkey));
@ -90,7 +91,7 @@ TEST_CASE("self-connection SN optimization", "[connect][self]") {
}
TEST_CASE("plain-text connections", "[plaintext][connect]") {
std::string listen = "tcp://127.0.0.1:4455";
std::string listen = random_localhost();
LokiMQ server{get_logger(""), LogLevel::trace};
server.add_category("public", Access{AuthLevel::none});
@ -129,7 +130,7 @@ TEST_CASE("plain-text connections", "[plaintext][connect]") {
}
TEST_CASE("unique connection IDs", "[connect][id]") {
std::string listen = "tcp://127.0.0.1:4455";
std::string listen = random_localhost();
LokiMQ server{get_logger(""), LogLevel::trace};
ConnectionID first, second;
@ -195,7 +196,7 @@ TEST_CASE("SN disconnections", "[connect][disconnect]") {
pubkey[i].resize(crypto_box_PUBLICKEYBYTES);
privkey[i].resize(crypto_box_SECRETKEYBYTES);
crypto_box_keypair(reinterpret_cast<unsigned char*>(&pubkey[i][0]), reinterpret_cast<unsigned char*>(&privkey[i][0]));
conn.emplace(pubkey[i], "tcp://127.0.0.1:" + std::to_string(4450 + i));
conn.emplace(pubkey[i], random_localhost());
}
std::atomic<int> his{0};
for (int i = 0; i < pubkey.size(); i++) {
@ -231,7 +232,7 @@ TEST_CASE("SN auth checks", "[sandwich][auth]") {
// isn't recognized as a SN but tries to invoke a SN command it'll be told to disconnect; if it
// tries to send again it should reconnect and reauthenticate. This test is meant to test this
// pattern where the reconnection/reauthentication now authenticates it as a SN.
std::string listen = "tcp://127.0.0.1:4455";
std::string listen = random_localhost();
std::string pubkey, privkey;
pubkey.resize(crypto_box_PUBLICKEYBYTES);
privkey.resize(crypto_box_SECRETKEYBYTES);
@ -350,7 +351,7 @@ TEST_CASE("SN auth checks", "[sandwich][auth]") {
TEST_CASE("SN single worker test", "[connect][worker]") {
// Tests a failure case that could trigger when all workers are allocated (here we make that
// simpler by just having one worker).
std::string listen = "tcp://127.0.0.1:4455";
std::string listen = random_localhost();
LokiMQ server{
"", "",
false, // service node

View File

@ -6,7 +6,7 @@
using namespace lokimq;
TEST_CASE("failure responses - UNKNOWNCOMMAND", "[failure][UNKNOWNCOMMAND]") {
std::string listen = "tcp://127.0.0.1:4567";
std::string listen = random_localhost();
LokiMQ server{
"", "", // generate ephemeral keys
false, // not a service node
@ -47,7 +47,7 @@ TEST_CASE("failure responses - UNKNOWNCOMMAND", "[failure][UNKNOWNCOMMAND]") {
}
TEST_CASE("failure responses - NO_REPLY_TAG", "[failure][NO_REPLY_TAG]") {
std::string listen = "tcp://127.0.0.1:4567";
std::string listen = random_localhost();
LokiMQ server{
"", "", // generate ephemeral keys
false, // not a service node
@ -108,7 +108,7 @@ TEST_CASE("failure responses - NO_REPLY_TAG", "[failure][NO_REPLY_TAG]") {
}
TEST_CASE("failure responses - FORBIDDEN", "[failure][FORBIDDEN]") {
std::string listen = "tcp://127.0.0.1:4567";
std::string listen = random_localhost();
LokiMQ server{
"", "", // generate ephemeral keys
false, // not a service node
@ -191,7 +191,7 @@ TEST_CASE("failure responses - FORBIDDEN", "[failure][FORBIDDEN]") {
}
TEST_CASE("failure responses - NOT_A_SERVICE_NODE", "[failure][NOT_A_SERVICE_NODE]") {
std::string listen = "tcp://127.0.0.1:4567";
std::string listen = random_localhost();
LokiMQ server{
"", "", // generate ephemeral keys
false, // not a service node
@ -258,7 +258,7 @@ TEST_CASE("failure responses - NOT_A_SERVICE_NODE", "[failure][NOT_A_SERVICE_NOD
}
TEST_CASE("failure responses - FORBIDDEN_SN", "[failure][FORBIDDEN_SN]") {
std::string listen = "tcp://127.0.0.1:4567";
std::string listen = random_localhost();
LokiMQ server{
"", "", // generate ephemeral keys
false, // not a service node

View File

@ -3,7 +3,7 @@
using namespace lokimq;
TEST_CASE("injected external commands", "[injected]") {
std::string listen = "tcp://127.0.0.1:4567";
std::string listen = random_localhost();
LokiMQ server{
"", "", // generate ephemeral keys
false, // not a service node

View File

@ -4,7 +4,7 @@
using namespace lokimq;
TEST_CASE("basic requests", "[requests]") {
std::string listen = "tcp://127.0.0.1:5678";
std::string listen = random_localhost();
LokiMQ server{
"", "", // generate ephemeral keys
false, // not a service node
@ -61,7 +61,7 @@ TEST_CASE("basic requests", "[requests]") {
}
TEST_CASE("request from server to client", "[requests]") {
std::string listen = "tcp://127.0.0.1:5678";
std::string listen = random_localhost();
LokiMQ server{
"", "", // generate ephemeral keys
false, // not a service node
@ -124,7 +124,7 @@ TEST_CASE("request from server to client", "[requests]") {
}
TEST_CASE("request timeouts", "[requests][timeout]") {
std::string listen = "tcp://127.0.0.1:5678";
std::string listen = random_localhost();
LokiMQ server{
"", "", // generate ephemeral keys
false, // not a service node