16#include "../attributes.hpp"
17#include "../core/completion.hpp"
19#include "../core/executor.hpp"
20#include "../core/observability.hpp"
23namespace morph::offline {
56 template <typename Count>
57 requires(std::integral<Count> && !std::same_as<std::remove_cv_t<Count>,
bool> &&
58 sizeof(Count) < sizeof(std::uint64_t))
59 constexpr
Attempts(Count count) noexcept : _value{
static_cast<std::uint32_t
>(count)} {}
63 [[nodiscard]]
constexpr std::uint32_t
value() const noexcept {
return _value; }
71 std::uint32_t _value{0};
74static_assert(!std::is_constructible_v<Attempts, std::uint64_t>,
75 "Attempts must not be constructible from a queue item id, or setAttempts()'s two "
76 "parameters become mutually convertible again and a transposed call compiles.");
77static_assert(std::is_convertible_v<std::uint32_t, Attempts>,
78 "Attempts must stay implicitly constructible from a narrow count, so existing "
79 "setAttempts(id, n) call sites keep reading the way they did.");
149 : std::runtime_error(
"IOfflineQueue: enqueue rejected, queue is at capacity (" +
150 std::to_string(currentSizeValue) +
"/" + std::to_string(maxDepthValue) +
")"),
184 [[nodiscard]]
virtual uint64_t
enqueue(std::string payload) = 0;
245#pragma GCC diagnostic push
246#pragma GCC diagnostic ignored "-Woverloaded-virtual"
247 [[nodiscard]]
virtual uint64_t
enqueue(std::string payload, std::string idempotencyKey) {
248 const uint64_t itemId =
enqueue(std::move(payload));
252#pragma GCC diagnostic pop
275 [[nodiscard]]
virtual std::vector<QueueItem>
drain()
const = 0;
289 [[nodiscard]]
virtual std::size_t
size()
const {
return drain().size(); }
296 [[nodiscard]]
virtual std::optional<std::size_t>
maxDepth()
const {
return std::nullopt; }
326 std::string payload, std::string idempotencyKey = {}) {
327 return askEnqueue(replyExec, std::move(payload), std::move(idempotencyKey));
360 std::string idempotencyKey) {
361 return answerHere<uint64_t>(replyExec, [&] {
return enqueue(std::move(payload), std::move(idempotencyKey)); });
368 [[nodiscard]] virtual ::morph::async::Completion<std::vector<QueueItem>>
askDrain(
370 return answerHere<std::vector<QueueItem>>(replyExec, [&] {
return drain(); });
378 return answerHere<std::size_t>(replyExec, [&] {
return size(); });
389 virtual void setIdempotencyKey([[maybe_unused]] uint64_t itemId, [[maybe_unused]] std::string idempotencyKey) {}
393 template <
typename T,
typename Body>
397 settleable.second.resolve(std::forward<Body>(body)());
399 settleable.second.reject(std::current_exception());
401 return std::move(settleable.first);
413 "setAttempts's parameters must not be transposable: writing an item id into an attempt count is "
414 "silent, because setAttempts on an unknown id is a documented no-op.");
416 "setAttempts must still accept a plain count in the right order.");
438 std::optional<std::size_t>
maxDepth = std::nullopt)
439 : _maxDepth{
maxDepth}, _owned{owner, std::make_shared<State>()} {}
444 [[nodiscard]] uint64_t
enqueue(std::string payload)
override {
return enqueue(std::move(payload), {}); }
452 [[nodiscard]] uint64_t
enqueue(std::string payload, std::string idempotencyKey)
override {
453 return insert(_owned.read(
"InMemoryOfflineQueue::enqueue"), _maxDepth, std::move(payload),
454 std::move(idempotencyKey));
459 [[nodiscard]] std::vector<QueueItem>
drain()
const override {
460 auto const& items = _owned.read(
"InMemoryOfflineQueue::drain").items;
461 return std::vector<QueueItem>{items.begin(), items.end()};
466 [[nodiscard]] std::size_t
size()
const override {
return _owned.read(
"InMemoryOfflineQueue::size").items.size(); }
471 [[nodiscard]] std::optional<std::size_t>
maxDepth()
const override {
return _maxDepth; }
478 _owned.apply(
"InMemoryOfflineQueue::markDone", [itemId](State& state) {
480 std::ranges::find_if(state.items, [itemId](
const QueueItem& item) { return item.id == itemId; });
481 if (iter != state.items.end()) {
482 state.items.erase(iter);
497 _owned.apply(
"InMemoryOfflineQueue::setAttempts", [itemId, attempts](State& state) {
499 std::ranges::find_if(state.items, [itemId](
const QueueItem& item) { return item.id == itemId; });
500 if (iter != state.items.end()) {
501 iter->attempts = attempts.
value();
514 std::string idempotencyKey)
override {
515 return _owned.ask<uint64_t>(
"InMemoryOfflineQueue::enqueue", replyExec,
516 [
maxDepth = _maxDepth, payload = std::move(payload),
517 idempotencyKey = std::move(idempotencyKey)](State& state)
mutable {
518 return insert(state,
maxDepth, std::move(payload), std::move(idempotencyKey));
527 return _owned.ask<std::vector<QueueItem>>(
"InMemoryOfflineQueue::drain", replyExec, [](State& state) {
528 return std::vector<QueueItem>{state.items.begin(), state.items.end()};
536 return _owned.ask<std::size_t>(
"InMemoryOfflineQueue::size", replyExec,
537 [](State& state) {
return state.items.size(); });
543 std::deque<QueueItem> items;
548 static uint64_t insert(State& state, std::optional<std::size_t>
maxDepth, std::string payload,
549 std::string idempotencyKey) {
552 ::morph::observe::detail::emitMetric(::morph::observe::Metric::queueOverflow,
553 static_cast<double>(state.items.size()));
554 throw OfflineQueueFullError{*
maxDepth, state.items.size()};
556 uint64_t
const itemId = ++state.nextId;
557 state.items.push_back(
558 QueueItem{.id = itemId, .payload = std::move(payload), .idempotencyKey = std::move(idempotencyKey)});
562 std::optional<std::size_t> _maxDepth;
563 ::morph::exec::detail::OwnedState<State> _owned;
Move-only handle representing the eventual result of an asynchronous operation.
static std::pair< Completion< T >, Promise > makeSettleable(::morph::exec::IExecutor *execPtr)
Constructs a Completion<T>/Promise<T> pair sharing one settleable state.
A replay attempt count, as a type distinct from a queue item id.
constexpr Attempts() noexcept=default
Constructs a zero attempt count.
constexpr bool operator==(const Attempts &other) const noexcept=default
Compares two attempt counts.
constexpr std::uint32_t value() const noexcept
Returns the wrapped count.
uint64_t enqueue(std::string payload, std::string idempotencyKey) override
Appends payload carrying idempotencyKey and returns a monotonically increasing id....
::morph::async::Completion< uint64_t > askEnqueue(::morph::exec::IExecutor &replyExec, std::string payload, std::string idempotencyKey) override
Enqueues on the owner, answered on replyExec.
std::vector< QueueItem > drain() const override
Returns a snapshot of all pending items. On the owner.
std::size_t size() const override
Returns the number of pending items. On the owner.
InMemoryOfflineQueue(::morph::exec::IExecutor &owner, std::optional< std::size_t > maxDepth=std::nullopt)
Constructs an in-memory queue belonging to owner, optionally bounded.
::morph::async::Completion< std::size_t > askSize(::morph::exec::IExecutor &replyExec) const override
Counts on the owner, answered on replyExec.
void setAttempts(uint64_t itemId, Attempts attempts) override
Updates the persisted attempt count on the in-deque item with itemId, on the owner....
void markDone(uint64_t itemId) override
Removes the item with itemId from the queue, on the owner.
uint64_t enqueue(std::string payload) override
Appends payload and returns a monotonically increasing id. On the owner.
::morph::async::Completion< std::vector< QueueItem > > askDrain(::morph::exec::IExecutor &replyExec) const override
Snapshots on the owner, answered on replyExec.
std::optional< std::size_t > maxDepth() const override
Returns the configured maximum depth, or std::nullopt if unbounded. Callable anywhere: fixed at const...
OwnedState: a component's state, and the one executor it is touched on.
Compile-time profiler instrumentation: Tracy zones, plots, messages and thread names,...
#define MORPH_ZONE(name)
Opens a profiler zone named name that ends with the enclosing scope. At most one per scope.
Abstract executor interface.
Interface for durable storage of actions that could not be delivered.
virtual void markDone(uint64_t itemId)=0
Removes the item identified by itemId.
virtual ::morph::async::Completion< std::vector< QueueItem > > askDrain(::morph::exec::IExecutor &replyExec) const
What drain(replyExec) asks. The default drains where it is called; one with an owner drains there.
::morph::async::Completion< std::vector< QueueItem > > drain(::morph::exec::IExecutor &replyExec) const
Snapshots the pending items on the queue's owner; delivered on replyExec.
virtual uint64_t enqueue(std::string payload)=0
Appends payload to the queue with no idempotency key.
virtual std::optional< std::size_t > maxDepth() const
Returns the configured maximum depth, or std::nullopt if unbounded.
::morph::async::Completion< uint64_t > enqueue(::morph::exec::IExecutor &replyExec, std::string payload, std::string idempotencyKey={})
Appends payload carrying idempotencyKey on the queue's owner; the new item's id is delivered on reply...
::morph::async::Completion< std::size_t > size(::morph::exec::IExecutor &replyExec) const
Counts the pending items on the queue's owner; delivered on replyExec.
virtual ::morph::async::Completion< std::size_t > askSize(::morph::exec::IExecutor &replyExec) const
What size(replyExec) asks. The default counts where it is called; one with an owner counts there.
virtual ::morph::async::Completion< uint64_t > askEnqueue(::morph::exec::IExecutor &replyExec, std::string payload, std::string idempotencyKey)
What enqueue(replyExec, payload, key) asks. The default enqueues where it is called,...
virtual void setIdempotencyKey(uint64_t itemId, std::string idempotencyKey)
Stamps an idempotency key onto an already-enqueued item.
virtual std::size_t size() const
Returns the number of pending items without removing them.
virtual std::vector< QueueItem > drain() const =0
Returns all pending items in enqueue order without removing them.
virtual uint64_t enqueue(std::string payload, std::string idempotencyKey)
Appends payload carrying the cross-subsystem idempotencyKey.
virtual void setAttempts(uint64_t itemId, Attempts attempts)
Persists an updated attempt count for an item. Default: no-op.
OfflineQueueFullError(std::size_t maxDepthValue, std::size_t currentSizeValue)
std::size_t currentSize
Number of pending items at the time of rejection.
std::size_t maxDepth
The configured capacity that was reached.
An item stored in the offline queue.
uint32_t attempts
Durable retry count for this item, authoritative when the queue persists it.
std::string payload
Opaque serialised representation of the queued action.
std::string idempotencyKey
Optional caller-supplied idempotency key that is stable across subsystems and process restarts for on...