Skip to content

Commit

Permalink
Add documentation on servicification recipes
Browse files Browse the repository at this point in the history
This documentation captures insights we've gained during initial
servicifications; hopefully it will be useful for future servicifications.

Review-Url: https://codereview.chromium.org/2849463006
Cr-Commit-Position: refs/heads/master@{#468586}
  • Loading branch information
colinblundell authored and Commit bot committed May 2, 2017
1 parent 7c599d9 commit 77afe7a
Show file tree
Hide file tree
Showing 2 changed files with 307 additions and 0 deletions.
301 changes: 301 additions & 0 deletions docs/servicification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
# Servicification Strategies

This document captures strategies, hints, and best practices for solving typical
challenges enountered when converting existing Chromium
code to services. It is assumed that you have already read the high-level
documentation on [what a service is](/services).

If you're looking for Mojo documentation, please see the [general
Mojo documentation](/mojo) and/or the [documentation on converting Chrome IPC to
Mojo](/ipc).

Note that throughout the below document we link to CLs to illustrate the
strategies being made. Over the course of time code tends to shift, so it is
likely that the code on trunk does not exactly match what it was at the time of
the CLs. When necessary, use the CLs as a starting point for examining the
current state of the codebase with respect to these issues (e.g., exactly where
a service is embedded within the content layer).

[TOC]

## Questions to Answer When Getting Started

For the basic nuts and bolts of how to create a new service, see [the
documentation on adding a new service](/services#Adding-a-new-service). This
section gives questions that you should answer in order to shape the design of
your service, as well as hints as to which answers make sense given your
situation.

### Is your service global or per-BrowserContext?
The Service Manager can either:

- create one service instance per user ID or
- field all connection requests for a given service via the same instance

Which of these policies the Service Manager employs is determined by the
contents of your service manifest: the former is the default, while the latter
is selected by informing the Service Manager that your service requires the
service_manager:all_users capability([example](https://cs.chromium.org/chromium/src/services/device/manifest.json)).

In practice, there is one user ID per-BrowserContext, so the question becomes:
Is your Service a global or keyed by BrowserContext? In considering this
question, there is one obvious hint: If you are converting per-Profile classes
(e.g., KeyedServices), then your service is almost certainly going to be
per-user. More generally, if you envision needing to use *any* state related to
the user (e.g., you need to store files in the user's home directory), then your
service should be per-user.

Conversely, your service could be a good fit for being global if it is a utility
that is unconcerned with the identity of the requesting client (e.g., the [data
decoder service](/services/data_decoder), which simply decodes untrusted data in
a separate process.

### Will you embed your service in //content, //chrome, or neither?

At the start (and potentially even long-term), your service will likely not
actually run in its own process but will rather be embedded in the browser
process. This is especially true in the common case where you are converting
existing browser-process code.

You then have a question: Where should it be embedded? The answer to this
question hinges on the nature and location of the code that you are converting:

- //content is the obvious choice if you are converting existing //content code
(e.g., the Device Service). Global services
are embedded by [content::ServiceManagerContext](https://cs.chromium.org/chromium/src/content/browser/service_manager/service_manager_context.cc?type=cs&q=CreateDeviceService),
while per-user services are naturally embedded by [content::BrowserContext](https://cs.chromium.org/chromium/src/content/browser/browser_context.cc?type=cs&q=CreateFileService).

- If your service is converting existing //chrome code, then you will need
to embed your service in //chrome rather than //content. Global services
are embedded by [ChromeContentBrowserClient](https://cs.chromium.org/chromium/src/chrome/browser/chrome_content_browser_client.cc?type=cs&q=CreateMediaService),
while per-user services are embedded by [ProfileImpl](https://cs.chromium.org/chromium/src/chrome/browser/profiles/profile_impl.cc?type=cs&q=CreateIdentityService).

- If you are looking to convert all or part of a component (i.e., a feature in
//components) into a service, the question arises of whether your new service
is worthy of being in //services (i.e., is it a foundational service?). If
not, then it can be placed in an appropriate subdirectory of the component
itself. See this [email
thread](https://groups.google.com/a/chromium.org/forum/#!topic/services-dev/3AJx3gjHbZE) and its [resulting CL](https://codereview.chromium.org/2832633002)
for discussion of this point, and if in doubt, start a similar email thread
discussing your feature.

### If your service is embedded in the browser process, what is its threading model?

If your service is embedded in the browser process, it will run on the IO thread
by default. You can change that by specifying a task runner as part of the
information for constructing your service. In particular, if the code that you
are converting is UI-thread code, then you likely want your service running on
the UI thread. Look at the changes to profile_impl.cc in [this
CL](https://codereview.chromium.org/2753753007) to see an example of setting the
task runner that a service should be run on as part of the factory for creating
the service.

### What is your approach for incremental conversion?

In creating your service, you likely have two goals:

- Making the service available to other services
- Making the service self-contained

Those two goals are not the same, and to some extent are at tension:

- To satisfy the first, you need to build out the API surface of the service to
a sufficient degree for the anticipated use cases.

- To satisfy the second, you need to convert all clients of the code that you
are servicifying to instead use the service, and then fold that code into the
internal implementation of the service.

Whatever your goals, you will need to proceed incrementally if your project is
at all non-trivial (as they basically all are given the nature of the effort).
You should explicitly decide what your approach to incremental bringup and
conversion will be. Here some approaches that have been taken for various
services:

- Build out your service depending directly on existing code,
convert the clients of that code 1-by-1, and fold the existing code into the
service implementation when complete ([Identity Service](https://docs.google.com/document/d/1EPLEJTZewjiShBemNP5Zyk3b_9sgdbrZlXn7j1fubW0/edit)).
- Build out the service with new code and make the existing code
into a client library of the service. In that fashion, all consumers of the
existing code get converted transparently ([Preferences Service](https://docs.google.com/document/d/1JU8QUWxMEXWMqgkvFUumKSxr7Z-nfq0YvreSJTkMVmU/edit#heading=h.19gc5b5u3e3x)).
- Build out the new service piece-by-piece by picking a given
bite-size piece of functionality and entirely servicifying that functionality
([Device Service](https://docs.google.com/document/d/1_1Vt4ShJCiM3fin-leaZx00-FoIPisOr8kwAKsg-Des/edit#heading=h.c3qzrjr1sqn7)).

These all have tradeoffs:

- The first lets you incrementally validate your API and implementation, but
leaves the service depending on external code for a long period of time.
- The second can create a self-contained service more quickly, but leaves
all the existing clients in place as potential cleanup work.
- The third ensures that you're being honest as you go, but delays having
the breadth of the service API up and going.

Which makes sense depends both on the nature of the existing code and on
the priorities for doing the servicification. The first two enable making the
service available for new use cases sooner at the cost of leaving legacy code in
place longer, while the last is most suitable when you want to be very exacting
about doing the servicification cleanly as you go.

## Platform-Specific Issues

### Android
As you servicify code running on Android, you might find that you need to port
interfaces that are served in Java. Here is an [example CL](https://codereview.chromium.org/2643713002) that gives a basic
pattern to follow in doing this.

You also might need to register JNI in your service. That is simple to set
up, as illustrated in [this CL](https://codereview.chromium.org/2690963002).
(Note that that CL is doing more than *just* enabling the Device Service to
register JNI; you should take the register_jni.cc file added there as your
starting point to examine the pattern to follow).

Finally, it is possible that your feature will have coupling to UI process state
(e.g., the Activity) via Android system APIs. To handle this challenging
issue, see the section on [Coupling to UI](#Coupling-to-UI).

### iOS
The high-level [servicification design doc](https://docs.google.com/document/d/15I7sQyQo6zsqXVNAlVd520tdGaS8FCicZHrN0yRu-oU/edit) gives the motivations and
vision for supporting services on iOS (in particular, search for the mentions
of iOS within the doc).

Services are not *yet* supported, but this support is being actively being
worked on; see [this bug](crbug.com/705982) for the current status. If you have
a use case or need for services on iOS, contact blundell@chromium.org.

## Client-Specific Issues

### Services and Blink
Connecting to services directly from Blink is fully supported. [This
CL](https://codereview.chromium.org/2698083007) gives a basic example of
connecting to an arbitrary service by name from Blink (look at the change to
SensorProviderProxy.cpp as a starting point).

Below, we go through strategies for some common challenges encountered when
servicifying features that have Blink as a client.

#### Mocking Interface Impls in JS
It is a common pattern in Blink's layout tests to mock a remote Mojo interface
in JS. [This CL](https://codereview.chromium.org/2643713002) illustrates the
basic pattern for porting such mocking of an interface hosted by
//content/browser to an interface hosted by an arbitrary service (see the
changes to mock-battery-monitor.js).

#### Feature Impls That Depend on Blink Headers
In the course of servicifying a feature that has Blink as a client, you might
encounter cases where the feature implementation has dependencies on Blink
public headers (e.g., defining POD structs that are used both by the client and
by the feature implementation). These dependencies pose a challenge:

- Services should not depend on Blink, as this is a dependency inversion (Blink
is a client of services).
- However, Blink is very careful about accepting dependencies from Chromium.

To meet this challenge, you have two options:

1. Move the code in question from C++ to mojom (e.g., if it is simple structs).
2. Move the code into the service's C++ client library, being very explicit
about its usage by Blink. See [this CL](https://codereview.chromium.org/2415083002) for a basic pattern to follow.

#### Frame-Scoped Connections
You must think carefully about the scoping of the connection being made
from Blink. In particular, some feature requests are necessarily scoped to a
frame in the context of Blink (e.g., geolocation, where permission to access the
interface is origin-scoped). Servicifying these features is then challenging, as
Blink has no frame-scoped connection to arbitrary services (by design, as
arbitrary services have no knowledge of frames or even a notion of what a frame
is).

After a [long
discussion](https://groups.google.com/a/chromium.org/forum/#!topic/services-dev/CSnDUjthAuw),
the policy that we have adopted for this challenge is the following:

CURRENT

- The renderer makes a request through its frame-scoped connection to the
browser.
- The browser obtains the necessary permissions before directly servicing the
request.

AFTER SERVICIFYING THE FEATURE IN QUESTION

- The renderer makes a request through its frame-scoped connection to the
browser.
- The browser obtains the necessary permissions before forwarding the
request on to the underlying service that hosts the feature.

Notably, from the renderer's POV essentially nothing changes here.

In the longer term, this will still be the basic model, only with "the browser"
replaced by "the Navigation Service" or "the web permissions broker".

## Strategies for Challenges to Decoupling from //content

### Coupling to UI

Some feature implementations have hard constraints on coupling to UI on various
platforms. An example is NFC on Android, which requires the Activity of the view
in which the requesting client is hosted in order to access the NFC platform
APIs. This coupling is at odds with the vision of servicification, which is to
make the service physically isolatable. However, when it occurs, we need to
accommodate it.

The high-level decision that we have reached is to scope the coupling to the
feature *and* platform in question (rather than e.g. introducing a
general-purpose FooServiceDelegate), in order to make it completely explicit
what requires the coupling and to avoid the coupling creeping in scope.

The basic strategy to support this coupling while still servicifying the feature
in question is to inject a mechanism of mapping from an opaque "context ID" to
the required context. The embedder (e.g., //content) maintains this map, and the
service makes use of it. The embedder also serves as an intermediary: It
provides a connection that is appropriately context-scoped to clients. When
clients request the feature in question, the embedder forwards the request on
along with the appropriate context ID. The service impl can then map that
context ID back to the needed context on-demand using the mapping functionality
injected into the service impl.

To make this more concrete, see [this CL](https://codereview.chromium.org/2734943003).

### Shutdown of singletons

You might find that your feature includes singletons that are shut down as part
of //content's shutdown process. As part of decoupling the feature
implementation entirely from //content, the shutdown of these singletons must be
either ported into your service or eliminated:

- In general, as Chromium is moving away from graceful shutdown, the first
question to analyze is: Do the singletons actually need to be shut down at
all?
- If you need to preserve shutdown of the singleton, the naive approach is to
move the shutdown of the singleton to the destructor of your service
- However, you should carefully examine when your service is destroyed compared
to when the previous code was executing, and ensure that any differences
introduced do not impact correctness.

See [this thread](https://groups.google.com/a/chromium.org/forum/#!topic/services-dev/Y9FKZf9n1ls) for more discussion of this issue.

### Tests that muck with service internals
It is often the case that browsertests reach directly into what will become part
of the internal service implementation to either inject mock/fake state or to
monitor private state.

This poses a challenge: As part of servicification, *no* code outside the
service impl should depend on the service impl. Thus, these dependencies need to
be removed. The question is how to do so while preserving testing coverage.

To answer this question, there are several different strategies. These
strategies are not mutually-exclusive; they can and should be combined to
preserve the full breadth of coverage.

- Blink client-side behavior can be tested via [layout tests](https://codereview.chromium.org/2731953003)
- To test service impl behavior, create [service tests](https://codereview.chromium.org/2774783003).
- To preserve tests of end-to-end behavior (e.g., that when Blink makes a
request via a Web API in JS, the relevant feature impl receives a connection
request), we are planning on introducing the ability to register mock
implementations with the Service Manager.

To emphasize one very important point: it is in general necessary to leave
*some* test of end-to-end functionality, as otherwise it is too easy for bustage
to slip in via e.g. changes to how services are registered. See [this thread](https://groups.google.com/a/chromium.org/forum/#!topic/services-dev/lJCKAElWz-E)
for further discussion of this point.
6 changes: 6 additions & 0 deletions services/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ differently, e.g. by combining them into a single package.

[Servicification Homepage](https://sites.google.com/a/chromium.org/dev/servicification)

[Servicification Strategies](docs/servicification.md)

## Relationship To Other Top-Level Directories

Services can be thought of as integrators of library code from across the
Expand All @@ -67,3 +69,7 @@ and from other services.

Please start a thread on [services-dev@chromium.org](https://groups.google.com/a/chromium.org/forum/#!forum/services-dev)
if you want to introduce a new service.

If you are servicifying existing Chromium code: Please first read the
[servicification strategies documentation](docs/servicification.md), which
contains information that will hopefully make your task easier.

0 comments on commit 77afe7a

Please sign in to comment.