mirror of https://github.com/grpc/grpc.git
Promise Activities (#26921)
* promise sequences * Activities for promises * Automated change: Fix sanity tests * review feedback * review feedback * review feedback * review feedback * review feedback Co-authored-by: ctiller <ctiller@users.noreply.github.com>pull/27120/head
parent
2d2576513d
commit
5cdaec9a4f
10 changed files with 1199 additions and 2 deletions
@ -0,0 +1,113 @@ |
||||
// Copyright 2021 gRPC authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include <grpc/impl/codegen/port_platform.h> |
||||
|
||||
#include "src/core/lib/gprpp/atomic_utils.h" |
||||
#include "src/core/lib/promise/activity.h" |
||||
|
||||
namespace grpc_core { |
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// GLOBALS
|
||||
|
||||
ABSL_CONST_INIT GPR_THREAD_LOCAL(Activity*) Activity::g_current_activity_ = |
||||
nullptr; |
||||
Waker::Unwakeable Waker::unwakeable_; |
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// HELPER TYPES
|
||||
|
||||
// Weak handle to an Activity.
|
||||
// Handle can persist while Activity goes away.
|
||||
class Activity::Handle final : public Wakeable { |
||||
public: |
||||
explicit Handle(Activity* activity) : activity_(activity) {} |
||||
|
||||
// Ref the Handle (not the activity).
|
||||
void Ref() { refs_.fetch_add(1, std::memory_order_relaxed); } |
||||
|
||||
// Activity is going away... drop its reference and sever the connection back.
|
||||
void DropActivity() ABSL_LOCKS_EXCLUDED(mu_) { |
||||
mu_.Lock(); |
||||
GPR_ASSERT(activity_ != nullptr); |
||||
activity_ = nullptr; |
||||
mu_.Unlock(); |
||||
Unref(); |
||||
} |
||||
|
||||
// Activity needs to wake up (if it still exists!) - wake it up, and drop the
|
||||
// ref that was kept for this handle.
|
||||
void Wakeup() override ABSL_LOCKS_EXCLUDED(mu_) { |
||||
mu_.Lock(); |
||||
// Note that activity refcount can drop to zero, but we could win the lock
|
||||
// against DropActivity, so we need to only increase activities refcount if
|
||||
// it is non-zero.
|
||||
if (activity_ && activity_->RefIfNonzero()) { |
||||
Activity* activity = activity_; |
||||
mu_.Unlock(); |
||||
// Activity still exists and we have a reference: wake it up, which will
|
||||
// drop the ref.
|
||||
activity->Wakeup(); |
||||
} else { |
||||
// Could not get the activity - it's either gone or going. No need to wake
|
||||
// it up!
|
||||
mu_.Unlock(); |
||||
} |
||||
// Drop the ref to the handle (we have one ref = one wakeup semantics).
|
||||
Unref(); |
||||
} |
||||
|
||||
void Drop() override { Unref(); } |
||||
|
||||
private: |
||||
// Unref the Handle (not the activity).
|
||||
void Unref() { |
||||
if (1 == refs_.fetch_sub(1, std::memory_order_acq_rel)) { |
||||
delete this; |
||||
} |
||||
} |
||||
|
||||
// Two initial refs: one for the waiter that caused instantiation, one for the
|
||||
// activity.
|
||||
std::atomic<size_t> refs_{2}; |
||||
Mutex mu_ ABSL_ACQUIRED_AFTER(activity_->mu_); |
||||
Activity* activity_ ABSL_GUARDED_BY(mu_); |
||||
}; |
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// ACTIVITY IMPLEMENTATION
|
||||
|
||||
bool Activity::RefIfNonzero() { return IncrementIfNonzero(&refs_); } |
||||
|
||||
Activity::Handle* Activity::RefHandle() { |
||||
if (handle_ == nullptr) { |
||||
// No handle created yet - construct it and return it.
|
||||
handle_ = new Handle(this); |
||||
return handle_; |
||||
} else { |
||||
// Already had to create a handle, ref & return it.
|
||||
handle_->Ref(); |
||||
return handle_; |
||||
} |
||||
} |
||||
|
||||
void Activity::DropHandle() { |
||||
handle_->DropActivity(); |
||||
handle_ = nullptr; |
||||
} |
||||
|
||||
Waker Activity::MakeNonOwningWaker() { return Waker(RefHandle()); } |
||||
|
||||
} // namespace grpc_core
|
@ -0,0 +1,437 @@ |
||||
// Copyright 2021 gRPC authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef GRPC_CORE_LIB_PROMISE_ACTIVITY_H |
||||
#define GRPC_CORE_LIB_PROMISE_ACTIVITY_H |
||||
|
||||
#include <grpc/impl/codegen/port_platform.h> |
||||
|
||||
#include <functional> |
||||
|
||||
#include <grpc/support/log.h> |
||||
|
||||
#include "src/core/lib/gprpp/construct_destruct.h" |
||||
#include "src/core/lib/gprpp/sync.h" |
||||
#include "src/core/lib/promise/context.h" |
||||
#include "src/core/lib/promise/detail/promise_factory.h" |
||||
#include "src/core/lib/promise/detail/status.h" |
||||
#include "src/core/lib/promise/poll.h" |
||||
|
||||
namespace grpc_core { |
||||
|
||||
// A Wakeable object is used by queues to wake activities.
|
||||
class Wakeable { |
||||
public: |
||||
// Wake up the underlying activity.
|
||||
// After calling, this Wakeable cannot be used again.
|
||||
virtual void Wakeup() = 0; |
||||
// Drop this wakeable without waking up the underlying activity.
|
||||
virtual void Drop() = 0; |
||||
|
||||
protected: |
||||
inline ~Wakeable() {} |
||||
}; |
||||
|
||||
// An owning reference to a Wakeable.
|
||||
// This type is non-copyable but movable.
|
||||
class Waker { |
||||
public: |
||||
explicit Waker(Wakeable* wakeable) : wakeable_(wakeable) {} |
||||
Waker() : wakeable_(&unwakeable_) {} |
||||
~Waker() { wakeable_->Drop(); } |
||||
Waker(const Waker&) = delete; |
||||
Waker& operator=(const Waker&) = delete; |
||||
Waker(Waker&& other) noexcept : wakeable_(other.wakeable_) { |
||||
other.wakeable_ = &unwakeable_; |
||||
} |
||||
Waker& operator=(Waker&& other) noexcept { |
||||
std::swap(wakeable_, other.wakeable_); |
||||
return *this; |
||||
} |
||||
|
||||
// Wake the underlying activity.
|
||||
void Wakeup() { |
||||
wakeable_->Wakeup(); |
||||
wakeable_ = &unwakeable_; |
||||
} |
||||
|
||||
template <typename H> |
||||
friend H AbslHashValue(H h, const Waker& w) { |
||||
return H::combine(std::move(h), w.wakeable_); |
||||
} |
||||
|
||||
bool operator==(const Waker& other) const noexcept { |
||||
return wakeable_ == other.wakeable_; |
||||
} |
||||
|
||||
private: |
||||
class Unwakeable final : public Wakeable { |
||||
public: |
||||
void Wakeup() final { abort(); } |
||||
void Drop() final {} |
||||
}; |
||||
|
||||
Wakeable* wakeable_; |
||||
static Unwakeable unwakeable_; |
||||
}; |
||||
|
||||
// An Activity tracks execution of a single promise.
|
||||
// It executes the promise under a mutex.
|
||||
// When the promise stalls, it registers the containing activity to be woken up
|
||||
// later.
|
||||
// The activity takes a callback, which will be called exactly once with the
|
||||
// result of execution.
|
||||
// Activity execution may be cancelled by simply deleting the activity. In such
|
||||
// a case, if execution had not already finished, the done callback would be
|
||||
// called with absl::CancelledError().
|
||||
// Activity also takes a CallbackScheduler instance on which to schedule
|
||||
// callbacks to itself in a lock-clean environment.
|
||||
class Activity : private Wakeable { |
||||
public: |
||||
// Cancel execution of the underlying promise.
|
||||
virtual void Cancel() ABSL_LOCKS_EXCLUDED(mu_) = 0; |
||||
|
||||
// Destroy the Activity - used for the type alias ActivityPtr.
|
||||
struct Deleter { |
||||
void operator()(Activity* activity) { |
||||
activity->Cancel(); |
||||
activity->Unref(); |
||||
} |
||||
}; |
||||
|
||||
// Fetch the size of the implementation of this activity.
|
||||
virtual size_t Size() = 0; |
||||
|
||||
// Wakeup the current threads activity - will force a subsequent poll after
|
||||
// the one that's running.
|
||||
static void WakeupCurrent() { current()->got_wakeup_during_run_ = true; } |
||||
|
||||
// Return the current activity.
|
||||
// Additionally:
|
||||
// - assert that there is a current activity (and catch bugs if there's not)
|
||||
// - indicate to thread safety analysis that the current activity is indeed
|
||||
// locked
|
||||
// - back up that assertation with a runtime check in debug builds (it's
|
||||
// prohibitively expensive in non-debug builds)
|
||||
static Activity* current() ABSL_ASSERT_EXCLUSIVE_LOCK(current()->mu_) { |
||||
#ifndef NDEBUG |
||||
GPR_ASSERT(g_current_activity_); |
||||
if (g_current_activity_ != nullptr) { |
||||
g_current_activity_->mu_.AssertHeld(); |
||||
} |
||||
#endif |
||||
return g_current_activity_; |
||||
} |
||||
|
||||
// Produce an activity-owning Waker. The produced waker will keep the activity
|
||||
// alive until it's awoken or dropped.
|
||||
Waker MakeOwningWaker() { |
||||
Ref(); |
||||
return Waker(this); |
||||
} |
||||
|
||||
// Produce a non-owning Waker. The waker will own a small heap allocated weak
|
||||
// pointer to this activity. This is more suitable for wakeups that may not be
|
||||
// delivered until long after the activity should be destroyed.
|
||||
Waker MakeNonOwningWaker() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_); |
||||
|
||||
protected: |
||||
inline virtual ~Activity() { |
||||
if (handle_) { |
||||
DropHandle(); |
||||
} |
||||
} |
||||
|
||||
// All promise execution occurs under this mutex.
|
||||
Mutex mu_; |
||||
|
||||
// Check if this activity is the current activity executing on the current
|
||||
// thread.
|
||||
bool is_current() const { return this == g_current_activity_; } |
||||
// Check if there is an activity executing on the current thread.
|
||||
static bool have_current() { return g_current_activity_ != nullptr; } |
||||
// Check if we got an internal wakeup since the last time this function was
|
||||
// called.
|
||||
bool got_wakeup() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_) { |
||||
return absl::exchange(got_wakeup_during_run_, false); |
||||
} |
||||
|
||||
// Set the current activity at construction, clean it up at destruction.
|
||||
class ScopedActivity { |
||||
public: |
||||
explicit ScopedActivity(Activity* activity) { |
||||
GPR_ASSERT(g_current_activity_ == nullptr); |
||||
g_current_activity_ = activity; |
||||
} |
||||
~ScopedActivity() { g_current_activity_ = nullptr; } |
||||
ScopedActivity(const ScopedActivity&) = delete; |
||||
ScopedActivity& operator=(const ScopedActivity&) = delete; |
||||
}; |
||||
|
||||
// Implementors of Wakeable::Wakeup should call this after the wakeup has
|
||||
// completed.
|
||||
void WakeupComplete() { Unref(); } |
||||
|
||||
private: |
||||
class Handle; |
||||
|
||||
void Ref() { refs_.fetch_add(1, std::memory_order_relaxed); } |
||||
void Unref() { |
||||
if (1 == refs_.fetch_sub(1, std::memory_order_acq_rel)) { |
||||
delete this; |
||||
} |
||||
} |
||||
|
||||
// Return a Handle instance with a ref so that it can be stored waiting for
|
||||
// some wakeup.
|
||||
Handle* RefHandle() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_); |
||||
// If our refcount is non-zero, ref and return true.
|
||||
// Otherwise, return false.
|
||||
bool RefIfNonzero(); |
||||
// Drop the (proved existing) wait handle.
|
||||
void DropHandle() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_); |
||||
|
||||
// Current refcount.
|
||||
std::atomic<uint32_t> refs_{1}; |
||||
// If wakeup is called during Promise polling, we raise this flag and repoll
|
||||
// until things settle out.
|
||||
bool got_wakeup_during_run_ ABSL_GUARDED_BY(mu_) = false; |
||||
// Handle for long waits. Allows a very small weak pointer type object to
|
||||
// queue for wakeups while Activity may be deleted earlier.
|
||||
Handle* handle_ ABSL_GUARDED_BY(mu_) = nullptr; |
||||
// Set during RunLoop to the Activity that's executing.
|
||||
// Being set implies that mu_ is held.
|
||||
static GPR_THREAD_LOCAL(Activity*) g_current_activity_; |
||||
}; |
||||
|
||||
// Owned pointer to one Activity.
|
||||
using ActivityPtr = std::unique_ptr<Activity, Activity::Deleter>; |
||||
|
||||
namespace promise_detail { |
||||
|
||||
template <typename Context> |
||||
class ContextHolder { |
||||
public: |
||||
explicit ContextHolder(Context value) : value_(std::move(value)) {} |
||||
Context* GetContext() { return &value_; } |
||||
|
||||
private: |
||||
Context value_; |
||||
}; |
||||
|
||||
template <typename Context> |
||||
class ContextHolder<Context*> { |
||||
public: |
||||
explicit ContextHolder(Context* value) : value_(value) {} |
||||
Context* GetContext() { return value_; } |
||||
|
||||
private: |
||||
Context* value_; |
||||
}; |
||||
|
||||
template <typename... Contexts> |
||||
class EnterContexts : public promise_detail::Context<Contexts>... { |
||||
public: |
||||
explicit EnterContexts(Contexts*... contexts) |
||||
: promise_detail::Context<Contexts>(contexts)... {} |
||||
}; |
||||
|
||||
// Implementation details for an Activity of an arbitrary type of promise.
|
||||
template <class F, class CallbackScheduler, class OnDone, typename... Contexts> |
||||
class PromiseActivity final |
||||
: public Activity, |
||||
private promise_detail::ContextHolder<Contexts>... { |
||||
public: |
||||
using Factory = PromiseFactory<void, F>; |
||||
PromiseActivity(F promise_factory, CallbackScheduler callback_scheduler, |
||||
OnDone on_done, Contexts... contexts) |
||||
: Activity(), |
||||
ContextHolder<Contexts>(std::move(contexts))..., |
||||
callback_scheduler_(std::move(callback_scheduler)), |
||||
on_done_(std::move(on_done)) { |
||||
// Lock, construct an initial promise from the factory, and step it.
|
||||
// This may hit a waiter, which could expose our this pointer to other
|
||||
// threads, meaning we do need to hold this mutex even though we're still
|
||||
// constructing.
|
||||
mu_.Lock(); |
||||
auto status = Start(Factory(std::move(promise_factory))); |
||||
mu_.Unlock(); |
||||
// We may complete immediately.
|
||||
if (status.has_value()) { |
||||
on_done_(std::move(*status)); |
||||
} |
||||
} |
||||
|
||||
~PromiseActivity() override { |
||||
// We shouldn't destruct without calling Cancel() first, and that must get
|
||||
// us to be done_, so we assume that and have no logic to destruct the
|
||||
// promise here.
|
||||
GPR_ASSERT(done_); |
||||
} |
||||
|
||||
size_t Size() override { return sizeof(*this); } |
||||
|
||||
void Cancel() final { |
||||
bool was_done; |
||||
{ |
||||
MutexLock lock(&mu_); |
||||
// Check if we were done, and flag done.
|
||||
was_done = done_; |
||||
if (!done_) MarkDone(); |
||||
} |
||||
// If we were not done, then call the on_done callback.
|
||||
if (!was_done) { |
||||
on_done_(absl::CancelledError()); |
||||
} |
||||
} |
||||
|
||||
private: |
||||
// Wakeup this activity. Arrange to poll the activity again at a convenient
|
||||
// time: this could be inline if it's deemed safe, or it could be by passing
|
||||
// the activity to an external threadpool to run. If the activity is already
|
||||
// running on this thread, a note is taken of such and the activity is
|
||||
// repolled if it doesn't complete.
|
||||
void Wakeup() final { |
||||
// If there's no active activity, we can just run inline.
|
||||
if (!Activity::have_current()) { |
||||
Step(); |
||||
WakeupComplete(); |
||||
return; |
||||
} |
||||
// If there is an active activity, but hey it's us, flag that and we'll loop
|
||||
// in RunLoop (that's calling from above here!).
|
||||
if (Activity::is_current()) { |
||||
WakeupCurrent(); |
||||
WakeupComplete(); |
||||
return; |
||||
} |
||||
// Can't safely run, so ask to run later.
|
||||
callback_scheduler_([this]() { |
||||
this->Step(); |
||||
this->WakeupComplete(); |
||||
}); |
||||
} |
||||
|
||||
// Drop a wakeup
|
||||
void Drop() final { this->WakeupComplete(); } |
||||
|
||||
// Notification that we're no longer executing - it's ok to destruct the
|
||||
// promise.
|
||||
void MarkDone() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_) { |
||||
GPR_ASSERT(!done_); |
||||
done_ = true; |
||||
Destruct(&promise_holder_.promise); |
||||
} |
||||
|
||||
// In response to Wakeup, run the Promise state machine again until it
|
||||
// settles. Then check for completion, and if we have completed, call on_done.
|
||||
void Step() ABSL_LOCKS_EXCLUDED(mu_) { |
||||
// Poll the promise until things settle out under a lock.
|
||||
mu_.Lock(); |
||||
if (done_) { |
||||
// We might get some spurious wakeups after finishing.
|
||||
mu_.Unlock(); |
||||
return; |
||||
} |
||||
auto status = RunStep(); |
||||
mu_.Unlock(); |
||||
if (status.has_value()) { |
||||
on_done_(std::move(*status)); |
||||
} |
||||
} |
||||
|
||||
// The main body of a step: set the current activity, and any contexts, and
|
||||
// then run the main polling loop. Contained in a function by itself in order
|
||||
// to keep the scoping rules a little easier in Step().
|
||||
absl::optional<absl::Status> RunStep() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_) { |
||||
ScopedActivity scoped_activity(this); |
||||
EnterContexts<Contexts...> contexts( |
||||
static_cast<ContextHolder<Contexts>*>(this)->GetContext()...); |
||||
return StepLoop(); |
||||
} |
||||
|
||||
// Similarly to RunStep, but additionally construct the promise from a promise
|
||||
// factory before entering the main loop. Called once from the constructor.
|
||||
absl::optional<absl::Status> Start(Factory promise_factory) |
||||
ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_) { |
||||
ScopedActivity scoped_activity(this); |
||||
EnterContexts<Contexts...> contexts( |
||||
static_cast<ContextHolder<Contexts>*>(this)->GetContext()...); |
||||
Construct(&promise_holder_.promise, promise_factory.Once()); |
||||
return StepLoop(); |
||||
} |
||||
|
||||
// Until there are no wakeups from within and the promise is incomplete: poll
|
||||
// the promise.
|
||||
absl::optional<absl::Status> StepLoop() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_) { |
||||
GPR_ASSERT(is_current()); |
||||
do { |
||||
// Run the promise.
|
||||
GPR_ASSERT(!done_); |
||||
auto r = promise_holder_.promise(); |
||||
if (auto* status = absl::get_if<kPollReadyIdx>(&r)) { |
||||
// If complete, destroy the promise, flag done, and exit this loop.
|
||||
MarkDone(); |
||||
return IntoStatus(status); |
||||
} |
||||
// Continue looping til no wakeups occur.
|
||||
} while (got_wakeup()); |
||||
return {}; |
||||
} |
||||
|
||||
using Promise = typename Factory::Promise; |
||||
// We wrap the promise in a union to allow control over the construction
|
||||
// simultaneously with annotating mutex requirements and noting that the
|
||||
// promise contained may not use any memory.
|
||||
union PromiseHolder { |
||||
PromiseHolder() {} |
||||
~PromiseHolder() {} |
||||
GPR_NO_UNIQUE_ADDRESS Promise promise; |
||||
}; |
||||
GPR_NO_UNIQUE_ADDRESS PromiseHolder promise_holder_ ABSL_GUARDED_BY(mu_); |
||||
// Schedule callbacks on some external executor.
|
||||
GPR_NO_UNIQUE_ADDRESS CallbackScheduler callback_scheduler_; |
||||
// Callback on completion of the promise.
|
||||
GPR_NO_UNIQUE_ADDRESS OnDone on_done_; |
||||
// Has execution completed?
|
||||
GPR_NO_UNIQUE_ADDRESS bool done_ ABSL_GUARDED_BY(mu_) = false; |
||||
}; |
||||
|
||||
} // namespace promise_detail
|
||||
|
||||
// Given a functor that returns a promise (a promise factory), a callback for
|
||||
// completion, and a callback scheduler, construct an activity.
|
||||
template <typename Factory, typename CallbackScheduler, typename OnDone, |
||||
typename... Contexts> |
||||
ActivityPtr MakeActivity(Factory promise_factory, |
||||
CallbackScheduler callback_scheduler, OnDone on_done, |
||||
Contexts... contexts) { |
||||
return ActivityPtr( |
||||
new promise_detail::PromiseActivity<Factory, CallbackScheduler, OnDone, |
||||
Contexts...>( |
||||
std::move(promise_factory), std::move(callback_scheduler), |
||||
std::move(on_done), std::move(contexts)...)); |
||||
} |
||||
|
||||
// A callback scheduler that simply crashes
|
||||
struct NoCallbackScheduler { |
||||
template <typename F> |
||||
void operator()(F) { |
||||
abort(); |
||||
} |
||||
}; |
||||
|
||||
} // namespace grpc_core
|
||||
|
||||
#endif // GRPC_CORE_LIB_PROMISE_ACTIVITY_H
|
@ -0,0 +1,72 @@ |
||||
// Copyright 2021 gRPC authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef GRPC_CORE_LIB_PROMISE_WAIT_SET_H |
||||
#define GRPC_CORE_LIB_PROMISE_WAIT_SET_H |
||||
|
||||
#include <grpc/impl/codegen/port_platform.h> |
||||
|
||||
#include "absl/container/flat_hash_set.h" |
||||
#include "src/core/lib/promise/activity.h" |
||||
|
||||
namespace grpc_core { |
||||
|
||||
// Helper type that can be used to enqueue many Activities waiting for some
|
||||
// external state.
|
||||
// Typically the external state should be guarded by mu_, and a call to
|
||||
// WakeAllAndUnlock should be made when the state changes.
|
||||
// Promises should bottom out polling inside pending(), which will register for
|
||||
// wakeup and return Pending().
|
||||
// Queues handles to Activities, and not Activities themselves, meaning that if
|
||||
// an Activity is destroyed prior to wakeup we end up holding only a small
|
||||
// amount of memory (around 16 bytes + malloc overhead) until the next wakeup
|
||||
// occurs.
|
||||
class WaitSet final { |
||||
using WakerSet = absl::flat_hash_set<Waker>; |
||||
|
||||
public: |
||||
// Register for wakeup, return Pending(). If state is not ready to proceed,
|
||||
// Promises should bottom out here.
|
||||
Pending AddPending(Waker waker) { |
||||
pending_.emplace(std::move(waker)); |
||||
return Pending(); |
||||
} |
||||
|
||||
class WakeupSet { |
||||
public: |
||||
void Wakeup() { |
||||
while (!wakeup_.empty()) { |
||||
wakeup_.extract(wakeup_.begin()).value().Wakeup(); |
||||
} |
||||
} |
||||
|
||||
private: |
||||
friend class WaitSet; |
||||
explicit WakeupSet(WakerSet&& wakeup) |
||||
: wakeup_(std::forward<WakerSet>(wakeup)) {} |
||||
WakerSet wakeup_; |
||||
}; |
||||
|
||||
GRPC_MUST_USE_RESULT WakeupSet TakeWakeupSet() { |
||||
return WakeupSet(std::move(pending_)); |
||||
} |
||||
|
||||
private: |
||||
// Handles to activities that need to be awoken.
|
||||
WakerSet pending_; |
||||
}; |
||||
|
||||
} // namespace grpc_core
|
||||
|
||||
#endif // GRPC_CORE_LIB_PROMISE_WAIT_SET_H
|
@ -0,0 +1,258 @@ |
||||
// Copyright 2021 gRPC authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "src/core/lib/promise/activity.h" |
||||
#include <gmock/gmock.h> |
||||
#include <gtest/gtest.h> |
||||
#include "src/core/lib/promise/join.h" |
||||
#include "src/core/lib/promise/promise.h" |
||||
#include "src/core/lib/promise/seq.h" |
||||
#include "src/core/lib/promise/wait_set.h" |
||||
|
||||
using testing::_; |
||||
using testing::Mock; |
||||
using testing::MockFunction; |
||||
using testing::SaveArg; |
||||
using testing::StrictMock; |
||||
|
||||
namespace grpc_core { |
||||
|
||||
class MockCallbackScheduler { |
||||
public: |
||||
MOCK_METHOD(void, Schedule, (std::function<void()>)); |
||||
}; |
||||
|
||||
// A simple Barrier type: stalls progress until it is 'cleared'.
|
||||
class Barrier { |
||||
public: |
||||
struct Result {}; |
||||
|
||||
Promise<Result> Wait() { |
||||
return [this]() -> Poll<Result> { |
||||
absl::MutexLock lock(&mu_); |
||||
if (cleared_) { |
||||
return Result{}; |
||||
} else { |
||||
return wait_set_.AddPending(Activity::current()->MakeOwningWaker()); |
||||
} |
||||
}; |
||||
} |
||||
|
||||
void Clear() { |
||||
mu_.Lock(); |
||||
cleared_ = true; |
||||
auto wakeup = wait_set_.TakeWakeupSet(); |
||||
mu_.Unlock(); |
||||
wakeup.Wakeup(); |
||||
} |
||||
|
||||
private: |
||||
absl::Mutex mu_; |
||||
WaitSet wait_set_ ABSL_GUARDED_BY(mu_); |
||||
bool cleared_ ABSL_GUARDED_BY(mu_) = false; |
||||
}; |
||||
|
||||
// A simple Barrier type: stalls progress until it is 'cleared'.
|
||||
// This variant supports only a single waiter.
|
||||
class SingleBarrier { |
||||
public: |
||||
struct Result {}; |
||||
|
||||
Promise<Result> Wait() { |
||||
return [this]() -> Poll<Result> { |
||||
absl::MutexLock lock(&mu_); |
||||
if (cleared_) { |
||||
return Result{}; |
||||
} else { |
||||
waker_ = Activity::current()->MakeOwningWaker(); |
||||
return Pending(); |
||||
} |
||||
}; |
||||
} |
||||
|
||||
void Clear() { |
||||
mu_.Lock(); |
||||
cleared_ = true; |
||||
auto waker = std::move(waker_); |
||||
mu_.Unlock(); |
||||
waker.Wakeup(); |
||||
} |
||||
|
||||
private: |
||||
absl::Mutex mu_; |
||||
Waker waker_ ABSL_GUARDED_BY(mu_); |
||||
bool cleared_ ABSL_GUARDED_BY(mu_) = false; |
||||
}; |
||||
|
||||
TEST(ActivityTest, ImmediatelyCompleteWithSuccess) { |
||||
StrictMock<MockFunction<void(absl::Status)>> on_done; |
||||
EXPECT_CALL(on_done, Call(absl::OkStatus())); |
||||
MakeActivity( |
||||
[] { return [] { return absl::OkStatus(); }; }, NoCallbackScheduler(), |
||||
[&on_done](absl::Status status) { on_done.Call(std::move(status)); }); |
||||
} |
||||
|
||||
TEST(ActivityTest, ImmediatelyCompleteWithFailure) { |
||||
StrictMock<MockFunction<void(absl::Status)>> on_done; |
||||
EXPECT_CALL(on_done, Call(absl::CancelledError())); |
||||
MakeActivity( |
||||
[] { return [] { return absl::CancelledError(); }; }, |
||||
NoCallbackScheduler(), |
||||
[&on_done](absl::Status status) { on_done.Call(std::move(status)); }); |
||||
} |
||||
|
||||
TEST(ActivityTest, DropImmediately) { |
||||
StrictMock<MockFunction<void(absl::Status)>> on_done; |
||||
EXPECT_CALL(on_done, Call(absl::CancelledError())); |
||||
MakeActivity( |
||||
[] { return []() -> Poll<absl::Status> { return Pending(); }; }, |
||||
NoCallbackScheduler(), |
||||
[&on_done](absl::Status status) { on_done.Call(std::move(status)); }); |
||||
} |
||||
|
||||
TEST(ActivityTest, Cancel) { |
||||
StrictMock<MockFunction<void(absl::Status)>> on_done; |
||||
auto activity = MakeActivity( |
||||
[] { return []() -> Poll<absl::Status> { return Pending(); }; }, |
||||
NoCallbackScheduler(), |
||||
[&on_done](absl::Status status) { on_done.Call(std::move(status)); }); |
||||
EXPECT_CALL(on_done, Call(absl::CancelledError())); |
||||
activity->Cancel(); |
||||
Mock::VerifyAndClearExpectations(&on_done); |
||||
activity.reset(); |
||||
} |
||||
|
||||
template <typename B> |
||||
class BarrierTest : public testing::Test { |
||||
public: |
||||
using Type = B; |
||||
}; |
||||
|
||||
using BarrierTestTypes = testing::Types<Barrier, SingleBarrier>; |
||||
TYPED_TEST_SUITE(BarrierTest, BarrierTestTypes); |
||||
|
||||
TYPED_TEST(BarrierTest, Barrier) { |
||||
typename TestFixture::Type b; |
||||
StrictMock<MockFunction<void(absl::Status)>> on_done; |
||||
auto activity = MakeActivity( |
||||
[&b] { |
||||
return Seq(b.Wait(), [](typename TestFixture::Type::Result) { |
||||
return absl::OkStatus(); |
||||
}); |
||||
}, |
||||
NoCallbackScheduler(), |
||||
[&on_done](absl::Status status) { on_done.Call(std::move(status)); }); |
||||
// Clearing the barrier should let the activity proceed to return a result.
|
||||
EXPECT_CALL(on_done, Call(absl::OkStatus())); |
||||
b.Clear(); |
||||
} |
||||
|
||||
TYPED_TEST(BarrierTest, BarrierPing) { |
||||
typename TestFixture::Type b1; |
||||
typename TestFixture::Type b2; |
||||
StrictMock<MockFunction<void(absl::Status)>> on_done1; |
||||
StrictMock<MockFunction<void(absl::Status)>> on_done2; |
||||
MockCallbackScheduler scheduler; |
||||
auto activity1 = MakeActivity( |
||||
[&b1, &b2] { |
||||
return Seq(b1.Wait(), [&b2](typename TestFixture::Type::Result) { |
||||
// Clear the barrier whilst executing an activity
|
||||
b2.Clear(); |
||||
return absl::OkStatus(); |
||||
}); |
||||
}, |
||||
[&scheduler](std::function<void()> f) { scheduler.Schedule(f); }, |
||||
[&on_done1](absl::Status status) { on_done1.Call(std::move(status)); }); |
||||
auto activity2 = MakeActivity( |
||||
[&b2] { |
||||
return Seq(b2.Wait(), [](typename TestFixture::Type::Result) { |
||||
return absl::OkStatus(); |
||||
}); |
||||
}, |
||||
[&scheduler](std::function<void()> f) { scheduler.Schedule(f); }, |
||||
[&on_done2](absl::Status status) { on_done2.Call(std::move(status)); }); |
||||
EXPECT_CALL(on_done1, Call(absl::OkStatus())); |
||||
// Since barrier triggers inside activity1 promise, activity2 wakeup will be
|
||||
// scheduled from a callback.
|
||||
std::function<void()> cb; |
||||
EXPECT_CALL(scheduler, Schedule(_)).WillOnce(SaveArg<0>(&cb)); |
||||
b1.Clear(); |
||||
Mock::VerifyAndClearExpectations(&on_done1); |
||||
EXPECT_CALL(on_done2, Call(absl::OkStatus())); |
||||
cb(); |
||||
} |
||||
|
||||
TYPED_TEST(BarrierTest, WakeSelf) { |
||||
typename TestFixture::Type b; |
||||
StrictMock<MockFunction<void(absl::Status)>> on_done; |
||||
EXPECT_CALL(on_done, Call(absl::OkStatus())); |
||||
MakeActivity( |
||||
[&b] { |
||||
return Seq(Join(b.Wait(), |
||||
[&b] { |
||||
b.Clear(); |
||||
return 1; |
||||
}), |
||||
[](std::tuple<typename TestFixture::Type::Result, int>) { |
||||
return absl::OkStatus(); |
||||
}); |
||||
}, |
||||
NoCallbackScheduler(), |
||||
[&on_done](absl::Status status) { on_done.Call(std::move(status)); }); |
||||
} |
||||
|
||||
TYPED_TEST(BarrierTest, WakeAfterDestruction) { |
||||
typename TestFixture::Type b; |
||||
{ |
||||
StrictMock<MockFunction<void(absl::Status)>> on_done; |
||||
EXPECT_CALL(on_done, Call(absl::CancelledError())); |
||||
MakeActivity( |
||||
[&b] { |
||||
return Seq(b.Wait(), [](typename TestFixture::Type::Result) { |
||||
return absl::OkStatus(); |
||||
}); |
||||
}, |
||||
NoCallbackScheduler(), |
||||
[&on_done](absl::Status status) { on_done.Call(std::move(status)); }); |
||||
} |
||||
b.Clear(); |
||||
} |
||||
|
||||
struct TestContext { |
||||
bool* done; |
||||
}; |
||||
template <> |
||||
struct ContextType<TestContext> {}; |
||||
|
||||
TEST(ActivityTest, WithContext) { |
||||
bool done = false; |
||||
StrictMock<MockFunction<void(absl::Status)>> on_done; |
||||
EXPECT_CALL(on_done, Call(absl::OkStatus())); |
||||
MakeActivity( |
||||
[] { |
||||
*GetContext<TestContext>()->done = true; |
||||
return Immediate(absl::OkStatus()); |
||||
}, |
||||
NoCallbackScheduler(), |
||||
[&on_done](absl::Status status) { on_done.Call(std::move(status)); }, |
||||
TestContext{&done}); |
||||
EXPECT_TRUE(done); |
||||
} |
||||
|
||||
} // namespace grpc_core
|
||||
|
||||
int main(int argc, char** argv) { |
||||
::testing::InitGoogleTest(&argc, argv); |
||||
return RUN_ALL_TESTS(); |
||||
} |
Loading…
Reference in new issue