blob: 0c143920d194f5cd4a57e2be3c1fd9713d44ef43 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 <condition_variable>
#include <exception>
#include <iterator>
#include <map>
#include <mutex>
#include <thread>
#include <utility>
#include "iceberg/util/scheduled_executor_internal.h"
namespace iceberg::internal {
// Kept alive so static destructors can still schedule tasks.
class SharedTimer {
public:
static SharedTimer& Instance() {
static auto* timer = new SharedTimer();
return *timer;
}
void Schedule(std::weak_ptr<ScheduledExecutorState> owner,
std::chrono::steady_clock::time_point deadline, ExecutorTask task,
std::optional<TaskSubmissionCallback> on_submission) {
std::lock_guard lock(mutex_);
// Equal deadlines keep insertion order.
auto it = tasks_.emplace(deadline, Task{.owner = std::move(owner),
.task = std::move(task),
.on_submission = std::move(on_submission)});
// Wake the timer only if its next deadline changed.
if (it == tasks_.begin()) {
cv_.notify_one();
}
}
void CancelAll(const std::weak_ptr<ScheduledExecutorState>& owner) {
Queue cancelled; // Release task captures outside the queue lock.
{
std::lock_guard lock(mutex_);
for (auto it = tasks_.begin(); it != tasks_.end();) {
auto next = std::next(it);
if (SameOwner(it->second.owner, owner)) {
cancelled.insert(tasks_.extract(it));
}
it = next;
}
}
if (!cancelled.empty()) {
cv_.notify_one();
}
}
bool IsTimerThread() const {
return timer_thread_.get_id() == std::this_thread::get_id();
}
private:
SharedTimer() : timer_thread_([this] { Run(); }) {}
struct Task {
std::weak_ptr<ScheduledExecutorState> owner;
ExecutorTask task;
std::optional<TaskSubmissionCallback> on_submission;
};
using Queue = std::multimap<std::chrono::steady_clock::time_point, Task>;
static bool SameOwner(const std::weak_ptr<ScheduledExecutorState>& a,
const std::weak_ptr<ScheduledExecutorState>& b) {
return !a.owner_before(b) && !b.owner_before(a);
}
void Run();
std::mutex mutex_;
std::condition_variable cv_;
Queue tasks_;
std::thread timer_thread_;
};
class ScheduledExecutorState
: public std::enable_shared_from_this<ScheduledExecutorState> {
public:
explicit ScheduledExecutorState(Executor& executor)
: executor_(executor), timer_(SharedTimer::Instance()) {}
ScheduledExecutorState(const ScheduledExecutorState&) = delete;
ScheduledExecutorState& operator=(const ScheduledExecutorState&) = delete;
void Schedule(std::chrono::steady_clock::time_point deadline, ExecutorTask task,
std::optional<TaskSubmissionCallback> on_submission) {
std::lock_guard lock(mutex_);
if (shutdown_) {
return;
}
timer_.Schedule(weak_from_this(), deadline, std::move(task),
std::move(on_submission));
}
void Shutdown() {
{
std::lock_guard lock(mutex_);
shutdown_ = true;
}
timer_.CancelAll(weak_from_this());
if (!timer_.IsTimerThread()) {
std::unique_lock lock(mutex_);
cv_.wait(lock, [this] { return active_dispatches_ == 0; });
}
}
private:
friend class SharedTimer;
void Dispatch(ExecutorTask task, std::optional<TaskSubmissionCallback> on_submission) {
{
std::lock_guard lock(mutex_);
// Shutdown may race with popping a task.
if (shutdown_) {
return;
}
++active_dispatches_;
}
struct DispatchGuard {
ScheduledExecutorState& state;
~DispatchGuard() {
std::lock_guard lock(state.mutex_);
if (--state.active_dispatches_ == 0) {
state.cv_.notify_all();
}
}
} guard{*this};
Status status;
try {
status = executor_.Submit(std::move(task));
} catch (const std::exception& e) {
status = UnknownError("Executor::Submit threw: {}", e.what());
} catch (...) {
status = UnknownError("Executor::Submit threw a non-standard exception");
}
if (!on_submission.has_value()) {
return;
}
try {
std::move (*on_submission)(std::move(status));
} catch (...) {
// Keep the shared timer running.
}
}
Executor& executor_;
SharedTimer& timer_;
std::mutex mutex_;
std::condition_variable cv_;
size_t active_dispatches_ = 0;
bool shutdown_ = false;
};
void SharedTimer::Run() {
while (true) {
Queue::node_type node;
{
std::unique_lock lock(mutex_);
cv_.wait(lock, [this] { return !tasks_.empty(); });
auto deadline = tasks_.begin()->first;
if (cv_.wait_until(lock, deadline, [this, deadline] {
return tasks_.empty() || tasks_.begin()->first != deadline;
})) {
continue;
}
node = tasks_.extract(tasks_.begin());
}
auto& task = node.mapped();
if (auto owner = task.owner.lock()) {
owner->Dispatch(std::move(task.task), std::move(task.on_submission));
}
}
}
ScheduledExecutor::ScheduledExecutor(Executor& executor)
: state_(std::make_shared<ScheduledExecutorState>(executor)) {}
ScheduledExecutor::~ScheduledExecutor() { Shutdown(); }
void ScheduledExecutor::Schedule(std::chrono::milliseconds delay, ExecutorTask task,
std::optional<TaskSubmissionCallback> on_submission) {
state_->Schedule(std::chrono::steady_clock::now() + delay, std::move(task),
std::move(on_submission));
}
void ScheduledExecutor::Shutdown() { state_->Shutdown(); }
} // namespace iceberg::internal