13#include "../attributes.hpp"
14#include "../core/completion.hpp"
16#include "../core/executor.hpp"
17#include "../core/logger.hpp"
18#include "../core/observability.hpp"
21namespace morph::offline {
24enum class ReconnectOutcome : std::uint8_t {
35constexpr std::string_view reconnectOutcomeName(ReconnectOutcome outcome)
noexcept {
37 case ReconnectOutcome::Reconnected:
39 case ReconnectOutcome::GaveUp:
41 case ReconnectOutcome::Aborted:
66 std::chrono::milliseconds
retryDelay = std::chrono::seconds{2};
138 std::function<void(std::chrono::milliseconds)>
sleep;
153 : _state{std::make_shared<State>(std::move(deps), cfg)}, _strand{executor} {
154 assertDepsNonNull(_state->deps);
181 _strand.postTask([state = _state,
strand = &_strand, promise = std::move(settleable.second)]()
mutable {
182 ::morph::exec::detail::noteOwner(
"ReconnectCoordinator::onOnline", strand->coreExecutor(),
183 strand->runningHere());
184 promise.resolve(state->runOnline());
186 return std::move(settleable.first);
196 _strand.postTask([state = _state,
strand = &_strand] {
197 ::morph::exec::detail::noteOwner(
"ReconnectCoordinator::onOffline",
strand->coreExecutor(),
199 state->deps.activateLocal();
200 state->deps.bindContext();
212 static void assertDepsNonNull(
const Deps& deps) {
213 auto check = [](
const char* name,
bool present) {
215 ::morph::log::logError(std::string{
"[reconnect_coordinator] null Deps member: "} + name);
218 check(
"tryReconnect",
static_cast<bool>(deps.tryReconnect));
219 check(
"activatePrimary",
static_cast<bool>(deps.activatePrimary));
220 check(
"activateLocal",
static_cast<bool>(deps.activateLocal));
221 check(
"bindContext",
static_cast<bool>(deps.bindContext));
222 check(
"replay",
static_cast<bool>(deps.replay));
223 check(
"shouldContinue",
static_cast<bool>(deps.shouldContinue));
224 check(
"sleep",
static_cast<bool>(deps.sleep));
231 State(Deps injected,
Config config) : deps{std::move(injected)}, cfg{config} {}
238 [[nodiscard]] ReconnectOutcome runOnline()
const {
239 for (
int attempt = 1; attempt <= cfg.maxAttempts; ++attempt) {
240 if (!callShouldContinue()) {
241 return emitOutcome(ReconnectOutcome::Aborted);
243 ::morph::observe::detail::emitMetric(::morph::observe::Metric::reconnectAttempts, 1.0);
244 if (callTryReconnect()) {
245 deps.activatePrimary();
250 if (callShouldContinue()) {
253 return emitOutcome(ReconnectOutcome::Reconnected);
257 if (attempt < cfg.maxAttempts) {
258 deps.sleep(cfg.retryDelay);
261 ::morph::log::logWarn(
"[reconnect_coordinator] gave up after " + std::to_string(cfg.maxAttempts) +
262 " attempts, staying offline");
263 return emitOutcome(ReconnectOutcome::GaveUp);
267 [[nodiscard]]
bool callTryReconnect() const noexcept {
269 return deps.tryReconnect();
276 [[nodiscard]]
bool callShouldContinue() const noexcept {
278 return deps.shouldContinue();
288 static ReconnectOutcome emitOutcome(ReconnectOutcome outcome)
noexcept {
289 std::array<std::pair<std::string_view, std::string_view>, 1>
const tags{
290 {{
"outcome", detail::reconnectOutcomeName(outcome)}}};
291 ::morph::observe::detail::emitMetric(::morph::observe::Metric::reconnectOutcome, 1.0, tags);
295 std::shared_ptr<State> _state;
298 ::morph::exec::OwnerStrand _strand;
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.
Sequences reconnect → activate → bind → replay when connectivity returns.
::morph::async::Completion< ReconnectOutcome > onOnline(::morph::exec::IExecutor &replyExec)
Runs the reconnect → activate → bind → replay sequence on the offline strand.
void onOffline()
Switches to the local backend and rebinds context, on the offline strand.
~ReconnectCoordinator()
Closes the offline strand: a sequence not yet started is dropped, one running on another thread is wa...
ReconnectCoordinator(Deps deps, ::morph::exec::IExecutor &executor, Config cfg=Config{})
Constructs a coordinator with injected dependencies and tuning, running its sequences on a strand ove...
ReconnectCoordinatorConfig Config
Alias for the configuration struct.
::morph::exec::IExecutor & strand() noexcept
The offline strand, as an executor: what a SyncWorker that replays for this coordinator is given as i...
The check every loop-owned body makes on entry: "am I on my owner?".
OwnerStrand: one core-cpp strand over a morph executor, which is itself a morph executor — the owner ...
Abstract executor interface.
Tuning parameters for ReconnectCoordinator.
std::chrono::milliseconds retryDelay
Delay between failed reconnect attempts.
int maxAttempts
Max reconnect attempts per onOnline() call before giving up.
Injected, host-supplied side effects. None may be null. Each is called on the offline strand.
std::function< bool()> shouldContinue
Returns false to abort the current onOnline() sequence early (e.g. the monitor reports the backend we...
std::function< void(std::chrono::milliseconds)> sleep
Sleep for the given duration between failed attempts. Injected so tests can substitute a no-op / coun...
std::function< void()> bindContext
Rebind per-connection / per-session context to the active backend. Called after every activate* step....
std::function< void()> activatePrimary
Make the freshly-reconnected primary the active backend. Called exactly once per successful onOnline(...
std::function< void()> activateLocal
Switch the active backend to the local/offline one. Called by onOffline(). Followed by bindContext() ...
std::function< void()> replay
Replay the offline queue against the now-active primary. Typically wraps SyncWorker::run(replyExec) o...
std::function< bool()> tryReconnect
Attempt to (re)open the primary backend.