blob: bd92d6319484ca36736e06142f0f539dad7c991d [file] [log] [blame]
// Copyright 2017 The Chromium 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 "third_party/blink/renderer/modules/service_worker/service_worker_event_queue.h"
#include "base/bind.h"
#include "base/containers/contains.h"
#include "base/time/default_tick_clock.h"
#include "base/time/time.h"
#include "third_party/blink/public/mojom/service_worker/service_worker_event_status.mojom-blink.h"
#include "third_party/blink/renderer/platform/wtf/functional.h"
namespace blink {
// static
constexpr base::TimeDelta ServiceWorkerEventQueue::kEventTimeout;
constexpr base::TimeDelta ServiceWorkerEventQueue::kUpdateInterval;
ServiceWorkerEventQueue::StayAwakeToken::StayAwakeToken(
base::WeakPtr<ServiceWorkerEventQueue> event_queue)
: event_queue_(std::move(event_queue)) {
DCHECK(event_queue_);
event_queue_->ResetIdleTimeout();
event_queue_->num_of_stay_awake_tokens_++;
}
ServiceWorkerEventQueue::StayAwakeToken::~StayAwakeToken() {
// If |event_queue_| has already been destroyed, it means the worker thread
// has already been killed.
if (!event_queue_)
return;
DCHECK_GT(event_queue_->num_of_stay_awake_tokens_, 0);
event_queue_->num_of_stay_awake_tokens_--;
if (!event_queue_->HasInflightEvent())
event_queue_->OnNoInflightEvent();
}
ServiceWorkerEventQueue::ServiceWorkerEventQueue(
BeforeStartEventCallback before_start_event_callback,
base::RepeatingClosure idle_callback,
scoped_refptr<base::SequencedTaskRunner> task_runner)
: ServiceWorkerEventQueue(std::move(before_start_event_callback),
std::move(idle_callback),
std::move(task_runner),
base::DefaultTickClock::GetInstance()) {}
ServiceWorkerEventQueue::ServiceWorkerEventQueue(
BeforeStartEventCallback before_start_event_callback,
base::RepeatingClosure idle_callback,
scoped_refptr<base::SequencedTaskRunner> task_runner,
const base::TickClock* tick_clock)
: task_runner_(std::move(task_runner)),
before_start_event_callback_(std::move(before_start_event_callback)),
idle_callback_(std::move(idle_callback)),
tick_clock_(tick_clock) {}
ServiceWorkerEventQueue::~ServiceWorkerEventQueue() {
// Abort all callbacks.
for (auto& event : all_events_) {
std::move(event.value->abort_callback)
.Run(blink::mojom::ServiceWorkerEventStatus::ABORTED);
}
}
void ServiceWorkerEventQueue::Start() {
DCHECK(!timer_.IsRunning());
if (!HasInflightEvent() && !HasScheduledIdleCallback()) {
// If no event happens until Start(), the idle callback should be scheduled.
OnNoInflightEvent();
}
timer_.Start(FROM_HERE, kUpdateInterval,
WTF::BindRepeating(&ServiceWorkerEventQueue::UpdateStatus,
WTF::Unretained(this)));
}
void ServiceWorkerEventQueue::EnqueueNormal(
int event_id,
StartCallback start_callback,
AbortCallback abort_callback,
base::Optional<base::TimeDelta> custom_timeout) {
EnqueueEvent(std::make_unique<Event>(
event_id, Event::Type::Normal, std::move(start_callback),
std::move(abort_callback), std::move(custom_timeout)));
}
void ServiceWorkerEventQueue::EnqueuePending(
int event_id,
StartCallback start_callback,
AbortCallback abort_callback,
base::Optional<base::TimeDelta> custom_timeout) {
EnqueueEvent(std::make_unique<Event>(
event_id, Event::Type::Pending, std::move(start_callback),
std::move(abort_callback), std::move(custom_timeout)));
}
void ServiceWorkerEventQueue::EnqueueOffline(
int event_id,
StartCallback start_callback,
AbortCallback abort_callback,
base::Optional<base::TimeDelta> custom_timeout) {
EnqueueEvent(std::make_unique<ServiceWorkerEventQueue::Event>(
event_id, ServiceWorkerEventQueue::Event::Type::Offline,
std::move(start_callback), std::move(abort_callback),
std::move(custom_timeout)));
}
bool ServiceWorkerEventQueue::CanStartEvent(const Event& event) const {
if (running_event_type_ == RunningEventType::kNone) {
DCHECK(!HasInflightEvent());
return true;
}
if (event.type == Event::Type::Offline)
return running_event_type_ == RunningEventType::kOffline;
return running_event_type_ == RunningEventType::kOnline;
}
std::map<int, std::unique_ptr<ServiceWorkerEventQueue::Event>>&
ServiceWorkerEventQueue::GetActiveEventQueue() {
if (running_event_type_ == RunningEventType::kNone) {
// Either online events or offline events can be started when inflight
// events don't exist. If online events exist in the queue, prioritize
// online events.
return queued_online_events_.empty() ? queued_offline_events_
: queued_online_events_;
}
if (running_event_type_ == RunningEventType::kOffline)
return queued_offline_events_;
return queued_online_events_;
}
void ServiceWorkerEventQueue::EnqueueEvent(std::unique_ptr<Event> event) {
DCHECK(event->type != Event::Type::Pending || did_idle_timeout());
DCHECK(!HasEvent(event->event_id));
DCHECK(!HasEventInQueue(event->event_id));
bool can_start_processing_events =
!processing_events_ && event->type != Event::Type::Pending;
// Start counting the timer when an event is enqueued.
all_events_.insert(
event->event_id,
std::make_unique<EventInfo>(
tick_clock_->NowTicks() +
event->custom_timeout.value_or(kEventTimeout),
WTF::Bind(std::move(event->abort_callback), event->event_id)));
auto& queue = event->type == Event::Type::Offline ? queued_offline_events_
: queued_online_events_;
queue.emplace(event->event_id, std::move(event));
if (!can_start_processing_events)
return;
ResetIdleTimeout();
ProcessEvents();
}
void ServiceWorkerEventQueue::ProcessEvents() {
DCHECK(!processing_events_);
processing_events_ = true;
auto& queue = GetActiveEventQueue();
while (!queue.empty() && CanStartEvent(*queue.begin()->second)) {
int event_id = queue.begin()->first;
std::unique_ptr<Event> event = std::move(queue.begin()->second);
queue.erase(queue.begin());
StartEvent(event_id, std::move(event));
}
processing_events_ = false;
// We have to check HasInflightEvent() and may trigger
// OnNoInflightEvent() here because StartEvent() can call EndEvent()
// synchronously, and EndEvent() never triggers OnNoInflightEvent()
// while ProcessEvents() is running.
if (!HasInflightEvent())
OnNoInflightEvent();
}
void ServiceWorkerEventQueue::StartEvent(int event_id,
std::unique_ptr<Event> event) {
DCHECK(HasEvent(event_id));
running_event_type_ = event->type == Event::Type::Offline
? RunningEventType::kOffline
: RunningEventType::kOnline;
if (before_start_event_callback_)
before_start_event_callback_.Run(event->type == Event::Type::Offline);
std::move(event->start_callback).Run(event_id);
}
void ServiceWorkerEventQueue::EndEvent(int event_id) {
DCHECK(HasEvent(event_id));
all_events_.erase(event_id);
// Check |processing_events_| here because EndEvent() can be called
// synchronously in StartEvent(). We don't want to trigger
// OnNoInflightEvent() while ProcessEvents() is running.
if (!processing_events_ && !HasInflightEvent())
OnNoInflightEvent();
}
bool ServiceWorkerEventQueue::HasEvent(int event_id) const {
return all_events_.find(event_id) != all_events_.end();
}
bool ServiceWorkerEventQueue::HasEventInQueue(int event_id) const {
return (base::Contains(queued_online_events_, event_id) ||
base::Contains(queued_offline_events_, event_id));
}
std::unique_ptr<ServiceWorkerEventQueue::StayAwakeToken>
ServiceWorkerEventQueue::CreateStayAwakeToken() {
return std::make_unique<ServiceWorkerEventQueue::StayAwakeToken>(
weak_factory_.GetWeakPtr());
}
void ServiceWorkerEventQueue::SetIdleDelay(base::TimeDelta idle_delay) {
idle_delay_ = idle_delay;
if (HasInflightEvent())
return;
if (did_idle_timeout()) {
// The idle callback has already been called. It should not be called again
// until this worker becomes active.
return;
}
// There should be a scheduled idle callback because this is now in the idle
// delay. The idle callback will be rescheduled based on the new idle delay.
DCHECK(HasScheduledIdleCallback());
idle_callback_handle_.Cancel();
// Calculate the updated time of when the |idle_callback_| should be invoked.
DCHECK(!last_no_inflight_event_.is_null());
auto new_idle_callback_time = last_no_inflight_event_ + idle_delay;
base::TimeDelta delta_until_idle =
new_idle_callback_time - tick_clock_->NowTicks();
if (delta_until_idle <= base::TimeDelta::FromSeconds(0)) {
// The new idle delay is shorter than the previous idle delay, and the idle
// time has been already passed. Let's run the idle callback immediately.
TriggerIdleCallback();
return;
}
// Let's schedule the idle callback in |delta_until_idle|.
ScheduleIdleCallback(delta_until_idle);
}
void ServiceWorkerEventQueue::UpdateStatus() {
base::TimeTicks now = tick_clock_->NowTicks();
// Construct a new map because WTF::HashMap doesn't support deleting elements
// while iterating.
HashMap<int /* event_id */, std::unique_ptr<EventInfo>> new_all_events;
bool should_idle_delay_to_be_zero = false;
// Time out all events exceeding `kEventTimeout`.
for (auto& it : all_events_) {
// Check if the event has timed out.
int event_id = it.key;
std::unique_ptr<EventInfo>& event_info = it.value;
if (event_info->expiration_time > now) {
new_all_events.insert(event_id, std::move(event_info));
continue;
}
// The event may still be in one of the queues when it timed out. Try to
// remove the event from both.
queued_online_events_.erase(event_id);
queued_offline_events_.erase(event_id);
// Run the abort callback.
std::move(event_info->abort_callback)
.Run(blink::mojom::ServiceWorkerEventStatus::TIMEOUT);
should_idle_delay_to_be_zero = true;
}
all_events_.swap(new_all_events);
// Set idle delay to zero if needed.
if (should_idle_delay_to_be_zero) {
// Inflight events might be timed out and there might be no inflight event
// at this point.
if (!HasInflightEvent()) {
OnNoInflightEvent();
}
// Shut down the worker as soon as possible since the worker may have gone
// into bad state.
SetIdleDelay(base::TimeDelta::FromSeconds(0));
}
}
void ServiceWorkerEventQueue::ScheduleIdleCallback(base::TimeDelta delay) {
DCHECK(!HasInflightEvent());
DCHECK(!HasScheduledIdleCallback());
// WTF::Unretained() is safe because the task runner will be destroyed
// before |this| is destroyed at ServiceWorkerGlobalScope::Dispose().
idle_callback_handle_ = PostDelayedCancellableTask(
*task_runner_, FROM_HERE,
WTF::Bind(&ServiceWorkerEventQueue::TriggerIdleCallback,
WTF::Unretained(this)),
delay);
}
void ServiceWorkerEventQueue::TriggerIdleCallback() {
DCHECK(!HasInflightEvent());
DCHECK(!HasScheduledIdleCallback());
DCHECK(!did_idle_timeout_);
did_idle_timeout_ = true;
idle_callback_.Run();
}
void ServiceWorkerEventQueue::OnNoInflightEvent() {
DCHECK(!HasInflightEvent());
running_event_type_ = RunningEventType::kNone;
// There might be events in the queue because offline (or non-offline) events
// can be enqueued during running non-offline (or offline) events.
auto& queue = GetActiveEventQueue();
if (!queue.empty()) {
ProcessEvents();
return;
}
last_no_inflight_event_ = tick_clock_->NowTicks();
ScheduleIdleCallback(idle_delay_);
}
bool ServiceWorkerEventQueue::HasInflightEvent() const {
size_t num_queued_events =
queued_online_events_.size() + queued_offline_events_.size();
DCHECK_LE(num_queued_events, all_events_.size());
return all_events_.size() - num_queued_events > 0 ||
num_of_stay_awake_tokens_ > 0;
}
void ServiceWorkerEventQueue::ResetIdleTimeout() {
last_no_inflight_event_ = base::TimeTicks();
idle_callback_handle_.Cancel();
did_idle_timeout_ = false;
}
bool ServiceWorkerEventQueue::HasScheduledIdleCallback() const {
return idle_callback_handle_.IsActive();
}
int ServiceWorkerEventQueue::NextEventId() {
CHECK_LT(next_event_id_, std::numeric_limits<int>::max());
return next_event_id_++;
}
ServiceWorkerEventQueue::Event::Event(
int event_id,
ServiceWorkerEventQueue::Event::Type type,
StartCallback start_callback,
AbortCallback abort_callback,
base::Optional<base::TimeDelta> custom_timeout)
: event_id(event_id),
type(type),
start_callback(std::move(start_callback)),
abort_callback(std::move(abort_callback)),
custom_timeout(custom_timeout) {}
ServiceWorkerEventQueue::Event::~Event() = default;
ServiceWorkerEventQueue::EventInfo::EventInfo(
base::TimeTicks expiration_time,
base::OnceCallback<void(blink::mojom::ServiceWorkerEventStatus)>
abort_callback)
: expiration_time(expiration_time),
abort_callback(std::move(abort_callback)) {}
ServiceWorkerEventQueue::EventInfo::~EventInfo() = default;
} // namespace blink