morph
0.1.0
Asynchronous bridge between GUI thread and business models
Toggle main menu visibility
Loading...
Searching...
No Matches
outbox.hpp
1
// SPDX-License-Identifier: Apache-2.0
2
3
#pragma once
4
#include <cstddef>
5
#include <functional>
6
#include <memory>
7
#include <span>
8
#include <stdexcept>
9
#include <vector>
10
11
#include "../core/logger.hpp"
12
#include "action_log.hpp"
13
14
namespace
morph::journal {
15
16
/// @brief Thrown by `OutboxRelay::relay()` when `sink` is null.
17
///
18
/// A null `drainOutbox`/`markRelayed` already throws a catchable
19
/// `std::bad_function_call` (invoking a null `std::function`); this makes a
20
/// null `sink` consistent with that instead of a raw null-`shared_ptr`
21
/// dereference, which is real undefined behavior and not portably catchable.
22
struct
NullSinkError
: std::runtime_error {
23
using
std::runtime_error::runtime_error;
24
};
25
26
/// @brief Outcome of one `OutboxRelay::relay()` call.
27
struct
OutboxRelayResult
{
28
/// @brief Outbox rows drained, appended to `OutboxRelay::sink`, and marked
29
/// relayed in this call. Includes rows the sink silently deduped via
30
/// `LogEntry::idempotencyKey` — a row is still marked relayed exactly
31
/// once even when the sink treats its append as a no-op.
32
std::size_t
relayed
= 0;
33
};
34
35
/// @brief Moves committed-but-unrelayed rows from a model's own outbox table to
36
/// a durable `IActionLog`, then marks them relayed in the model's own
37
/// store.
38
///
39
/// Closes the "two independent writes" gap between a model's own transactional
40
/// store and the action log: a store-backed model writes its business tables
41
/// *and* an outbox row (shaped like `LogEntry`, including a stable
42
/// `idempotencyKey`) in one local transaction — see
43
/// `IModelHolder::setOutboxManaged` — and `OutboxRelay` is the separate,
44
/// asynchronous step that moves that row into the real durable sink.
45
///
46
/// `drainOutbox`/`markRelayed` are injected callables against the model's own
47
/// store, exactly as `morph::offline::ReconnectCoordinator::Deps` and
48
/// `morph::offline::SyncWorker::ReplayFunction` inject their side effects —
49
/// `morph::journal` never touches the model's database. Mirroring
50
/// `ReconnectCoordinator::Deps`, a null member is logged (via
51
/// `morph::log::logError`) at the start of every `relay()` call but is not
52
/// rejected — invoking a null member still throws (`std::bad_function_call`) or
53
/// crashes as usual.
54
///
55
/// @par Crash safety
56
/// `relay()` is idempotent and safe to call repeatedly: it appends every row
57
/// `drainOutbox()` currently reports to `sink`, flushes `sink`, and only then
58
/// calls `markRelayed` with the whole batch. A crash between the sink append and
59
/// `markRelayed` committing simply means the next `relay()` call sees the same
60
/// row again from `drainOutbox()` — appending it again is a safe no-op as long
61
/// as `sink` dedups by `LogEntry::idempotencyKey` (`InMemoryActionLog` and
62
/// `FileActionLog` do this out of the box — see their class docs; `SessionLog`
63
/// deliberately does not, since its contract is full fidelity with nothing
64
/// dropped). This is at-least-once-plus-dedup, not a two-phase commit: `sink`
65
/// and the model's own store are never committed as a single distributed
66
/// transaction.
67
struct
OutboxRelay
{
68
/// @brief Pulls committed-but-unrelayed rows from the model's own store.
69
/// Must be non-destructive: the same rows are re-drained until
70
/// `markRelayed` records them, including across process restarts.
71
std::function<std::vector<LogEntry>()>
drainOutbox
;
72
73
/// @brief Marks @p rows relayed in the model's own store (by identity or
74
/// `LogEntry::idempotencyKey`) so a later `drainOutbox()` call no
75
/// longer returns them.
76
std::function<void(std::span<const LogEntry>)>
markRelayed
;
77
78
/// @brief Durable audit sink rows are forwarded to.
79
std::shared_ptr<IActionLog>
sink
;
80
81
/// @brief Moves every row `drainOutbox()` currently reports to `sink`, then
82
/// marks the whole batch relayed via `markRelayed`.
83
///
84
/// A no-op (returns `{.relayed = 0}` without touching `sink` or calling
85
/// `markRelayed`) if `drainOutbox()` returns no rows.
86
///
87
/// `markRelayed` runs only after `sink->flush()` returns normally. An
88
/// `IActionLog` that cannot make the batch durable throws (see
89
/// `IActionLog::flush`), which propagates out of here *before* the rows are
90
/// marked — so they stay in the outbox and a later `relay()` retries them.
91
/// This is what keeps the relay at-least-once instead of at-most-once: were
92
/// the failure swallowed, the rows would be recorded as relayed while
93
/// nothing reached the sink, and nothing would ever surface the loss.
94
///
95
/// Runs on `sink`'s owner (see `IActionLog::owner()`): `sink->flush()`
96
/// answers only there, which is what lets its failure reach this call.
97
///
98
/// @return The number of rows relayed in this call.
99
/// @throws NullSinkError if `sink` is null and `drainOutbox()` reports at
100
/// least one row — thrown before `sink` is ever dereferenced, so
101
/// no row is lost or marked relayed. A null `sink` with nothing
102
/// to relay is still a no-op, exactly like an empty outbox with a
103
/// real `sink`: nothing would touch `sink` either way.
104
/// @throws std::exception propagated from `sink->append()` or `sink->flush()`;
105
/// the batch is left unmarked and therefore retryable.
106
OutboxRelayResult
relay
() {
107
logIfAnyDepNull();
108
auto
rows =
drainOutbox
();
109
if
(rows.empty()) {
110
return
{};
111
}
112
if
(!
sink
) {
113
throw
NullSinkError
(
"journal::OutboxRelay::relay: sink is null"
);
114
}
115
for
(
const
auto
& row : rows) {
116
sink
->append(row);
117
}
118
sink
->flush();
119
markRelayed
(rows);
120
return
OutboxRelayResult
{.relayed = rows.size()};
121
}
122
123
private
:
124
// Logs any null member, mirroring ReconnectCoordinator::assertDepsNonNull —
125
// a diagnostic, not a rejection; construction/assignment of OutboxRelay
126
// itself is never intercepted (it is a plain aggregate), so the check runs
127
// at the start of every relay() call instead.
128
void
logIfAnyDepNull()
const
{
129
if
(!
drainOutbox
) {
130
::morph::log::logError(
"[journal::OutboxRelay] null drainOutbox"
);
131
}
132
if
(!
markRelayed
) {
133
::morph::log::logError(
"[journal::OutboxRelay] null markRelayed"
);
134
}
135
if
(!
sink
) {
136
::morph::log::logError(
"[journal::OutboxRelay] null sink"
);
137
}
138
// This branch itself is unit-tested (test_outbox.cpp asserts the
139
// warning fires). relay() throws NullSinkError right after this call
140
// if sink is null and there is at least one row to relay -- a null
141
// drainOutbox/markRelayed still throws std::bad_function_call as
142
// usual, invoking a null std::function.
143
}
144
};
145
146
}
// namespace morph::journal
morph::journal::NullSinkError
Thrown by OutboxRelay::relay() when sink is null.
Definition
outbox.hpp:22
morph::journal::OutboxRelayResult
Outcome of one OutboxRelay::relay() call.
Definition
outbox.hpp:27
morph::journal::OutboxRelayResult::relayed
std::size_t relayed
Outbox rows drained, appended to OutboxRelay::sink, and marked relayed in this call....
Definition
outbox.hpp:32
morph::journal::OutboxRelay
Moves committed-but-unrelayed rows from a model's own outbox table to a durable IActionLog,...
Definition
outbox.hpp:67
morph::journal::OutboxRelay::relay
OutboxRelayResult relay()
Moves every row drainOutbox() currently reports to sink, then marks the whole batch relayed via markR...
Definition
outbox.hpp:106
morph::journal::OutboxRelay::drainOutbox
std::function< std::vector< LogEntry >()> drainOutbox
Pulls committed-but-unrelayed rows from the model's own store. Must be non-destructive: the same rows...
Definition
outbox.hpp:71
morph::journal::OutboxRelay::markRelayed
std::function< void(std::span< const LogEntry >)> markRelayed
Marks rows relayed in the model's own store (by identity or LogEntry::idempotencyKey) so a later drai...
Definition
outbox.hpp:76
morph::journal::OutboxRelay::sink
std::shared_ptr< IActionLog > sink
Durable audit sink rows are forwarded to.
Definition
outbox.hpp:79
morph
journal
outbox.hpp
Generated by
1.17.0