| // Copyright 2022 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. |
| |
| #include "src/connectivity/wlan/drivers/lib/timer/cpp/include/wlan/drivers/timer/timer.h" |
| |
| #include <lib/async/time.h> |
| #include <lib/ddk/debug.h> |
| #include <lib/sync/completion.h> |
| |
| namespace wlan::drivers::timer { |
| |
| Timer::Timer(async_dispatcher_t* dispatcher, FunctionPtr callback, void* context) |
| : Timer(dispatcher, [=]() { callback(context); }) {} |
| |
| Timer::Timer(async_dispatcher_t* dispatcher, std::function<void()>&& callback) |
| : async_task_t{{ASYNC_STATE_INIT}, &Timer::Handler, 0}, |
| dispatcher_(dispatcher), |
| callback_(std::move(callback)) {} |
| |
| Timer::~Timer() { |
| zx_status_t status = Stop(); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "Failed to stop timer during destruction"); |
| } |
| } |
| |
| zx_status_t Timer::StartPeriodic(zx_duration_mono_t interval) { return Start(interval, true); } |
| |
| zx_status_t Timer::StartOneshot(zx_duration_mono_t delay) { return Start(delay, false); } |
| |
| zx_status_t Timer::Stop() { |
| // Make sure Start/Stop cannot be called from multiple threads at once. Doing so would open up |
| // for race conditions for the section below where we don't hold unlock handler_mutex_ and wait |
| // for handler completion. std::lock locks both std::unique_locks without deadlocks. |
| // Note: std::scoped_lock can cause a double unlock in this case, since need handler_mutex_ to be |
| // unlocked while start_stop_mutex_ is still locked. see https://fxbug.dev/42072819 for context. |
| std::unique_lock start_stop_lock(start_stop_mutex_, std::defer_lock); |
| std::unique_lock handler_lock(handler_mutex_, std::defer_lock); |
| std::lock(start_stop_lock, handler_lock); |
| |
| // Set is_periodic_ to false right away. This ensures that if Stop was called from the callback |
| // of a periodic timer (where scheduled_ would be false) it will not re-arm again. |
| is_periodic_ = false; |
| if (!scheduled_) { |
| return ZX_OK; |
| } |
| scheduled_ = false; |
| // Attempt to cancel the task. If this succeeds there is no risk of the timer handler being called |
| // and we don't need to wait for completion. |
| zx_status_t status = async_cancel_task(dispatcher_, this); |
| if (status == ZX_OK) { |
| return status; |
| } |
| if (status != ZX_ERR_NOT_FOUND) { |
| zxlogf(ERROR, "Failed to cancel task: %s", zx_status_get_string(status)); |
| return status; |
| } |
| // At this point we know the task is scheduled but we were not able to cancel it. This means |
| // that the only remaining possibility is that the task is about to run. It has been removed |
| // from the dispatcher task list but did not acquire the handler mutex yet. We know this because |
| // scheduled_ was still true and we hold the lock. By setting scheduled_ to false above we prevent |
| // the timer handler from calling the callback. It should short-circuit and signal the completion. |
| handler_lock.unlock(); |
| |
| status = sync_completion_wait(&finished_, ZX_TIME_INFINITE); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "Failed to wait for completion: %s", zx_status_get_string(status)); |
| return status; |
| } |
| return ZX_OK; |
| } |
| |
| zx_status_t Timer::Start(zx_duration_mono_t interval, bool periodic) { |
| if (interval < 0) { |
| // Negative intervals and delays don't really make sense. |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| // Calculate the deadline at this point to make sure that we get as close to the requested |
| // interval as possible. Acquiring the locks might block for a while, causing timer drift if we |
| // calculate the deadline when posting the task. |
| zx_instant_mono_t deadline = async_now(dispatcher_) + interval; |
| |
| // Make sure Start/Stop cannot be called from multiple threads at once. Doing so would open up |
| // for race conditions for the section below where we have to unlock handler_mutex_ and wait for |
| // handler completion. std::lock locks both mutexes without deadlocks. |
| // Note: std::scoped_lock can cause a double unlock in this case, since need handler_mutex_ to be |
| // unlocked while start_stop_mutex_ is still locked. see https://fxbug.dev/42072819 for context. |
| std::unique_lock start_stop_lock(start_stop_mutex_, std::defer_lock); |
| std::unique_lock handler_lock(handler_mutex_, std::defer_lock); |
| std::lock(start_stop_lock, handler_lock); |
| if (scheduled_) { |
| // If Start was called from the dispatcher thread and scheduled_ is true that means that the |
| // user called Start at least twice in the same callback, so we can safely cancel the previous |
| // task. If Start was called from another thread then the task has to be scheduled at this |
| // point. |
| scheduled_ = false; |
| is_periodic_ = false; |
| |
| zx_status_t status = async_cancel_task(dispatcher_, this); |
| if (status == ZX_ERR_NOT_FOUND) { |
| // We encountered the situation where the dispatcher has taken the task out its queue but the |
| // timer handler has not yet locked the mutex. We've set scheduled_ to false so once the timer |
| // handler is allowed to run it should immediately signal the completion and return. |
| handler_lock.unlock(); |
| status = sync_completion_wait(&finished_, ZX_TIME_INFINITE); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "Failed to wait for completion: %s", zx_status_get_string(status)); |
| return status; |
| } |
| handler_lock.lock(); |
| } else if (status != ZX_OK) { |
| zxlogf(ERROR, "Failed to cancel task: %s", zx_status_get_string(status)); |
| return status; |
| } |
| } |
| // At this point the timer must have stopped. Schedule it again. |
| this->deadline = deadline; |
| |
| sync_completion_reset(&finished_); |
| zx_status_t status = async_post_task(dispatcher_, this); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "Failed to post task: %s", zx_status_get_string(status)); |
| return status; |
| } |
| |
| // Only set these on success, otherwise a Stop might attempt to cancel a task that doesn't exist. |
| scheduled_ = true; |
| is_periodic_ = periodic; |
| interval_ = interval; |
| |
| return ZX_OK; |
| } |
| |
| void Timer::Handler(async_dispatcher_t* dispatcher, async_task_t* task, zx_status_t status) { |
| auto timer = static_cast<Timer*>(task); |
| |
| if (status != ZX_OK) { |
| if (status != ZX_ERR_CANCELED) { |
| // ZX_ERR_CANCELED is common enough that we don't need to log it, other errors are unexpected. |
| zxlogf(ERROR, "Timer task failed to run: %s", zx_status_get_string(status)); |
| } |
| if (timer) { |
| // Signal the completion here in case someone is waiting for it. |
| sync_completion_signal(&timer->finished_); |
| } |
| return; |
| } |
| |
| std::lock_guard lock(timer->handler_mutex_); |
| if (!timer->scheduled_) { |
| // Timer was stopped but the task could not be removed from the dispatcher. Signal completion |
| // and return without calling the callback, effectively stopping the timer. |
| sync_completion_signal(&timer->finished_); |
| return; |
| } |
| // Set scheduled_ to false here so that Start and Stop calls in the callback don't attempt to |
| // stop or wait for the timer to trigger. |
| timer->scheduled_ = false; |
| |
| // We intentionally keep the mutex held here to prevent tricky race conditions. This is fine since |
| // it's a recursive mutex. Calls to Start and Stop from the callback will still work while at the |
| // same time preventing other threads from getting through at the wrong time. |
| timer->callback_(); |
| |
| if (!timer->scheduled_ && timer->is_periodic_) { |
| // The periodic timer should only be restarted if scheduled_ is false, otherwise a new timer was |
| // started in the callback and we don't want to delay that timer by re-arming it here. |
| zx_status_t status = timer->Start(timer->interval_, timer->is_periodic_); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "Failed to re-arm periodic timer: %s", zx_status_get_string(status)); |
| } |
| } |
| } |
| |
| } // namespace wlan::drivers::timer |