Skip to content

Commit 9e26856

Browse files
committed
feat(worker): new worker package
1 parent 16a4fa6 commit 9e26856

File tree

15 files changed

+325
-0
lines changed

15 files changed

+325
-0
lines changed

WORKSPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,4 +370,5 @@ local_repository(
370370
"vendored_node_and_yarn",
371371
"web_testing",
372372
"webapp",
373+
"worker",
373374
]]

commitlint.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ module.exports = {
1818
'stylus',
1919
'rollup',
2020
'typescript',
21+
'worker',
2122
]
2223
]
2324
}

examples/BUILD.bazel

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,3 +228,24 @@ bazel_integration_test(
228228
],
229229
workspace_files = "@examples_vendored_node//:all_files",
230230
)
231+
232+
bazel_integration_test(
233+
name = "examples_worker",
234+
# There are no tests in this example
235+
bazel_commands = [
236+
# By default this will build with worker enabled
237+
"build //:do_work",
238+
# Build again without the worker
239+
"build --define=cache_bust=true --strategy=DoWork=standalone //:do_work",
240+
],
241+
bazelrc_imports = {
242+
"//:common.bazelrc": "import %workspace%/../../common.bazelrc",
243+
},
244+
check_npm_packages = NPM_PACKAGES,
245+
npm_packages = {"//packages/worker:npm_package": "@bazel/worker"},
246+
repositories = {
247+
"//:release": "build_bazel_rules_nodejs",
248+
},
249+
tags = ["examples"],
250+
workspace_files = "@examples_worker//:all_files",
251+
)

examples/worker/.bazelrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import %workspace%/../../common.bazelrc

examples/worker/BUILD.bazel

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
load("@build_bazel_rules_nodejs//:defs.bzl", "nodejs_binary")
2+
load(":uses_workers.bzl", "work")
3+
4+
# This is our program that we want to run as a worker
5+
# Imagine that it takes a long time to start, or benefits from caching work
6+
nodejs_binary(
7+
name = "tool",
8+
# For the integration test, allow a second bazel build
9+
# to explicitly be a cache miss, letting us test both
10+
# worker and standalone modes.
11+
configuration_env_vars = ["cache_bust"],
12+
data = ["@npm//@bazel/worker"],
13+
entry_point = ":tool.js",
14+
)
15+
16+
# How a user would call our rule that uses workers.
17+
work(
18+
name = "do_work",
19+
src = "foo.js",
20+
)
21+
22+
# For running this example as a bazel_integration_test
23+
# See //examples:BUILD.bazel
24+
filegroup(
25+
name = "all_files",
26+
srcs = glob(
27+
include = ["**/*"],
28+
exclude = [
29+
"bazel-out/**/*",
30+
"dist/**/*",
31+
"node_modules/**/*",
32+
],
33+
),
34+
visibility = ["//visibility:public"],
35+
)

examples/worker/README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Worker example
2+
3+
This shows how to keep a tool running a persistent worker. This is like a daemon process that Bazel will start and manage as needed to perform actions.
4+
5+
Bazel's protocol for workers is:
6+
7+
- start a pool of processes
8+
- when an action needs to be run, it encodes the request as a protocol buffer and writes it to the worker's stdin
9+
- the `@bazel/worker` package provides a utility to speak this protocol, and dispatches to a function you provide that performs the work of the tool. See /packages/worker/README.md for a description of that utility.
10+
- the tool returns a response written as another protocol buffer to stdout (note this means you cannot log to stdout)
11+
12+
## Files in the example
13+
14+
`foo.js` is some arbitrary input to the rule. You can run `ibazel build :do_work` and then make edits to this JS input to observe how every change triggers the action to run, and it's quite fast because the worker process stays running.
15+
16+
The `tool.js` file shows how to use the `@bazel/worker` package to implement the worker protocol.
17+
Note that the main method first checks whether the tool is being run under the worker mode, or should just do the work once and exit.
18+
19+
`uses_workers.bzl` shows how the tool is wrapped in a Bazel rule. When the action is declared, we mark it with attribute `execution_requirements = {"supports-workers": "1"}` which informs Bazel that it speaks the worker protocol. Bazel will decide whether to actually keep the process running as a persistent worker.
20+
21+
By also providing `mnemonic` attribute to the action, users will be able to control the scheduling if desired.
22+
Note the `--strategy=DoWork=standalone` flag passed to Bazel in the integration test in the /examples directory. This tells Bazel not to use workers. Similarly the user could set some other strategy like `--strategy=DoWork=worker` to explicitly opt-in.
23+
24+
`BUILD.bazel` defines the binary for the tool, then shows how it would be used by calling `work()`. Note that the usage site just calls the rule without knowing whether it uses workers for performance.
25+

examples/worker/WORKSPACE

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Copyright 2019 The Bazel Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
workspace(
16+
name = "examples_worker",
17+
managed_directories = {"@npm": ["node_modules"]},
18+
)
19+
20+
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
21+
22+
http_archive(
23+
name = "build_bazel_rules_nodejs",
24+
sha256 = "6625259f9f77ef90d795d20df1d0385d9b3ce63b6619325f702b6358abb4ab33",
25+
urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/0.35.0/rules_nodejs-0.35.0.tar.gz"],
26+
)
27+
28+
load("@build_bazel_rules_nodejs//:defs.bzl", "yarn_install")
29+
30+
yarn_install(
31+
name = "npm",
32+
package_json = "//:package.json",
33+
yarn_lock = "//:yarn.lock",
34+
)

examples/worker/foo.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// This is an arbitrary file used as an input to the worker action
2+
// Any time this file is changed, the action will need to re-run
3+
export const num = 0;

examples/worker/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"private": true,
3+
"devDependencies": {
4+
"@bazel/worker": "latest"
5+
}
6+
}

examples/worker/tool.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* @fileoverview this program does a trivial job of writing a dummy string to an output
3+
*/
4+
const worker = require('@bazel/worker');
5+
6+
function runOneBuild(args, inputs) {
7+
// IMPORTANT don't log with console.out - stdout is reserved for the worker protocol.
8+
// This is true for any code running in the program, even if it comes from a third-party library.
9+
worker.log('Performing a build with args', args);
10+
if (inputs) {
11+
// The inputs help you manage a cache within the worker process
12+
// They are available only when run as a worker, not in standalone mode
13+
worker.log('We were run as a worker so we also got a manifest of all the inputs', inputs);
14+
}
15+
16+
// Parse our arguments as usual. The worker library handles getting these out of the protocol
17+
// buffer.
18+
const [output] = args;
19+
require('fs').writeFileSync(output, 'Dummy output', {encoding: 'utf-8'});
20+
21+
// Return true if the tool succeeded, false otherwise.
22+
return true;
23+
}
24+
25+
if (require.main === module) {
26+
// One reason to run a program under a worker is that it takes a long time to start
27+
// Imagine that several seconds are spent here
28+
29+
// Bazel will pass a special argument to the program when it's running us as a worker
30+
if (worker.runAsWorker(process.argv)) {
31+
worker.log('Running as a Bazel worker');
32+
33+
worker.runWorkerLoop(runOneBuild);
34+
} else {
35+
// Running standalone so stdout is available as usual
36+
console.log('Running as a standalone process');
37+
38+
// Help our users get on the fast path
39+
console.error(
40+
'Started a new process to perform this action. Your build might be misconfigured, try --strategy=DoWork=worker');
41+
42+
// The first argument to the program is prefixed with '@'
43+
// because Bazel does that for param files. Strip it first.
44+
const paramFile = process.argv[2].replace(/^@/, '');
45+
const args = require('fs').readFileSync(paramFile, 'utf-8').trim().split('\n');
46+
47+
// Bazel is just running the program as a single action, don't act like a worker
48+
if (!runOneBuild(args)) {
49+
process.exitCode = 1;
50+
}
51+
}
52+
}

0 commit comments

Comments
 (0)