// Copyright 2024 The Centipede 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 // // https://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 "./centipede/periodic_action.h" #include #include #include #include #include "absl/base/thread_annotations.h" #include "absl/functional/any_invocable.h" #include "absl/synchronization/mutex.h" #include "absl/time/time.h" namespace fuzztest::internal { class PeriodicAction::Impl { public: Impl(absl::AnyInvocable action, PeriodicAction::Options options) : action_{std::move(action)}, options_{std::move(options)}, thread_{[this]() { RunLoop(); }} {} void Stop() { StopAsync(); // The run-loop should exit the next time it checks `stop_`. Note that if // the loop is currently in the middle of an invocation of `action_`, it // will wait for the invocation to finish, so we might block here for an // `action_`-dependent amount of time. if (thread_.joinable()) { thread_.join(); } } void StopAsync() { absl::MutexLock lock{&mu_}; stop_ = true; } void Nudge() { absl::MutexLock lock{&mu_}; nudge_ = true; } private: void RunLoop() { uint64_t iteration = 0; while (true) { SleepOrWakeEarly(options_.sleep_before_each(iteration)); const bool schedule = !nudge_ && !stop_; const bool nudge = nudge_; const bool stop = stop_; mu_.Unlock(); // NOTE: The caller might call `Stop()` immediately after one final // `Nudge()`: in that case we still should run the action, and only then // terminate the loop. This is in contrast to waking after sleeping the // full duration while the caller calls `Stop()` during that time: in that // case, we should NOT run the action and terminate the loop immediately. if (schedule || nudge) { action_(); } if (stop) { return; } ++iteration; } } void SleepOrWakeEarly(absl::Duration duration) ABSL_EXCLUSIVE_LOCK_FUNCTION(mu_) { mu_.Lock(); // NOTE: Reset only `nudge_`, but not `stop_`: nudging is transient and // can be activated repeatedly, the latter is persistent and can be // activated only once (repeated calls to `Stop()` are no-ops). nudge_ = false; mu_.Unlock(); const auto wake_early = [this]() { mu_.AssertReaderHeld(); return nudge_ || stop_; }; mu_.LockWhenWithTimeout(absl::Condition{&wake_early}, duration); mu_.AssertHeld(); } absl::AnyInvocable action_; PeriodicAction::Options options_; // WARNING!!! The order below is important. absl::Mutex mu_; bool nudge_ ABSL_GUARDED_BY(mu_) = false; bool stop_ ABSL_GUARDED_BY(mu_) = false; std::thread thread_; }; PeriodicAction::PeriodicAction( // absl::AnyInvocable action, Options options) : pimpl_{std::make_unique(std::move(action), std::move(options))} {} PeriodicAction::~PeriodicAction() { // NOTE: `pimpl_` will be null if this object has been moved to another one. if (pimpl_ != nullptr) pimpl_->Stop(); } void PeriodicAction::Stop() { pimpl_->Stop(); } void PeriodicAction::StopAsync() { pimpl_->StopAsync(); } void PeriodicAction::Nudge() { pimpl_->Nudge(); } // NOTE: Even though these are defaulted, they still must be defined here in the // .cc, because `Impl` is an incomplete type in the .h. PeriodicAction::PeriodicAction(PeriodicAction&&) = default; PeriodicAction& PeriodicAction::operator=(PeriodicAction&&) = default; } // namespace fuzztest::internal