morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
owned_state.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4#include <exception>
5#include <memory>
6#include <string>
7#include <type_traits>
8#include <utility>
9
10#include "../../attributes.hpp"
11#include "../completion.hpp"
12#include "../executor.hpp"
13#include "../logger.hpp"
14#include "owner_affinity.hpp"
15
16/// @file
17/// @brief `OwnedState`: a component's state, and the one executor it is
18/// touched on.
19///
20/// Specified in `docs/spec/core/executor.md`, "Owned state".
21
22namespace morph::exec::detail {
23
24/// @brief A component's state, held by `std::shared_ptr`, and the owner every
25/// access to it runs on.
26///
27/// What the storage types (`journal::SessionLog`, the action logs, the offline
28/// queues, `offline::InMemoryReplayLedger`) are built on. Three ways in:
29///
30/// - `apply()` — a write. On the owner it runs now; anywhere else it is posted
31/// to the owner, one hop, and the caller does not wait.
32/// - `read()` — owner-only access, for a verb that returns data or a failure
33/// the caller must see. Off the owner it is reported (asserted in a debug
34/// build, handed to a test's probe otherwise).
35/// - `ask()` — the same verb for a caller off the owner: runs on the owner and
36/// answers with a `Completion` delivered on the executor the caller names.
37///
38/// A posted task holds the state, never the component, so the component may
39/// be destroyed on any thread: what it posted still runs on the owner, and the
40/// state is freed with the last task that holds it.
41///
42/// The owner must run one task at a time (a strand, a GUI executor, a pumped
43/// `MainThreadExecutor`): state touched in two of its tasks at once would race.
44/// @tparam State The component's state.
45template <typename State>
46class OwnedState {
47public:
48 /// @param owner The executor the state belongs to. Borrowed: it must
49 /// outlive this object and run what is posted to it.
50 /// @param state The state, built by the component's constructor.
51 OwnedState(IExecutor& owner MORPH_LIFETIMEBOUND, std::shared_ptr<State> state)
52 : _affinity{owner}, _state{std::move(state)} {}
53
54 /// @brief The owner.
55 /// @return The executor passed at construction.
56 [[nodiscard]] IExecutor& owner() const noexcept { return _affinity.owner(); }
57
58 /// @brief Whether the calling thread is on the owner (`OwnerAffinity::here`).
59 /// @return True inside an owner task, or on the constructing thread when it
60 /// was outside every task.
61 [[nodiscard]] bool here() const noexcept { return _affinity.here(); }
62
63 /// @brief Owner-only access to the state.
64 /// @param site A static string naming the verb, e.g. `"FileActionLog::flush"`.
65 /// @return The state.
66 [[nodiscard]] State& read(char const* site) const noexcept {
67 _affinity.note(site);
68 return *_state;
69 }
70
71 /// @brief Runs @p body with the state on the owner: now when called there,
72 /// posted otherwise.
73 ///
74 /// A throw out of a posted body has nowhere to go but the log, which names
75 /// @p site; on the owner it propagates to the caller.
76 /// @tparam Body Copyable callable taking `State&`.
77 /// @param site A static string naming the verb.
78 /// @param body The write.
79 template <typename Body>
80 void apply(char const* site, Body body) {
81 if (_affinity.here()) {
82 _affinity.note(site);
83 body(*_state);
84 return;
85 }
86 _affinity.owner().post([affinity = _affinity, state = _state, site, body = std::move(body)]() mutable {
87 affinity.note(site);
88 try {
89 body(*state);
90 } catch (const std::exception& exc) {
91 ::morph::log::logError("{}: posted write failed: {}", site, exc.what());
92 } catch (...) {
93 ::morph::log::logError("{}: posted write failed with an unknown exception", site);
94 }
95 });
96 }
97
98 /// @brief Answers @p body's result, computed with the state on the owner,
99 /// delivered on @p replyExec.
100 ///
101 /// On the owner @p body runs now; elsewhere it is posted. Either way the
102 /// answer is delivered by a task on @p replyExec, and a throw out of
103 /// @p body rejects it.
104 /// @tparam T The answer's type.
105 /// @tparam Body Copyable callable taking `State&` and returning `T`.
106 /// @param site A static string naming the verb.
107 /// @param replyExec Where the answer is delivered and the caller attaches
108 /// its callbacks. Borrowed: it must outlive the completion.
109 /// @param body The read.
110 /// @return The answer.
111 template <typename T, typename Body>
112 [[nodiscard]] ::morph::async::Completion<T> ask(char const* site, IExecutor& replyExec MORPH_LIFETIMEBOUND,
113 Body body) const {
114 auto settleable = ::morph::async::Completion<T>::makeSettleable(&replyExec);
115 auto answer = [affinity = _affinity, state = _state, site, body = std::move(body),
116 promise = std::make_shared<typename ::morph::async::Completion<T>::Promise>(
117 std::move(settleable.second))]() mutable {
118 affinity.note(site);
119 try {
120 promise->resolve(body(*state));
121 } catch (...) {
122 promise->reject(std::current_exception());
123 }
124 };
125 if (_affinity.here()) {
126 answer();
127 } else {
128 _affinity.owner().post(std::move(answer));
129 }
130 return std::move(settleable.first);
131 }
132
133private:
134 OwnerAffinity _affinity;
135 std::shared_ptr<State> _state;
136};
137
138} // namespace morph::exec::detail
static std::pair< Completion< T >, Promise > makeSettleable(::morph::exec::IExecutor *execPtr)
Constructs a Completion<T>/Promise<T> pair sharing one settleable state.
"Am I on my owner?" for a component that belongs to one executor.