Skip to content

Commit 84f527e

Browse files
authored
Merge pull request #44 from lf-lang/enclave-connections
Enclave connections and enclave coordination
2 parents e80cd36 + b4c6963 commit 84f527e

File tree

13 files changed

+524
-113
lines changed

13 files changed

+524
-113
lines changed

include/reactor-cpp/action.hh

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@
99
#ifndef REACTOR_CPP_ACTION_HH
1010
#define REACTOR_CPP_ACTION_HH
1111

12+
#include "assert.hh"
1213
#include "environment.hh"
1314
#include "fwd.hh"
1415
#include "logical_time.hh"
1516
#include "reactor.hh"
17+
#include "time_barrier.hh"
1618
#include "value_ptr.hh"
1719

20+
#include <condition_variable>
1821
#include <map>
1922
#include <mutex>
2023

@@ -35,6 +38,21 @@ protected:
3538
virtual void setup() noexcept { present_ = true; }
3639
virtual void cleanup() noexcept { present_ = false; }
3740

41+
/**
42+
* Use the given condition variable and lock to wait until the given tag it
43+
* safe to process. The waiting is interrupted when the condition variable is
44+
* notified (or has a spurious wakeup) and a call to the given `abort_waiting`
45+
* function returns true. or until the condition variable is notified.
46+
*
47+
* Returns false if the wait was interrupted and true otherwise. True
48+
* indicates that the tag is safe to process.
49+
*/
50+
virtual auto acquire_tag(const Tag& tag, std::unique_lock<std::mutex>& lock, std::condition_variable& cv,
51+
const std::function<bool(void)>& abort_waiting) -> bool {
52+
reactor_assert(!logical_);
53+
return PhysicalTimeBarrier::acquire_tag(tag, lock, cv, abort_waiting);
54+
}
55+
3856
BaseAction(const std::string& name, Reactor* container, bool logical, Duration min_delay)
3957
: ReactorElement(name, ReactorElement::Type::Action, container)
4058
, min_delay_(min_delay)
@@ -94,6 +112,7 @@ public:
94112
void shutdown() final {}
95113

96114
template <class Dur = Duration> void schedule(const ImmutableValuePtr<T>& value_ptr, Dur delay = Dur::zero());
115+
auto schedule_at(const ImmutableValuePtr<T>& value_ptr, const Tag& tag) -> bool;
97116

98117
template <class Dur = Duration> void schedule(MutableValuePtr<T>&& value_ptr, Dur delay = Dur::zero()) {
99118
schedule(ImmutableValuePtr<T>(std::forward<MutableValuePtr<T>>(value_ptr)), delay);
@@ -122,6 +141,7 @@ protected:
122141

123142
public:
124143
template <class Dur = Duration> void schedule(Dur delay = Dur::zero());
144+
auto schedule_at(const Tag& tag) -> bool;
125145

126146
void startup() final {}
127147
void shutdown() final {}

include/reactor-cpp/assert.hh

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -67,16 +67,16 @@ constexpr inline void validate([[maybe_unused]] bool condition, [[maybe_unused]]
6767
}
6868
}
6969

70-
// Use plain assert on Windows and when assertions are enabled.
71-
// Otherwise, the locations of assertion errors are not properly reported.
72-
#if defined(_WIN32) && !defined(NDEBUG)
73-
#define reactor_assert(condition) assert(condition)
70+
// assert macro that avoids unused variable warnings
71+
#ifdef NDEBUG
72+
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
73+
#define reactor_assert(x) \
74+
do { \
75+
(void)sizeof(x); \
76+
} while (0)
7477
#else
75-
constexpr inline void reactor_assert([[maybe_unused]] bool condition) {
76-
if constexpr (runtime_assertion) { // NOLINT
77-
assert(condition); // NOLINT
78-
}
79-
}
78+
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
79+
#define reactor_assert(x) assert(x)
8080
#endif
8181

8282
template <typename E> constexpr auto extract_value(E enum_value) -> typename std::underlying_type<E>::type {

include/reactor-cpp/connection.hh

Lines changed: 151 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@
1313
#include "action.hh"
1414
#include "assert.hh"
1515
#include "environment.hh"
16+
#include "fwd.hh"
17+
#include "logical_time.hh"
1618
#include "port.hh"
1719
#include "reaction.hh"
1820
#include "reactor.hh"
1921
#include "time.hh"
22+
#include "time_barrier.hh"
2023

2124
namespace reactor {
2225

@@ -28,26 +31,32 @@ private:
2831
protected:
2932
Connection(const std::string& name, Reactor* container, bool is_logical, Duration min_delay)
3033
: Action<T>(name, container, is_logical, min_delay) {}
34+
Connection(const std::string& name, Environment* environment, bool is_logical, Duration min_delay)
35+
: Action<T>(name, environment, is_logical, min_delay) {}
3136

3237
[[nodiscard]] auto downstream_ports() -> auto& { return downstream_ports_; }
3338
[[nodiscard]] auto downstream_ports() const -> const auto& { return downstream_ports_; }
39+
[[nodiscard]] auto upstream_port() -> auto* { return upstream_port_; }
40+
[[nodiscard]] auto upstream_port() const -> const auto* { return upstream_port_; }
3441

3542
virtual auto upstream_set_callback() noexcept -> PortCallback = 0;
3643

3744
public:
38-
void bind_upstream_port(Port<T>* port) {
45+
virtual void bind_upstream_port(Port<T>* port) {
3946
reactor_assert(upstream_port_ == nullptr);
4047
upstream_port_ = port;
4148
port->register_set_callback(upstream_set_callback());
4249
}
4350

44-
void bind_downstream_port(Port<T>* port) { reactor_assert(this->downstream_ports_.insert(port).second); };
51+
virtual void bind_downstream_port(Port<T>* port) { reactor_assert(this->downstream_ports_.insert(port).second); };
4552
};
4653

4754
template <class T> class BaseDelayedConnection : public Connection<T> {
48-
public:
55+
protected:
4956
BaseDelayedConnection(const std::string& name, Reactor* container, bool is_logical, Duration delay)
5057
: Connection<T>(name, container, is_logical, delay) {}
58+
BaseDelayedConnection(const std::string& name, Environment* environment, bool is_logical, Duration delay)
59+
: Connection<T>(name, environment, is_logical, delay) {}
5160

5261
inline auto upstream_set_callback() noexcept -> PortCallback override {
5362
return [this](const BasePort& port) {
@@ -61,6 +70,7 @@ public:
6170
};
6271
}
6372

73+
public:
6474
void setup() noexcept override {
6575
Action<T>::setup();
6676

@@ -88,6 +98,144 @@ public:
8898
: BaseDelayedConnection<T>(name, container, false, delay) {}
8999
};
90100

101+
template <class T> class EnclaveConnection : public BaseDelayedConnection<T> {
102+
private:
103+
LogicalTimeBarrier logical_time_barrier_;
104+
105+
protected:
106+
log::NamedLogger log_; // NOLINT
107+
108+
EnclaveConnection(const std::string& name, Environment* enclave, const Duration& delay)
109+
: BaseDelayedConnection<T>(name, enclave, false, delay)
110+
, log_{this->fqn()} {}
111+
112+
public:
113+
EnclaveConnection(const std::string& name, Environment* enclave)
114+
: BaseDelayedConnection<T>(name, enclave, false, Duration::zero())
115+
, log_{this->fqn()} {}
116+
117+
inline auto upstream_set_callback() noexcept -> PortCallback override {
118+
return [this](const BasePort& port) {
119+
// We know that port must be of type Port<T>
120+
auto& typed_port = reinterpret_cast<const Port<T>&>(port); // NOLINT
121+
const auto* scheduler = port.environment()->scheduler();
122+
// This callback will be called from a reaction executing in the context
123+
// of the upstream port. Hence, we can retrieve the current tag directly
124+
// without locking.
125+
auto tag = Tag::from_logical_time(scheduler->logical_time());
126+
bool result{false};
127+
if constexpr (std::is_same<T, void>::value) {
128+
result = this->schedule_at(tag);
129+
} else {
130+
result = this->schedule_at(std::move(typed_port.get()), tag);
131+
}
132+
reactor_assert(result);
133+
};
134+
}
135+
136+
inline auto acquire_tag(const Tag& tag, std::unique_lock<std::mutex>& lock, std::condition_variable& cv,
137+
const std::function<bool(void)>& abort_waiting) -> bool override {
138+
log_.debug() << "downstream tries to acquire tag " << tag;
139+
140+
if (this->upstream_port() == nullptr) {
141+
return true;
142+
}
143+
144+
if (logical_time_barrier_.try_acquire_tag(tag)) {
145+
return true;
146+
}
147+
148+
// Insert an empty event into the upstream event queue. This ensures that we
149+
// will get notified and woken up as soon as the tag becomes safe to process.
150+
// It is important to unlock the mutex here. Otherwise we could enter a deadlock as
151+
// scheduling the upstream event also requires holding the upstream mutex.
152+
lock.unlock();
153+
bool result = this->upstream_port()->environment()->scheduler()->schedule_empty_async_at(tag);
154+
lock.lock();
155+
156+
// If inserting the empty event was not successful, then this is because the upstream
157+
// scheduler already processes a later event. In this case, it is safe to assume that
158+
// the tag is acquired.
159+
if (!result) {
160+
return true;
161+
}
162+
163+
// Wait until we receive a release_tag message from upstream
164+
return logical_time_barrier_.acquire_tag(tag, lock, cv, abort_waiting);
165+
}
166+
167+
void bind_upstream_port(Port<T>* port) override {
168+
Connection<T>::bind_upstream_port(port);
169+
port->environment()->scheduler()->register_release_tag_callback([this](const LogicalTime& tag) {
170+
logical_time_barrier_.release_tag(tag);
171+
log_.debug() << "upstream released tag " << tag;
172+
this->environment()->scheduler()->notify();
173+
});
174+
}
175+
};
176+
177+
template <class T> class DelayedEnclaveConnection : public EnclaveConnection<T> {
178+
public:
179+
DelayedEnclaveConnection(const std::string& name, Environment* enclave, Duration delay)
180+
: EnclaveConnection<T>(name, enclave, delay) {}
181+
182+
inline auto upstream_set_callback() noexcept -> PortCallback override {
183+
return [this](const BasePort& port) {
184+
// We know that port must be of type Port<T>
185+
auto& typed_port = reinterpret_cast<const Port<T>&>(port); // NOLINT
186+
const auto* scheduler = port.environment()->scheduler();
187+
// This callback will be called from a reaction executing in the context
188+
// of the upstream port. Hence, we can retrieve the current tag directly
189+
// without locking.
190+
auto tag = Tag::from_logical_time(scheduler->logical_time()).delay(this->min_delay());
191+
bool result{false};
192+
if constexpr (std::is_same<T, void>::value) {
193+
result = this->schedule_at(tag);
194+
} else {
195+
result = this->schedule_at(std::move(typed_port.get()), tag);
196+
}
197+
reactor_assert(result);
198+
};
199+
}
200+
201+
inline auto acquire_tag(const Tag& tag, std::unique_lock<std::mutex>& lock, std::condition_variable& cv,
202+
const std::function<bool(void)>& abort_waiting) -> bool override {
203+
// Since this is a delayed connection, we can go back in time and need to
204+
// acquire the latest upstream tag that can create an event at the given
205+
// tag. We also need to consider that given a delay d and a tag g=(t, n),
206+
// for any value of n, g + d = (t, 0). Hence, we need to quire a tag with
207+
// the highest possible microstep value.
208+
auto upstream_tag = tag.subtract(this->min_delay());
209+
return EnclaveConnection<T>::acquire_tag(upstream_tag, lock, cv, abort_waiting);
210+
}
211+
};
212+
213+
template <class T> class PhysicalEnclaveConnection : public EnclaveConnection<T> {
214+
public:
215+
PhysicalEnclaveConnection(const std::string& name, Environment* enclave)
216+
: EnclaveConnection<T>(name, enclave) {}
217+
218+
inline auto upstream_set_callback() noexcept -> PortCallback override {
219+
return [this](const BasePort& port) {
220+
// We know that port must be of type Port<T>
221+
auto& typed_port = reinterpret_cast<const Port<T>&>(port); // NOLINT
222+
if constexpr (std::is_same<T, void>::value) {
223+
this->schedule();
224+
} else {
225+
this->schedule(std::move(typed_port.get()));
226+
}
227+
};
228+
}
229+
230+
inline auto acquire_tag(const Tag& tag, std::unique_lock<std::mutex>& lock, std::condition_variable& cv,
231+
const std::function<bool(void)>& abort_waiting) -> bool override {
232+
this->log_.debug() << "downstream tries to acquire tag " << tag;
233+
return PhysicalTimeBarrier::acquire_tag(tag, lock, cv, abort_waiting);
234+
}
235+
236+
void bind_upstream_port(Port<T>* port) override { Connection<T>::bind_upstream_port(port); }
237+
};
238+
91239
} // namespace reactor
92240

93241
#endif // REACTOR_CPP_CONNECTION_HH

include/reactor-cpp/environment.hh

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ private:
5555

5656
Scheduler scheduler_;
5757
Phase phase_{Phase::Construction};
58-
TimePoint start_time_{};
58+
Tag start_tag_{};
5959

6060
const Duration timeout_{};
6161

@@ -96,7 +96,7 @@ public:
9696
auto scheduler() noexcept -> Scheduler* { return &scheduler_; }
9797

9898
[[nodiscard]] auto logical_time() const noexcept -> const LogicalTime& { return scheduler_.logical_time(); }
99-
[[nodiscard]] auto start_time() const noexcept -> const TimePoint& { return start_time_; }
99+
[[nodiscard]] auto start_tag() const noexcept -> const Tag& { return start_tag_; }
100100
[[nodiscard]] auto timeout() const noexcept -> const Duration& { return timeout_; }
101101

102102
static auto physical_time() noexcept -> TimePoint { return get_physical_time(); }
@@ -105,6 +105,8 @@ public:
105105
[[nodiscard]] auto fast_fwd_execution() const noexcept -> bool { return fast_fwd_execution_; }
106106
[[nodiscard]] auto run_forever() const noexcept -> bool { return run_forever_; }
107107
[[nodiscard]] auto max_reaction_index() const noexcept -> unsigned int { return max_reaction_index_; }
108+
109+
friend Scheduler;
108110
};
109111
} // namespace reactor
110112

include/reactor-cpp/impl/action_impl.hh

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,41 +20,68 @@ template <class T> template <class Dur> void Action<T>::schedule(const Immutable
2020
Duration time_delay = std::chrono::duration_cast<Duration>(delay);
2121
reactor::validate(time_delay >= Duration::zero(), "Schedule cannot be called with a negative delay!");
2222
reactor::validate(value_ptr != nullptr, "Actions may not be scheduled with a nullptr value!");
23+
2324
auto* scheduler = environment()->scheduler();
2425
if (is_logical()) {
2526
time_delay += this->min_delay();
2627
auto tag = Tag::from_logical_time(scheduler->logical_time()).delay(time_delay);
27-
events_[tag] = value_ptr;
28+
2829
scheduler->schedule_sync(this, tag);
30+
events_[tag] = value_ptr;
2931
} else {
30-
{
31-
std::lock_guard<std::mutex> lock(mutex_events_);
32-
// We must call schedule_async while holding the mutex, because otherwise
33-
// the scheduler could already start processing the event that we schedule
34-
// and call setup() on this action before we insert the value in events_.
35-
// Holding both the local mutex mutex_events_ and them scheduler mutes (in
36-
// schedule async) should not lead to a deadlock as the scheduler will
37-
// only hold one of the two mutexes at once.
38-
auto tag = scheduler->schedule_async(this, time_delay);
39-
events_[tag] = value_ptr;
40-
}
32+
std::lock_guard<std::mutex> lock{mutex_events_};
33+
// We must call schedule_async while holding the mutex, because otherwise
34+
// the scheduler could already start processing the event that we schedule
35+
// and call setup() on this action before we insert the value in events_.
36+
// Holding both the local mutex mutex_events_ and the scheduler mutex (in
37+
// schedule async) should not lead to a deadlock as the scheduler will
38+
// only hold one of the two mutexes at once.
39+
auto tag = scheduler->schedule_async(this, time_delay);
40+
events_[tag] = value_ptr;
4141
}
4242
}
4343

4444
template <class Dur> void Action<void>::schedule(Dur delay) {
45-
auto time_delay = std::chrono::duration_cast<Duration>(delay);
45+
Duration time_delay = std::chrono::duration_cast<Duration>(delay);
4646
reactor::validate(time_delay >= Duration::zero(), "Schedule cannot be called with a negative delay!");
4747
auto* scheduler = environment()->scheduler();
4848
if (is_logical()) {
4949
time_delay += this->min_delay();
5050
auto tag = Tag::from_logical_time(scheduler->logical_time()).delay(time_delay);
5151
scheduler->schedule_sync(this, tag);
5252
} else {
53-
// physical action
5453
scheduler->schedule_async(this, time_delay);
5554
}
5655
}
5756

57+
template <class T> auto Action<T>::schedule_at(const ImmutableValuePtr<T>& value_ptr, const Tag& tag) -> bool {
58+
reactor::validate(value_ptr != nullptr, "Actions may not be scheduled with a nullptr value!");
59+
60+
auto* scheduler = environment()->scheduler();
61+
if (is_logical()) {
62+
if (tag <= scheduler->logical_time()) {
63+
return false;
64+
}
65+
66+
scheduler->schedule_sync(this, tag);
67+
events_[tag] = value_ptr;
68+
} else {
69+
std::lock_guard<std::mutex> lock{mutex_events_};
70+
// We must call schedule_async while holding the mutex, because otherwise
71+
// the scheduler could already start processing the event that we schedule
72+
// and call setup() on this action before we insert the value in events_.
73+
// Holding both the local mutex mutex_events_ and the scheduler mutex (in
74+
// schedule async) should not lead to a deadlock as the scheduler will
75+
// only hold one of the two mutexes at once.
76+
bool result = scheduler->schedule_async_at(this, tag);
77+
if (result) {
78+
events_[tag] = value_ptr;
79+
}
80+
return result;
81+
}
82+
return true;
83+
}
84+
5885
template <class T> void Action<T>::setup() noexcept {
5986
BaseAction::setup();
6087
if (value_ptr_ == nullptr) { // only do this once, even if the action was triggered multiple times

0 commit comments

Comments
 (0)