Extension layer/service worker interactions

An extension background is the context that an extension runs on. It allows extensions to react to events or messages with specified instructions. Up until Manifest V2, there were two types of extension background pages, persistent background pages and non-persistent background pages. As part of Manifest V3, we are migrating extensions from the persistent/non-persistent background pages to service workers. Service worker is a web platform feature that forms the basis of app-like capabilities such as offline support, push notifications, and background sync. A service worker is an event-driven JavaScript program that runs in a worker thread. For a more detailed explanation, see the Service Workers documentation.

This document describes the assumptions the //extensions layer makes when relying on the service worker layer for registering/unregistering/startinga service worker or ensuring the service worker’s liveness.

Registration

When adding/loading an extension, ExtensionRegistrar::ActivateExtension is called which results in calling ServiceWorkerTaskQueue::ActivateExtension which calls ServiceWorkerContext::RegisterServiceWorker. During registration, script_url is set to the URL corresponding to the relative path from manifest.json's “background.service_worker” and scope is set to the extension root, i.e., chrome-extension://<extension_id>/.

When registering the service worker, the //extensions layer relies on the content layer’s guarantee that the registration is completed. OnRegistrationStored is the first observer function that can guarantee StartWorkerForScope can find the registration. After ServiceWorkerContextObserver::OnRegistrationStored, ServiceWorkerContext::StartWorkerForScope should be able to find the registration.

Unregistration

When an extension is removed/disabled/terminated, ExtensionRegistrar::DeactivateExtension is called which will call ServiceWorkerTaskQueue::DeactivateExtension. This will result in unregistering the service worker, by calling ServiceWorkerContext::UnregisterServiceWorker.

Registration/Unregistration failure

DidRegisterServiceWorker might fail, due to a few reasons: bad disk state, invalid service worker script. The recovery steps would depend on the use case.

DidUnregisterServiceWorker failure is rare because it does not involve any user-provided JS code.

When DidRegisterServiceWorker/DidUnregisterServiceWorker fails due to a disk error, the SW layer will try to wipe the whole SW database as the current implementation considers it a critical error.

Starting the service worker

A service worker is started when a pending task (e.g. an event dispatch) is run. A pending task is run only when all of the following conditions are met:

  • Service worker registration has completed.
  • Call to ServiceWorkerContext::StartWorkerForScope has returned.
  • Worker thread (in the renderer) has seen DidStartServiceWorkerContextOnWorkerThread.

ServiceWorkerTaskQueue starts a service worker via StartWorkerForScope. We note that, in the current code, StartWorkerForScope should be called every time before asking the worker to do something instead of relying on ServiceWorkerContextObserver::OnVersionStoppedRunning. The reason is that OnVersionStoppedRunning is called after the worker thread is actually terminated. As a result, we can not rely on OnVersionStoppedRunning to determine worker liveness. There are more fine-grained running status in the content layer: RUNNING, STOPPING and STOPPED. The listener is called when the worker’s state becomes STOPPED. When an event is dispatched to the worker, it should not be done when the worker is in STOPPPING state.

The flow of dispatching an event to a service worker is

1- Calling StartWorkerForScope regardless of its running status,

2- Dispatching an event to a service worker inside of the callback triggered from StartWorkerForScope synchronously.

In this way, we do not have to track whether the worker is running or not.

There are several possible reasons for StartWorkerForScope failure, such as process allocation failure, timeout of the script evaluation, and disk corruption.

Notifications

When starting a service worker, the //extensions layer wait for readiness notification from both the browser process and the renderer process. In the current code, after receiving both notifications and before OnVersionStoppedRunning, the //extensions layer assume that the SW is alive and can dispatch events to an extension service worker. As explained above, we should call StartWorkerForScope every time before asking the worker to do something instead of relying on OnVersionStoppedRunning. We plan to fix this in our code. Bug 1162193 tracks this fix.

Service worker’s liveness

The //extensions layer rely on the service worker layer to ensure the service worker’s liveness. We use EventAck IPC to ensure that the service worker is alive while an event is dispatched. This is performed in two steps:

1- An event is dispatched from the browser process to the renderer.

2- Renderer responds with EventAck to the browser process.

We ensure that between step 1 and step 2, we do not consider the service worker as “inactive”. We achieve this with workers, i.e., we call ServiceWorkerContext::StartingExternalRequest on step 1, and then we call ServiceWorkerContext::FinishedExternalRequest after step 2.

It is guaranteed that the worker will not be stopped between step 1 and step 2, as long as we use ServiceWorkerContext::StartingExternalRequest and ServiceWorkerContext::FinishedExternalRequest. The external request is a mechanism to keep the worker alive.