morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
network_monitor.hpp
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4#include <atomic>
5#include <chrono>
6#include <core/net/EventLoop.hpp>
7#include <functional>
8#include <memory>
9#include <utility>
10
11#include "../attributes.hpp"
13#include "../core/io_loop.hpp"
14
15namespace morph::offline {
16
17/// @brief Configuration for `NetworkMonitor`.
18///
19/// Declared outside `NetworkMonitor` so that its default member initialisers
20/// are fully parsed before any constructor default argument that names
21/// `Config{}` is evaluated. Compilers (clang, GCC) reject `Config cfg = Config{}`
22/// in a constructor default argument when `Config` is an incomplete nested type.
24 /// @brief Time between probe calls.
25 std::chrono::milliseconds probeInterval = std::chrono::seconds{5};
26
27 /// @brief Consecutive failures required before going offline.
29
30 /// @brief Consecutive successes required before going online.
32};
33
34/// @brief Connectivity monitor that calls a probe on a timer and fires
35/// callbacks on state changes.
36///
37/// The probe runs every `probeInterval` on an `exec::IoLoop`'s thread, as a
38/// loop timer. The monitor starts in the *online* state and transitions to
39/// *offline* only after `failureThreshold` consecutive failures. It returns to
40/// *online* after `onlineThreshold` consecutive successes.
41///
42/// The probe and both callbacks run on the loop's thread and **must not block
43/// it**: every socket and timer on the same loop waits while they run. A probe
44/// that needs a slow check should start it elsewhere and report the last
45/// result it has.
46///
47/// Cross-thread surface: `isOnline()`, an atomic read, and `stop()` and the
48/// destructor, which run their close on the loop and wait for it. Every other
49/// field is touched only on the loop.
50///
51/// The monitor is non-copyable and non-movable. Destroy it to stop monitoring.
53public:
54 /// @brief Returns `true` when the network is reachable.
55 using ProbeFunction = std::function<bool()>;
56
57 /// @brief Called on connectivity state change.
58 using Callback = std::function<void()>;
59
60 /// @brief Alias for the configuration struct.
62
63 /// @brief Starts the monitor on @p loop.
64 ///
65 /// The monitor starts in the online state. The first `failureThreshold`
66 /// consecutive probe failures must occur before `onOffline` is invoked.
67 /// Callers that know the initial state is offline should use a probe that
68 /// starts returning `false` immediately.
69 ///
70 /// @param loop The application's loop, whose thread runs the probe and
71 /// the callbacks. Borrowed: it must outlive this monitor.
72 /// @param probe Callable that tests connectivity. Must not throw
73 /// (exceptions are swallowed) and must not block. Stored and
74 /// invoked on the loop's thread for this monitor's whole
75 /// lifetime, so anything the callable refers to must outlive
76 /// the monitor.
77 /// @param onOffline Called on the loop's thread when the monitor goes
78 /// offline. Retained on the same terms as @p probe.
79 /// @param onOnline Called on the loop's thread when the monitor comes back
80 /// online. Retained on the same terms as @p probe.
81 /// @param cfg Tuning parameters (interval, thresholds).
82 NetworkMonitor(::morph::exec::IoLoop& loop MORPH_LIFETIMEBOUND, ProbeFunction probe MORPH_LIFETIMEBOUND,
83 Callback onOffline MORPH_LIFETIMEBOUND, Callback onOnline MORPH_LIFETIMEBOUND,
84 Config cfg = Config{})
85 : _loop{&loop},
86 _state{std::make_shared<State>(loop, std::move(probe), std::move(onOffline), std::move(onOnline), cfg)} {
87 start();
88 }
89
90 /// @brief Starts the monitor on a loop of its own, for a caller with no
91 /// `IoLoop` to share. Natively that loop owns one thread.
92 ///
93 /// @param probe As for the other constructor.
94 /// @param onOffline As for the other constructor.
95 /// @param onOnline As for the other constructor.
96 /// @param cfg Tuning parameters (interval, thresholds).
97 NetworkMonitor(ProbeFunction probe MORPH_LIFETIMEBOUND, Callback onOffline MORPH_LIFETIMEBOUND,
98 Callback onOnline MORPH_LIFETIMEBOUND, Config cfg = Config{})
99 : _ownedLoop{std::make_unique<::morph::exec::IoLoop>()},
100 _loop{_ownedLoop.get()},
101 _state{
102 std::make_shared<State>(*_ownedLoop, std::move(probe), std::move(onOffline), std::move(onOnline), cfg)} {
103 start();
104 }
105
106 /// @brief Stops the monitor. Once this returns, no probe or callback of
107 /// this monitor will run again, and from any thread but the loop's
108 /// none is running.
109 ///
110 /// May run anywhere, including inside this monitor's own probe or
111 /// callback: the timer that called it keeps the loop-side state alive
112 /// until it has returned.
114
115 NetworkMonitor(const NetworkMonitor&) = delete;
116 NetworkMonitor& operator=(const NetworkMonitor&) = delete;
117 NetworkMonitor(NetworkMonitor&&) = delete;
118 NetworkMonitor& operator=(NetworkMonitor&&) = delete;
119
120 /// @brief Returns `true` if the monitor currently considers the network reachable.
121 ///
122 /// Reads an atomic flag — safe to call from any thread at any time.
123 ///
124 /// @return Current online state.
125 [[nodiscard]] bool isOnline() const noexcept { return _state->online.load(); }
126
127 /// @brief Stops probing. Idempotent.
128 ///
129 /// Runs on the loop: inline when called there (from the probe or a
130 /// callback, which then finishes normally, and no further probe runs),
131 /// posted and waited for from any other thread, so that once it returns
132 /// no probe or callback is running.
133 void stop() {
134 _loop->runAndWait([state = _state] { state->stop(); });
135 }
136
137private:
138 /// Everything the loop owns. `online` is the one field read elsewhere.
139 struct State : std::enable_shared_from_this<State> {
140 State(::morph::exec::IoLoop& ioLoop, ProbeFunction probeFn, Callback offline, Callback online_, Config config)
141 : loop{ioLoop},
142 probe{std::move(probeFn)},
143 onOffline{std::move(offline)},
144 onOnline{std::move(online_)},
145 cfg{config} {}
146
147 void armNext() {
148 timer = loop.loop().addTimer(loop.loop().clock().now() + cfg.probeInterval, &State::fire, this);
149 }
150
151 void stop() {
152 ::morph::exec::detail::noteOwner("NetworkMonitor::stop", loop.loop(), loop.runningHere());
153 if (stopped) {
154 return;
155 }
156 stopped = true;
157 static_cast<void>(loop.loop().cancelTimer(timer));
158 }
159
160 static bool safeProbe(const ProbeFunction& probeFn) noexcept {
161 try {
162 return probeFn ? probeFn() : false;
163 } catch (...) {
164 return false;
165 }
166 }
167
168 void handleProbeResult(bool probeOk) {
169 if (!probeOk) {
170 consecutiveSuccesses = 0;
171 ++consecutiveFailures;
172 if (online.load() && consecutiveFailures >= cfg.failureThreshold) {
173 online.store(false);
174 if (onOffline) {
175 onOffline();
176 }
177 }
178 } else {
179 consecutiveFailures = 0;
180 ++consecutiveSuccesses;
181 if (!online.load() && consecutiveSuccesses >= cfg.onlineThreshold) {
182 online.store(true);
183 if (onOnline) {
184 onOnline();
185 }
186 }
187 }
188 }
189
190 /// The probe timer's callback, on the loop's thread. The timer has
191 /// already fired, so a `stop()` from the probe or a callback retires
192 /// nothing and only prevents the re-arm below.
193 static void fire(void* statePtr) {
194 // Held for the whole call: the probe or a callback may destroy
195 // the monitor, and with it the monitor's own share of this state.
196 std::shared_ptr<State> const keep = static_cast<State*>(statePtr)->shared_from_this();
197 State& self = *keep;
198 ::morph::exec::detail::noteOwner("NetworkMonitor::probe", self.loop.loop(), self.loop.runningHere());
199 self.timer = {};
200 if (self.stopped) {
201 return;
202 }
203 bool const probeOk = safeProbe(self.probe);
204 if (self.stopped) {
205 return;
206 }
207 self.handleProbeResult(probeOk);
208 if (!self.stopped) {
209 self.armNext();
210 }
211 }
212
213 ::morph::exec::IoLoop& loop;
214 ProbeFunction probe;
215 Callback onOffline;
216 Callback onOnline;
217 Config cfg;
218 std::atomic<bool> online{true};
219 int consecutiveFailures{0};
220 int consecutiveSuccesses{0};
221 ::core::net::TimerId timer{};
222 bool stopped{false};
223 };
224
225 void start() {
226 _loop->post([state = _state] {
227 if (!state->stopped) {
228 state->armNext();
229 }
230 });
231 }
232
233 /// Present only for the owning constructor. First, so it is destroyed
234 /// last, after the destructor has stopped the state on it.
235 std::unique_ptr<::morph::exec::IoLoop> _ownedLoop;
236 ::morph::exec::IoLoop* _loop;
237 std::shared_ptr<State> _state;
238};
239
240} // namespace morph::offline
constexpr Self self
The dialog or drawer a command list runs in.
Definition author.hpp:1295
Owns a core::net::PlatformLoop and, for IoLoopDriver::OwnThread natively, the one thread that runs it...
Definition io_loop.hpp:74
::core::net::EventLoop & loop() noexcept
The loop itself, for a component arming timers or opening sockets from inside one of its tasks,...
Definition io_loop.hpp:176
bool runningHere() const noexcept
Whether the calling thread may touch the loop's state directly.
Definition io_loop.hpp:190
Connectivity monitor that calls a probe on a timer and fires callbacks on state changes.
NetworkMonitor(ProbeFunction probe, Callback onOffline, Callback onOnline, Config cfg=Config{})
Starts the monitor on a loop of its own, for a caller with no IoLoop to share. Natively that loop own...
std::function< void()> Callback
Called on connectivity state change.
bool isOnline() const noexcept
Returns true if the monitor currently considers the network reachable.
std::function< bool()> ProbeFunction
Returns true when the network is reachable.
NetworkMonitorConfig Config
Alias for the configuration struct.
NetworkMonitor(::morph::exec::IoLoop &loop, ProbeFunction probe, Callback onOffline, Callback onOnline, Config cfg=Config{})
Starts the monitor on loop.
void stop()
Stops probing. Idempotent.
~NetworkMonitor()
Stops the monitor. Once this returns, no probe or callback of this monitor will run again,...
The check every loop-owned body makes on entry: "am I on my owner?".
Configuration for NetworkMonitor.
int onlineThreshold
Consecutive successes required before going online.
std::chrono::milliseconds probeInterval
Time between probe calls.
int failureThreshold
Consecutive failures required before going offline.