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+
}

examples/worker/uses_workers.bzl

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"Shows how to define a bazel rule that runs its action as a persistent worker."
2+
3+
def _work(ctx):
4+
output = ctx.actions.declare_file(ctx.label.name + ".out")
5+
6+
# Bazel workers always get their arguments spilled into a params file
7+
args = ctx.actions.args()
8+
9+
# Bazel requires a flagfile for worker mode,
10+
# either prefixed with @ or --flagfile= argument
11+
args.use_param_file("@%s", use_always = True)
12+
args.set_param_file_format("multiline")
13+
14+
args.add(output.path)
15+
16+
ctx.actions.run(
17+
arguments = [args],
18+
executable = ctx.executable.tool,
19+
inputs = [ctx.file.src],
20+
outputs = [output],
21+
# Tell Bazel that this program speaks the worker protocol
22+
execution_requirements = {"supports-workers": "1"},
23+
# The user can explicitly set the execution strategy
24+
mnemonic = "DoWork",
25+
)
26+
27+
return [DefaultInfo(files = depset([output]))]
28+
29+
work = rule(
30+
implementation = _work,
31+
attrs = {
32+
"src": attr.label(allow_single_file = True),
33+
"tool": attr.label(
34+
default = Label("//:tool"),
35+
executable = True,
36+
cfg = "host",
37+
),
38+
},
39+
)

packages/index.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,5 @@ NESTED_PACKAGES = [
2828
NPM_PACKAGES = [
2929
"@bazel/create",
3030
"@bazel/hide-bazel-files",
31+
"@bazel/worker",
3132
] + ["@bazel/%s" % pkg for pkg in NESTED_PACKAGES]

packages/worker/BUILD.bazel

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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+
load("@build_bazel_rules_nodejs//:tools/defaults.bzl", "npm_package")
16+
17+
# We reach inside the @bazel/typescript npm package to grab this one .js file
18+
# This avoids a complex refactoring where we extract that .ts file from tsc_wrapped to a common library
19+
_worker_path = "external/build_bazel_rules_typescript/internal/tsc_wrapped/worker"
20+
21+
# Copy the proto file to a matching third_party/... nested directory
22+
# so the runtime require() statements still work
23+
_worker_proto_dir = "third_party/github.com/bazelbuild/bazel/src/main/protobuf"
24+
25+
genrule(
26+
name = "copy_worker_js",
27+
srcs = ["@build_bazel_rules_typescript//:npm_bazel_typescript_package"],
28+
outs = ["index.js"],
29+
cmd = "cp $(location @build_bazel_rules_typescript//:npm_bazel_typescript_package)/%s.js $@" % _worker_path,
30+
)
31+
32+
genrule(
33+
name = "copy_worker_dts",
34+
srcs = ["@build_bazel_rules_typescript//:npm_bazel_typescript_package"],
35+
outs = ["index.d.ts"],
36+
cmd = "cp $(location @build_bazel_rules_typescript//:npm_bazel_typescript_package)/%s.d.ts $@" % _worker_path,
37+
)
38+
39+
genrule(
40+
name = "copy_worker_proto",
41+
srcs = ["@build_bazel_rules_typescript//%s:worker_protocol.proto" % _worker_proto_dir],
42+
outs = ["%s/worker_protocol.proto" % _worker_proto_dir],
43+
cmd = "cp $< $@",
44+
)
45+
46+
npm_package(
47+
name = "npm_package",
48+
srcs = [
49+
"README.md",
50+
"package.json",
51+
],
52+
replacements = {
53+
# Fix the require() statement that loads the worker_protocol.proto file
54+
# we are re-rooting the sources into the @bazel/worker package so it's no longer
55+
# relative to the build_bazel_rules_typescript workspace.
56+
"build_bazel_rules_typescript": "@bazel/worker",
57+
},
58+
deps = [
59+
":copy_worker_dts",
60+
":copy_worker_js",
61+
":copy_worker_proto",
62+
],
63+
)

packages/worker/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Bazel Worker support
2+
3+
Bazel workers allow actions to be executed by a program that stays running.
4+
5+
Learn more about Bazel workers from Mike Morearty's [medium article](https://medium.com/@mmorearty/how-to-create-a-persistent-worker-for-bazel-7738bba2cabb)
6+
7+
## Typical usage
8+
9+
Read `index.d.ts` for the worker API. Essentially you call `runWorkerLoop` passing it a function to call back when each build request arrives.
10+
11+
See the [worker example] for a full example with comments.
12+
13+
[worker example]: https://github.com/bazelbuild/rules_nodejs/tree/master/examples/worker
14+
15+
## Restrictions on programs that run as a worker
16+
17+
Accept arguments as a params file
18+
19+
stdin and stdout of the process are reserved for the worker protocol with Bazel.
20+
That means anything that does a `console.log` can cause an error.
21+
Bazel prints a snippet of whatever was printed to stdout to help you track it down.
22+
Writing to stderr is fine, for example with `console.error`.
23+
In the future, we might improve this worker library to patch out the nodejs console.log function so that it doesn't interfere with the worker protocol.

packages/worker/package.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "@bazel/worker",
3+
"dependencies": {
4+
"protobufjs": "6.8.8"
5+
},
6+
"description": "Adapt Node programs to run as a Bazel worker",
7+
"license": "Apache-2.0",
8+
"version": "0.0.0-PLACEHOLDER",
9+
"repository": {
10+
"type" : "git",
11+
"url" : "https://github.com/bazelbuild/rules_nodejs.git",
12+
"directory": "packages/worker"
13+
},
14+
"bugs": {
15+
"url": "https://github.com/bazelbuild/rules_nodejs/issues"
16+
},
17+
"keywords": [
18+
"bazel"
19+
]
20+
}

0 commit comments

Comments
 (0)