mirror of
https://github.com/oxen-io/oxen-mq.git
synced 2023-12-13 21:00:31 +01:00
Add better deferred reply capabilities to Message
This provides an interface for sending a reply to a message later (i.e. after the Message& itself is no longer valid) by using a new `send_later()` method of the Message instance that returns an object that can properly route replies (and can outlive the Message it was called on). Intended use is: run_this_lambda_later([send=msg.send_later()] { send.reply("content"); }); which is equivalent to: run_this_lambda_later([&msg] { msg.send_reply("content"); }); except that it works properly even if the lambda is invoked beyond the lifetime of `msg`.
This commit is contained in:
parent
86247bc5c7
commit
506bd65b05
3 changed files with 126 additions and 2 deletions
|
@ -36,7 +36,7 @@ public:
|
|||
/// If you want to send a non-strong reply even when the remote is a service node then add
|
||||
/// an explicit `send_option::optional()` argument.
|
||||
template <typename... Args>
|
||||
void send_back(std::string_view, Args&&... args);
|
||||
void send_back(std::string_view command, Args&&... args);
|
||||
|
||||
/// Sends a reply to a request. This takes no command: the command is always the built-in
|
||||
/// "REPLY" command, followed by the unique reply tag, then any reply data parts. All other
|
||||
|
@ -51,7 +51,47 @@ public:
|
|||
/// Sends a request back to whomever sent this message. This is effectively a wrapper around
|
||||
/// lmq.request() that takes care of setting up the recipient arguments.
|
||||
template <typename ReplyCallback, typename... Args>
|
||||
void send_request(std::string_view cmd, ReplyCallback&& callback, Args&&... args);
|
||||
void send_request(std::string_view command, ReplyCallback&& callback, Args&&... args);
|
||||
|
||||
/** Class returned by `send_later()` that can be used to call `back()`, `reply()`, or
|
||||
* `request()` beyond the lifetime of the Message instance as if calling `msg.send_back()`,
|
||||
* `msg.send_reply()`, or `msg.send_request()`. For example:
|
||||
*
|
||||
* auto send = msg.send_later();
|
||||
* // ... later, perhaps in a lambda or scheduled job:
|
||||
* send.reply("content");
|
||||
*
|
||||
* is equivalent to
|
||||
*
|
||||
* msg.send_reply("content");
|
||||
*
|
||||
* except that it is valid even if `msg` is no longer valid.
|
||||
*/
|
||||
class DeferredSend {
|
||||
public:
|
||||
OxenMQ& oxenmq; ///< The owning OxenMQ object
|
||||
ConnectionID conn; ///< The connection info for routing a reply; also contains the pubkey/sn status
|
||||
std::string reply_tag; ///< If the invoked command is a request command this is the required reply tag that will be prepended by `reply()`.
|
||||
|
||||
explicit DeferredSend(Message& m) : oxenmq{m.oxenmq}, conn{m.conn}, reply_tag{m.reply_tag} {}
|
||||
|
||||
/// Equivalent to msg.send_back(...), but can be invoked later.
|
||||
template <typename... Args>
|
||||
void back(std::string_view command, Args&&... args) const;
|
||||
|
||||
/// Equivalent to msg.send_reply(...), but can be invoked later.
|
||||
template <typename... Args>
|
||||
void reply(Args&&... args) const;
|
||||
|
||||
/// Equivalent to msg.send_request(...), but can be invoked later.
|
||||
template <typename ReplyCallback, typename... Args>
|
||||
void request(std::string_view command, ReplyCallback&& callback, Args&&... args) const;
|
||||
};
|
||||
|
||||
/// Returns a DeferredSend object that can be used to send replies to this message even if the
|
||||
/// message expires. Typically this is used when sending a reply requires waiting on another
|
||||
/// task to complete without needing to block the handler thread.
|
||||
DeferredSend send_later() { return DeferredSend{*this}; }
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -1489,18 +1489,32 @@ template <typename... Args>
|
|||
void Message::send_back(std::string_view command, Args&&... args) {
|
||||
oxenmq.send(conn, command, send_option::optional{!conn.sn()}, std::forward<Args>(args)...);
|
||||
}
|
||||
template <typename... Args>
|
||||
void Message::DeferredSend::back(std::string_view command, Args&&... args) const {
|
||||
oxenmq.send(conn, command, send_option::optional{!conn.sn()}, std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
template <typename... Args>
|
||||
void Message::send_reply(Args&&... args) {
|
||||
assert(!reply_tag.empty());
|
||||
oxenmq.send(conn, "REPLY", reply_tag, send_option::optional{!conn.sn()}, std::forward<Args>(args)...);
|
||||
}
|
||||
template <typename... Args>
|
||||
void Message::DeferredSend::reply(Args&&... args) const {
|
||||
assert(!reply_tag.empty());
|
||||
oxenmq.send(conn, "REPLY", reply_tag, send_option::optional{!conn.sn()}, std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
template <typename Callback, typename... Args>
|
||||
void Message::send_request(std::string_view cmd, Callback&& callback, Args&&... args) {
|
||||
oxenmq.request(conn, cmd, std::forward<Callback>(callback),
|
||||
send_option::optional{!conn.sn()}, std::forward<Args>(args)...);
|
||||
}
|
||||
template <typename Callback, typename... Args>
|
||||
void Message::DeferredSend::request(std::string_view cmd, Callback&& callback, Args&&... args) const {
|
||||
oxenmq.request(conn, cmd, std::forward<Callback>(callback),
|
||||
send_option::optional{!conn.sn()}, std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
// When log messages are invoked we strip out anything before this in the filename:
|
||||
constexpr std::string_view LOG_PREFIX{"oxenmq/", 7};
|
||||
|
|
|
@ -437,3 +437,73 @@ TEST_CASE("data parts", "[send][data_parts]") {
|
|||
REQUIRE( r == expected );
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("deferred replies", "[send][deferred]") {
|
||||
std::string listen = random_localhost();
|
||||
OxenMQ server{
|
||||
"", "", // generate ephemeral keys
|
||||
false, // not a service node
|
||||
[](auto) { return ""; },
|
||||
};
|
||||
server.listen_curve(listen);
|
||||
|
||||
std::atomic<int> hellos{0}, his{0};
|
||||
|
||||
server.add_category("public", Access{AuthLevel::none});
|
||||
server.add_request_command("public", "echo", [&](Message& m) {
|
||||
std::string msg = m.data.empty() ? ""s : std::string{m.data.front()};
|
||||
std::thread t{[send=m.send_later(), msg=std::move(msg)] {
|
||||
{ auto lock = catch_lock(); INFO("sleeping"); }
|
||||
std::this_thread::sleep_for(50ms);
|
||||
{ auto lock = catch_lock(); INFO("sending"); }
|
||||
send.reply(msg);
|
||||
}};
|
||||
t.detach();
|
||||
});
|
||||
server.set_general_threads(1);
|
||||
server.start();
|
||||
|
||||
OxenMQ client(
|
||||
[](LogLevel, const char* file, int line, std::string msg) { std::cerr << file << ":" << line << " --C-- " << msg << "\n"; }
|
||||
);
|
||||
//client.log_level(LogLevel::trace);
|
||||
|
||||
client.start();
|
||||
|
||||
std::atomic<bool> connected{false}, failed{false};
|
||||
std::string pubkey;
|
||||
|
||||
auto c = client.connect_remote(address{listen, server.get_pubkey()},
|
||||
[&](auto conn) { pubkey = conn.pubkey(); connected = true; },
|
||||
[&](auto, auto) { failed = true; });
|
||||
|
||||
wait_for([&] { return connected || failed; });
|
||||
{
|
||||
auto lock = catch_lock();
|
||||
REQUIRE( connected );
|
||||
REQUIRE_FALSE( failed );
|
||||
REQUIRE( to_hex(pubkey) == to_hex(server.get_pubkey()) );
|
||||
}
|
||||
|
||||
std::unordered_set<std::string> replies;
|
||||
std::mutex reply_mut;
|
||||
std::vector<std::string> data;
|
||||
for (auto str : {"hello", "world", "omg"})
|
||||
client.request(c, "public.echo", [&](bool ok, std::vector<std::string> data_) {
|
||||
std::lock_guard lock{reply_mut};
|
||||
replies.insert(std::move(data_[0]));
|
||||
}, str);
|
||||
|
||||
reply_sleep();
|
||||
{
|
||||
std::lock_guard lq{reply_mut};
|
||||
auto lock = catch_lock();
|
||||
REQUIRE( replies.size() == 0 ); // The server waits 50ms before sending, so we shouldn't have any reply yet
|
||||
}
|
||||
std::this_thread::sleep_for(60ms); // We're at least 70ms in now so the 50ms-delayed server responses should have arrived
|
||||
{
|
||||
std::lock_guard lq{reply_mut};
|
||||
auto lock = catch_lock();
|
||||
REQUIRE( replies == std::unordered_set<std::string>{{"hello", "world", "omg"}} );
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue