blob: 98eae833b10d7a6c74730186f90ea41c317ea29a [file] [view]
# Detailed design
Lightweight distribution server with a legion of dumb bots.
**One line description**: Running all tests concurrently, not be impacted by
network or device flakiness.
## Overview
### Server
The server runs on [AppEngine](https://cloud.google.com/appengine/docs) and is
the only communication point for any client or bot that wants to use Swarming.
Clients send task requests to the server and receive a task ID. It's up to the
client to poll the server to know when a task is done.
The server uses its DB to store the tasks, the bots' state and stdout/stderr
from the tasks. It exposes the web frontend UI, client JSON REST API and bot
JSON API. It uses OAuth2 (or optionally the IP allowlist) to authenticate
clients.
Task requests have a set of dimensions associated to it. It's the server that
matches the request's dimensions to find a bot which has the same set of
dimensions.
The server serializes all its state in the DB. There is **no** continuously
running thread. This makes upgrading the server and rolling back trivial. When
the bot code is affected by the server upgrade, the bots seamlessly upgrade
after their currently running task completes.
### APIs
The client API is implemented via Cloud Endpoints over HTTPS.
- API can be browsed online at
https://chromium-swarm.appspot.com/_ah/api/explorer in the [swarming
API](https://apis-explorer.appspot.com/apis-explorer/?base=https://chromium-swarm.appspot.com/_ah/api#p/swarming/v1/)
section.
- Implementation is in [handlers_endpoints.py](../handlers_endpoints.py).
- Messages are defined in [swarming_rpcs.py](../swarming_rpcs.py).
- Authentication is done via OAuth2. The ACL groups are defined via
[LUCI Auth Service](https://go.chromium.org/luci/auth_service) integration.
**All the APIs are idempotent**: retrying a call in case of error is always
safe. This permits transparent retry in case of failure. The exception is
/tasks/new, which creates a new task, in case of failure, it may leave an orphan
task.
The bot API is an implementation detail and doesn't provide any compatibility
guarantee.
### Server configuration
The server has a few major configuration points;
- Authorization configuration, usually distributed by
[LUCI Auth Service](https://go.chromium.org/luci/auth_service) instance.
- [bootstrap.py](../swarming_bot/config/bootstrap.py) which permits automatic
single-command bot bootstrapping.
- [bot_config.py](../swarming_bot/config/bot_config.py)
which permits Swarming server specific global configuration. It can hook
what the bots do after running a task, their dimensions, etc. See the file
itself for the APIs. The Bot interface is provided by
[bot.py](../swarming/swarming_bot/api/bot.py).
### Tasks
Each task is represented by a `TaskRequest` and a `TaskProperties` described in
[task_request.py](../server/task_request.py). The `TaskRequest` represents the
meta data about a task, who, when, expiration timestamp, etc. The
`TaskProperties` contains the actual details to run a task, commands,
environment variables, execution timeout, etc. This separation makes it possible
to dedupe the task requests when the exact same `.isolated` file is ran multiple
times, so that task-deduplication can be eventually implemented.
A task also has a `TaskResultSummary` to describe its pending state and a tiny
`TaskToRunShard` entity for the actual scheduling. They are respectively
defined in [task_result.py](../server/task_result.py) and
[task_to_run.py](../server/task_to_run.py).
The task ID is the milliseconds since epoch plus low order random bits and the
last byte set to 0. The last byte is used to differentiate between each try.
#### Priority task queues
The server implements a Priority queue based on the creation timestamp of
request. The priority is a 0-255 value with lower is higher priority. The
priority enforces ordering, higher priority (lower value) tasks are run first.
Then tasks *with the same priority* are run in either FIFO or LIFO order,
depending on the server's configuration.
Technically speaking, it's possible to do more elastic priority scheduling, like
old pending requests have their priority slowly increasing over time but the
code to implement this was not committed since there was no immediate need.
#### Assignment
When a bot polls the server for work, the server assigns the first available
matching task available.
Matching is done via the dimensions of the request vs the dimensions of the bot.
The bot must have all the dimensions listed on the request to be given the task.
For example, it could be "os=Windows-Vista-SP2; gpu=15ad:0405".
To make the process efficient, the dimensions are MD5 hashed and only the first
32 bits are saved so integer comparison can be used. This greatly reduce the
size of the hot `TaskToRunShard` entities used for task scheduling and the
amount of memory necessary on the frontend server.
Once a bot is assigned to a task, a `TaskRunResult` is created. If the task is
actually a retry, multiple `TaskRunResult` can be created for a single
`TaskRequest`.
#### Task execution
During execution, the bot streams back the stdout and a heartbeat over HTTPS
requests every 10 seconds. This works around stable long-lived network
connectivity, as a failing HTTPS POST will simply be retried.
#### Task success
Swarming distributes tasks but it doesn't care much about the task itself. A
task is marked as `COMPLETED_SUCCESS` when the exit code is 0.
#### Orphaned task
If a task stops being updated by its bot after 20 minutes (this can be
configured by `bot_ping_tolerance_secs` in task request property), a cron job
will abort the task with BOT_DIED.
If any part of the scheduling, execution or processing of results fails, this is
considered an infrastructure failure.
### Task deduplication
If a task is marked as idempotent, e.g. `-idempotent` is used, the client
certifies that the task do not have side effects. This means that running the
task twice shall return the same results (pending flakiness).
The way it works internally is by calculating the SHA256 of `TaskProperties`
when marked as idempotent. When a `TaskResultSummary` succeeds that was also
idempotent, it sets a property to tell that its values can be reused.
When a new request comes in, it looks for a `TaskResultSummary` that has
`properties_hash` set. If it finds one, the results are reused as-is and served
to the client immediately, without ever scheduling a task.
**Efficient task deduplication requires a deterministic build and no side
effects in the tasks themselves**. On the other hand, successful task
deduplication can result in large infrastructure savings.
See [the user guide about idempotency](User-Guide.md#idempotency) for more
information.
### Caveats of running on AppEngine
- Reliability. The main caveat of running on AppEngine is that it is
[99.95%](https://cloud.google.com/appengine/sla) stable. A simple task
scheduling services that is running on a single host would never have to
care about this. This forces the code and client to be
*extremely defensive*.
- No "process" or "thread" makes simple things difficult; message passing has
to be DB based, cannot be only in-memory. Have to use memcache instead of
in-memory lookup, which causes some overhead.
- No long lived TCP connections makes it hard to have push based design.
- DB operations scale horizontally but are vertically slow.
- It's pretty rare that MySQL or Postgres would save half of the entities in
a DB.put_multi() call. AppEngine does this all the time.
- Transactions have to be avoided as much as possible. This forces the DB
schema to be of a particular style.
- Increased latency due to polling based design.
We accepted these caveats as we found the benefits outweighed, and by far, the
caveats. The main issue has been coding defensively up-front, which represented
a sunk cost in coding time.
### Handling flakiness
Running on AppEngine forced Swarming to make every subsystem to support
flakiness;
- The server tolerates DB failure. In this case, it usually returns HTTP 500.
- The client and the bot handles HTTP 500 by automatically retrying with
exponential backoff. This is fine because the REST APIs are safe to retry.
- No long lived TCP connection is ever created, so a network switch reset or
flakiness network conditions are transparently handled.
- The task scheduler handles flaky bots by retrying the task when the bot
stops sending heartbeats.
## Bot
Each Swarming bot is intended to be extremely dumb and replaceable. These
workers have a very limited understanding of the system and access the server
via a JSON API. Each bot polls the server for a task. If the server hands a
task, the bot runs the associated commands and then pipe the output back to the
server. Once done, it starts polling again.
### Bootstrapping
Only two basic assumptions are:
- The bot must be able to access the server through HTTPS.
- python3.6+ must be installed.
The bot's code is served directly from the server as a self-packaged
`swarming_bot.zip`. The server generates it on the fly and embeds its own URL in
it. The server can also optionally have a custom
[bootstrap.py](../swarming_bot/config/bootstrap.py) to further automate the bot
bootstrapping process.
### Self updating
The bot keeps itself up to date with what the server provides.
- At each poll, the bot hands to the server the SHA256 of the contents of
`swarming_bot.zip`. If it mismatches what the server expects, it is told to
auto-update;
- The bot downloads the new bot code to `swarming_bot.2.zip` or
`swarming_bot.1.zip`, depending on the currently running version and
alternates between both names.
- `swarming_bot.zip` is generated by the server and includes 2 generated
files:
- [bot_config.py](../swarming_bot/config/bot_config.py) is
user-configurable and contains hooks to be run on bot startup, shutdown
and also before and after task execution.
- [config.json](../swarming_bot/config/config.json) contains the URL of
the server itself.
- When a bot runs a task, it locks itself into the server version it started
the task with. This permits to do breaking bot API change safely. This
implies two side-effects:
- A server version must not be deleted on AppEngine until all bot locked
into this version completed their task. It's normally below one hour.
- A server shouldn't be updated in-place, in particular if it modifies the
bot API. Use a new server version name when changing the server or bot
code.
Since the bot version calculation is done solely by the hash, the bot will also
roll back to earlier versions if the server is rolled back. All the bot's code
is inside the zip, this greatly reduces issues like a partial update, update
failure when there's no free space available, etc.
The bot also keeps a `LKGBC` copy (Last Known Good Bot Code):
- Upon startup, if the bot was executed via `swarming_bot.zip`;
- It copies itself to swarming_bot.1.zip and starts itself back, e.g.
execv().
- After successfully running a task, it looks if `swarming_bot.zip` is not
the same version as the currently running version, if so;
- It copies itself (`swarming.1.zip` or `swarming.2.zip`) back to
`swarming_bot.zip`. This permits that at the next VM reboot, the most
recent LKGBC version will be used right away.
The bot code has been tested on Linux, Mac and Windows, Chrome OS' crouton and
Raspbian.
### Bot dimensions
The bot publishes a dictionary of *dimensions*, which is a dict(key,
list(values)), where each value can have multiple values. For example, a Windows
10 bot would have `'os': ['Windows', 'Windows-10-15063']`. This permits broad or
scoped selection of bot type.
For desktop OSes, it's about the OS and hardware properties. For devices, it's
about the device, not about the host driving it.
These "dimensions" are used to associate tasks with the bots. See below.
### Multiple bots on a single host
Multiple bots can run on a host simultaneously, as long as each bot has its own
base directory. So for example, one could be located in `/b/s/bot1` and a second
in `/b/s/bot2`.
In the scenario of multiple bots running on a host, make sure to never call
[Bot.host_reboot()](../swarming_bot/api/bot.py).
### Device Bot
For bots that represent a device (Android, iOS, ChromeOS, Fuchsia), a bot can
"own" all the devices connected to the host (generally via USB) or each bot can
be in a docker container to own a single device.
In the case of devices that are communicated through IP, it's up to
[bot_config.py](../swarming_bot/config/bot_config.py) to decide what is "owned"
by this bot. In some cases this can be determined by hardware (like when the
host has two ethernet cards and devices are connected on the second), vlan
proximity or hard coded host names in
[bot_config.py](../swarming_bot/config/bot_config.py).
In the USB case, a prototype recipe to create
[udev](http://en.wikipedia.org/wiki/Udev) rules to fire up the bot upon
connection is included. The general idea is to reduce sysadmin overhead to its
minimum, configure the host once, then connect devices. No need to touch the
host again afterward. The server tolerates devices going Missing In Action or
the host rebooting, forcibly killing the on-going tasks. The server will retry
these task in this case, since it is an *infrastructure failure*.
The only sysadmin overhead remaining is to look for dead devices once in a while
via the client tools or server monitoring functionality.
## Client
Clients trigger tasks and requests results via a [Cloud Endpoints JSON REST
API](#apis).
It is not possible for the client to access bots directly, no interactivity is
provided _by design_.
See [APIs](#apis) above to write your own client.
### Requesting a task
When a client wishes to run something on Swarming, they can use the REST API or
use the Go client `swarming trigger`. It's a simple HTTPS POST with the
`TaskRequest` and `TaskProperties` serialized as JSON.
The request message is `NewTaskRequest` as defined in
[swarming_rpcs.py](../swarming_rpcs.py).
### Task -> Bot assignment
The bot selection process is inverted. It's the bot that polls for tasks. It
looks at all the products of all the `dimensions` it has and look at the oldest
task with highest priority that has `dimensions` which are also defined on the
bot. If a task uses OR dimension (e.g. '|' in 'os:Ubuntu-18|Ubuntu-16), then
bots having one of the OR'd dimension can poll the task.
### Authentication
- The Web UI is implemented in [lit](https://lit.dev/) and uses the same API
as the client, both are authenticated via OAuth2.
- Bots using the REST APIs can optionally if their IPs are in the allowlist.
### Access control
The access control groups are optionally configured via single
[LUCI Auth Service](https://go.chromium.org/luci/auth_service). This presents a
coherent view on all the associated services.
## Testing Plan
Swarming is tested by python tests in the following ways:
- Pre-commit testing:
- Unit tests
- Smoke test
- Canarying on the chromium infrastructure, to ensure the code works before
deploying to prod.