├── e2e_test ├── analysis_options.yaml ├── pubspec.yaml ├── bin │ ├── sync_worker.dart │ ├── async_worker.dart │ └── async_worker_in_isolate.dart ├── lib │ ├── sync_worker.dart │ ├── async_worker.dart │ └── forwards_to_isolate_async_worker.dart └── test │ └── e2e_test.dart ├── .gitignore ├── example ├── README.md ├── worker.dart └── client.dart ├── AUTHORS ├── analysis_options.yaml ├── pubspec.yaml ├── .github ├── dependabot.yml └── workflows │ ├── publish.yaml │ ├── test-package.yml │ └── no-response.yml ├── lib ├── src │ ├── constants.dart │ ├── message_grouper.dart │ ├── utils.dart │ ├── worker │ │ ├── worker_loop.dart │ │ ├── sync_worker_loop.dart │ │ ├── async_worker_loop.dart │ │ └── worker_connection.dart │ ├── sync_message_grouper.dart │ ├── message_grouper_state.dart │ ├── driver │ │ ├── driver_connection.dart │ │ └── driver.dart │ ├── async_message_grouper.dart │ └── worker_protocol.pb.dart ├── driver.dart ├── bazel_worker.dart └── testing.dart ├── test ├── test_all.dart ├── driver_connection_test.dart ├── message_grouper_test.dart ├── worker_loop_test.dart └── driver_test.dart ├── tool ├── travis.sh └── update_proto.sh ├── LICENSE ├── CONTRIBUTING.md ├── benchmark └── benchmark.dart ├── README.md └── CHANGELOG.md /e2e_test/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | ../analysis_options.yaml -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .buildlog 2 | .DS_Store 3 | .idea 4 | .pub/ 5 | .settings/ 6 | build/ 7 | packages 8 | .packages 9 | pubspec.lock 10 | .dart_tool 11 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | Run `dart example/client.dart`. The client will start up a worker process, send 2 | a single work request, read a file written by the worker, then terminate the 3 | worker. 4 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # Below is a list of people and organizations that have contributed 2 | # to the project. Names should be added to the list like so: 3 | # 4 | # Name/Organization 5 | 6 | Google Inc. 7 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # https://dart.dev/guides/language/analysis-options 2 | include: package:dart_flutter_team_lints/analysis_options.yaml 3 | 4 | analyzer: 5 | language: 6 | strict-casts: true 7 | errors: 8 | # For the generated file 9 | lines_longer_than_80_chars: ignore 10 | -------------------------------------------------------------------------------- /e2e_test/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: e2e_test 2 | publish_to: none 3 | 4 | environment: 5 | sdk: '>=2.19.0 <4.0.0' 6 | 7 | dependencies: 8 | bazel_worker: 9 | path: ../ 10 | 11 | dev_dependencies: 12 | cli_util: ^0.4.0 13 | dart_flutter_team_lints: ^1.0.0 14 | path: ^1.8.0 15 | test: ^1.16.0 16 | -------------------------------------------------------------------------------- /e2e_test/bin/sync_worker.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:e2e_test/sync_worker.dart'; 6 | 7 | void main() { 8 | ExampleSyncWorker().run(); 9 | } 10 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: bazel_worker 2 | version: 1.1.2-wip 3 | description: >- 4 | Protocol and utilities to implement or invoke persistent bazel workers. 5 | repository: https://github.com/dart-lang/bazel_worker 6 | 7 | environment: 8 | sdk: ^3.1.0 9 | 10 | dependencies: 11 | async: ^2.5.0 12 | protobuf: ^3.0.0 13 | 14 | dev_dependencies: 15 | dart_flutter_team_lints: ^3.0.0 16 | test: ^1.16.0 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Dependabot configuration file. 2 | # See https://docs.github.com/en/code-security/dependabot/dependabot-version-updates 3 | version: 2 4 | 5 | updates: 6 | - package-ecosystem: github-actions 7 | directory: / 8 | schedule: 9 | interval: monthly 10 | labels: 11 | - autosubmit 12 | groups: 13 | github-actions: 14 | patterns: 15 | - "*" 16 | -------------------------------------------------------------------------------- /lib/src/constants.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | // ignore_for_file: constant_identifier_names 6 | 7 | const int EXIT_CODE_OK = 0; 8 | const int EXIT_CODE_ERROR = 15; 9 | const int EXIT_CODE_BROKEN_PIPE = 32; 10 | -------------------------------------------------------------------------------- /lib/driver.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | export 'src/constants.dart'; 6 | export 'src/driver/driver.dart'; 7 | export 'src/driver/driver_connection.dart'; 8 | export 'src/message_grouper.dart'; 9 | export 'src/worker_protocol.pb.dart'; 10 | -------------------------------------------------------------------------------- /example/worker.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:bazel_worker/bazel_worker.dart'; 3 | 4 | void main() { 5 | // Blocks until it gets an EOF from stdin. 6 | SyncSimpleWorker().run(); 7 | } 8 | 9 | class SyncSimpleWorker extends SyncWorkerLoop { 10 | @override 11 | WorkResponse performRequest(WorkRequest request) { 12 | File('hello.txt').writeAsStringSync(request.arguments.first); 13 | return WorkResponse(exitCode: EXIT_CODE_OK); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | # A CI configuration to auto-publish pub packages. 2 | 3 | name: Publish 4 | 5 | on: 6 | pull_request: 7 | branches: [ master ] 8 | push: 9 | tags: [ 'v[0-9]+.[0-9]+.[0-9]+' ] 10 | 11 | jobs: 12 | publish: 13 | if: ${{ github.repository_owner == 'dart-lang' }} 14 | uses: dart-lang/ecosystem/.github/workflows/publish.yaml@main 15 | permissions: 16 | id-token: write # Required for authentication using OIDC 17 | pull-requests: write # Required for writing the pull request note 18 | -------------------------------------------------------------------------------- /lib/bazel_worker.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | export 'src/constants.dart'; 6 | export 'src/message_grouper.dart'; 7 | export 'src/worker/async_worker_loop.dart'; 8 | export 'src/worker/sync_worker_loop.dart'; 9 | export 'src/worker/worker_connection.dart'; 10 | export 'src/worker/worker_loop.dart'; 11 | export 'src/worker_protocol.pb.dart'; 12 | -------------------------------------------------------------------------------- /test/test_all.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'driver_connection_test.dart' as driver_connection; 6 | import 'driver_test.dart' as driver; 7 | import 'message_grouper_test.dart' as message_grouper; 8 | import 'worker_loop_test.dart' as worker_loop; 9 | 10 | void main() { 11 | driver.main(); 12 | message_grouper.main(); 13 | worker_loop.main(); 14 | driver_connection.main(); 15 | } 16 | -------------------------------------------------------------------------------- /e2e_test/bin/async_worker.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:isolate'; 7 | 8 | import 'package:e2e_test/async_worker.dart'; 9 | 10 | /// This worker can run in one of two ways: normally, using stdin/stdout, or 11 | /// in an isolate, communicating over a [SendPort]. 12 | Future main(List args, [SendPort? sendPort]) async { 13 | await ExampleAsyncWorker(sendPort).run(); 14 | } 15 | -------------------------------------------------------------------------------- /e2e_test/lib/sync_worker.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:bazel_worker/bazel_worker.dart'; 6 | 7 | /// Example worker that just returns in its response all the arguments passed 8 | /// separated by newlines. 9 | class ExampleSyncWorker extends SyncWorkerLoop { 10 | @override 11 | WorkResponse performRequest(WorkRequest request) { 12 | return WorkResponse(exitCode: 0, output: request.arguments.join('\n')); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tool/travis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 4 | # for details. All rights reserved. Use of this source code is governed by a 5 | # BSD-style license that can be found in the LICENSE file. 6 | 7 | # Fast fail the script on failures. 8 | set -e 9 | 10 | dart pub get 11 | 12 | # Verify that the libraries are error free. 13 | dart analyze --fatal-infos \ 14 | lib/bazel_worker.dart \ 15 | lib/driver.dart \ 16 | lib/testing.dart \ 17 | test/test_all.dart 18 | 19 | # Run the tests. 20 | dart test 21 | 22 | pushd e2e_test 23 | dart pub get 24 | dart analyze --fatal-infos test/e2e_test.dart 25 | dart test 26 | popd 27 | -------------------------------------------------------------------------------- /lib/src/message_grouper.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'async_message_grouper.dart'; 6 | import 'sync_message_grouper.dart'; 7 | 8 | /// Interface for a [MessageGrouper], which groups bytes in delimited proto 9 | /// format into the bytes for each message. 10 | /// 11 | /// This interface should not generally be implemented directly, instead use 12 | /// the [SyncMessageGrouper] or [AsyncMessageGrouper] implementations. 13 | abstract class MessageGrouper { 14 | /// Returns either a [List] or a [Future>]. 15 | dynamic get next; 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/test-package.yml: -------------------------------------------------------------------------------- 1 | name: Dart CI 2 | 3 | on: 4 | # Run on PRs and pushes to the default branch. 5 | push: 6 | branches: [ master ] 7 | pull_request: 8 | branches: [ master ] 9 | schedule: 10 | - cron: "0 0 * * 0" 11 | 12 | env: 13 | PUB_ENVIRONMENT: bot.github 14 | 15 | jobs: 16 | # Run the test script against the latest dev build. 17 | test: 18 | runs-on: ubuntu-latest 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | sdk: [3.1.0, dev] 23 | steps: 24 | - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 25 | - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 26 | with: 27 | sdk: ${{ matrix.sdk }} 28 | - run: "dart format --output=none --set-exit-if-changed ." 29 | - name: Test 30 | run: ./tool/travis.sh 31 | -------------------------------------------------------------------------------- /tool/update_proto.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | if [ -z "$1" ]; then 6 | echo "Expected exactly one argument which is the protoc_plugin version to use" 7 | else 8 | echo "Using protoc_plugin version $1" 9 | dart pub global activate protoc_plugin "$1" 10 | fi 11 | 12 | BAZEL_REPO=.dart_tool/bazel_worker/bazel.git/ 13 | # Bash away old versions if they exist 14 | rm -rf "$BAZEL_REPO" 15 | git clone --depth 1 https://github.com/bazelbuild/bazel.git "$BAZEL_REPO" 16 | 17 | protoc --proto_path="${BAZEL_REPO}/src/main/protobuf" --dart_out="lib/src" worker_protocol.proto 18 | dart format lib/src/worker_protocol.pb.dart 19 | 20 | # We only care about the *.pb.dart file, not the extra files 21 | rm lib/src/worker_protocol.pbenum.dart 22 | rm lib/src/worker_protocol.pbjson.dart 23 | rm lib/src/worker_protocol.pbserver.dart 24 | 25 | rm -rf "$BAZEL_REPO" 26 | -------------------------------------------------------------------------------- /lib/src/utils.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:typed_data'; 6 | 7 | import 'package:protobuf/protobuf.dart'; 8 | 9 | List protoToDelimitedBuffer(GeneratedMessage message) { 10 | var messageBuffer = CodedBufferWriter(); 11 | message.writeToCodedBufferWriter(messageBuffer); 12 | 13 | var delimiterBuffer = CodedBufferWriter(); 14 | delimiterBuffer.writeInt32NoTag(messageBuffer.lengthInBytes); 15 | 16 | var result = 17 | Uint8List(messageBuffer.lengthInBytes + delimiterBuffer.lengthInBytes); 18 | 19 | delimiterBuffer.writeTo(result); 20 | messageBuffer.writeTo(result, delimiterBuffer.lengthInBytes); 21 | 22 | return result; 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/worker/worker_loop.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import '../worker_protocol.pb.dart'; 6 | import 'async_worker_loop.dart'; 7 | import 'sync_worker_loop.dart'; 8 | 9 | /// Interface for a [WorkerLoop]. 10 | /// 11 | /// This interface should not generally be implemented directly, instead use 12 | /// the [SyncWorkerLoop] or [AsyncWorkerLoop] implementations. 13 | abstract class WorkerLoop { 14 | /// Perform a single [WorkRequest], and return either a [WorkResponse] or 15 | /// a [Future]. 16 | dynamic performRequest(WorkRequest request); 17 | 18 | /// Run the worker loop. Should return either a [Future] or `null`. 19 | dynamic run(); 20 | } 21 | -------------------------------------------------------------------------------- /example/client.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:bazel_worker/driver.dart'; 4 | 5 | void main() async { 6 | var scratchSpace = await Directory.systemTemp.createTemp(); 7 | var driver = BazelWorkerDriver( 8 | () => Process.start(Platform.resolvedExecutable, 9 | [Platform.script.resolve('worker.dart').toFilePath()], 10 | workingDirectory: scratchSpace.path), 11 | maxWorkers: 4); 12 | var response = await driver.doWork(WorkRequest(arguments: ['foo'])); 13 | if (response.exitCode != EXIT_CODE_OK) { 14 | print('Worker request failed'); 15 | } else { 16 | print('Worker request succeeded, file content:'); 17 | var outputFile = File.fromUri(scratchSpace.uri.resolve('hello.txt')); 18 | print(await outputFile.readAsString()); 19 | } 20 | await scratchSpace.delete(recursive: true); 21 | await driver.terminateWorkers(); 22 | } 23 | -------------------------------------------------------------------------------- /e2e_test/lib/async_worker.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:isolate'; 7 | 8 | import 'package:bazel_worker/bazel_worker.dart'; 9 | 10 | /// Example worker that just returns in its response all the arguments passed 11 | /// separated by newlines. 12 | class ExampleAsyncWorker extends AsyncWorkerLoop { 13 | /// Set [sendPort] to run in an isolate. 14 | ExampleAsyncWorker([SendPort? sendPort]) 15 | : super(connection: AsyncWorkerConnection(sendPort: sendPort)); 16 | 17 | @override 18 | Future performRequest(WorkRequest request) async { 19 | return WorkResponse( 20 | exitCode: 0, 21 | output: request.arguments.join('\n'), 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/driver_connection_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:isolate'; 6 | 7 | import 'package:bazel_worker/src/constants.dart'; 8 | import 'package:bazel_worker/src/driver/driver_connection.dart'; 9 | import 'package:test/test.dart'; 10 | 11 | void main() { 12 | group('IsolateDriverConnection', () { 13 | test('handles closed port', () async { 14 | var isolatePort = ReceivePort(); 15 | var outsidePort = ReceivePort(); 16 | isolatePort.sendPort.send(outsidePort.sendPort); 17 | var connection = await IsolateDriverConnection.create(isolatePort); 18 | 19 | isolatePort.close(); 20 | 21 | expect((await connection.readResponse()).exitCode, EXIT_CODE_BROKEN_PIPE); 22 | }); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /e2e_test/bin/async_worker_in_isolate.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:isolate'; 7 | 8 | import 'package:e2e_test/forwards_to_isolate_async_worker.dart'; 9 | 10 | /// Wraps the worker provided by `async_worker.dart`, launching it in an 11 | /// isolate. Requests are forwarded to the isolate and responses are returned 12 | /// directly from the isolate. 13 | /// 14 | /// Anyone actually using the facility to wrap a worker in an isolate will want 15 | /// to use this code to do additional work, for example post processing one of 16 | /// the output files. 17 | Future main(List args, [SendPort? message]) async { 18 | var receivePort = ReceivePort(); 19 | await Isolate.spawnUri( 20 | Uri.file('async_worker.dart'), [], receivePort.sendPort); 21 | 22 | var worker = await ForwardsToIsolateAsyncWorker.create(receivePort); 23 | await worker.run(); 24 | } 25 | -------------------------------------------------------------------------------- /e2e_test/lib/forwards_to_isolate_async_worker.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:isolate'; 7 | 8 | import 'package:bazel_worker/bazel_worker.dart'; 9 | import 'package:bazel_worker/driver.dart'; 10 | 11 | /// Example worker that just forwards requests to an isolate. 12 | class ForwardsToIsolateAsyncWorker extends AsyncWorkerLoop { 13 | final IsolateDriverConnection _isolateDriverConnection; 14 | 15 | static Future create( 16 | ReceivePort receivePort) async { 17 | return ForwardsToIsolateAsyncWorker( 18 | await IsolateDriverConnection.create(receivePort)); 19 | } 20 | 21 | ForwardsToIsolateAsyncWorker(this._isolateDriverConnection); 22 | 23 | @override 24 | Future performRequest(WorkRequest request) { 25 | _isolateDriverConnection.writeRequest(request); 26 | return _isolateDriverConnection.readResponse(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/src/sync_message_grouper.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:io'; 6 | 7 | import 'message_grouper.dart'; 8 | import 'message_grouper_state.dart'; 9 | 10 | /// Groups bytes in delimited proto format into the bytes for each message. 11 | class SyncMessageGrouper implements MessageGrouper { 12 | final _state = MessageGrouperState(); 13 | final Stdin _stdin; 14 | 15 | SyncMessageGrouper(this._stdin); 16 | 17 | /// Blocks until the next full message is received, and then returns it. 18 | /// 19 | /// Returns null at end of file. 20 | @override 21 | List? get next { 22 | try { 23 | List? message; 24 | while (message == null) { 25 | var nextByte = _stdin.readByteSync(); 26 | if (nextByte == -1) return null; 27 | message = _state.handleInput(nextByte); 28 | } 29 | return message; 30 | } catch (e) { 31 | // It appears we sometimes get an exception instead of -1 as expected when 32 | // stdin closes, this handles that in the same way (returning a null 33 | // message) 34 | return null; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/no-response.yml: -------------------------------------------------------------------------------- 1 | # A workflow to close issues where the author hasn't responded to a request for 2 | # more information; see https://github.com/actions/stale. 3 | 4 | name: No Response 5 | 6 | # Run as a daily cron. 7 | on: 8 | schedule: 9 | # Every day at 8am 10 | - cron: '0 8 * * *' 11 | 12 | # All permissions not specified are set to 'none'. 13 | permissions: 14 | issues: write 15 | pull-requests: write 16 | 17 | jobs: 18 | no-response: 19 | runs-on: ubuntu-latest 20 | if: ${{ github.repository_owner == 'dart-lang' }} 21 | steps: 22 | - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e 23 | with: 24 | # Don't automatically mark inactive issues+PRs as stale. 25 | days-before-stale: -1 26 | # Close needs-info issues and PRs after 14 days of inactivity. 27 | days-before-close: 14 28 | stale-issue-label: "needs-info" 29 | close-issue-message: > 30 | Without additional information we're not able to resolve this issue. 31 | Feel free to add more info or respond to any questions above and we 32 | can reopen the case. Thanks for your contribution! 33 | stale-pr-label: "needs-info" 34 | close-pr-message: > 35 | Without additional information we're not able to resolve this PR. 36 | Feel free to add more info or respond to any questions above. 37 | Thanks for your contribution! 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016, the Dart project authors. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following 11 | disclaimer in the documentation and/or other materials provided 12 | with the distribution. 13 | * Neither the name of Google LLC nor the names of its 14 | contributors may be used to endorse or promote products derived 15 | from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Want to contribute? Great! First, read this page (including the small print at 2 | the end). 3 | 4 | ### Before you contribute 5 | Before we can use your code, you must sign the 6 | [Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual) 7 | (CLA), which you can do online. The CLA is necessary mainly because you own the 8 | copyright to your changes, even after your contribution becomes part of our 9 | codebase, so we need your permission to use and distribute your code. We also 10 | need to be sure of various other things—for instance that you'll tell us if you 11 | know that your code infringes on other people's patents. You don't have to sign 12 | the CLA until after you've submitted your code for review and a member has 13 | approved it, but you must do it before we can put your code into our codebase. 14 | 15 | Before you start working on a larger contribution, you should get in touch with 16 | us first through the issue tracker with your idea so that we can help out and 17 | possibly guide you. Coordinating up front makes it much easier to avoid 18 | frustration later on. 19 | 20 | ### Code reviews 21 | All submissions, including submissions by project members, require review. 22 | 23 | ### File headers 24 | All files in the project must start with the following header. 25 | 26 | // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file 27 | // for details. All rights reserved. Use of this source code is governed by a 28 | // BSD-style license that can be found in the LICENSE file. 29 | 30 | ### The small print 31 | Contributions made by corporations are covered by a different agreement than the 32 | one above, the 33 | [Software Grant and Corporate Contributor License Agreement](https://developers.google.com/open-source/cla/corporate). 34 | -------------------------------------------------------------------------------- /lib/src/worker/sync_worker_loop.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | import 'dart:async'; 5 | 6 | import '../constants.dart'; 7 | import '../worker_protocol.pb.dart'; 8 | import 'worker_connection.dart'; 9 | import 'worker_loop.dart'; 10 | 11 | /// Persistent Bazel worker loop. 12 | /// 13 | /// Extend this class and implement the `performRequest` method. 14 | abstract class SyncWorkerLoop implements WorkerLoop { 15 | final SyncWorkerConnection connection; 16 | 17 | SyncWorkerLoop({SyncWorkerConnection? connection}) 18 | : connection = connection ?? StdSyncWorkerConnection(); 19 | 20 | /// Perform a single [WorkRequest], and return a [WorkResponse]. 21 | @override 22 | WorkResponse performRequest(WorkRequest request); 23 | 24 | /// Run the worker loop. Blocks until [connection#readRequest] returns `null`. 25 | @override 26 | void run() { 27 | while (true) { 28 | late WorkResponse response; 29 | try { 30 | var request = connection.readRequest(); 31 | if (request == null) break; 32 | var printMessages = StringBuffer(); 33 | response = runZoned(() => performRequest(request), zoneSpecification: 34 | ZoneSpecification(print: (self, parent, zone, message) { 35 | printMessages.writeln(); 36 | printMessages.write(message); 37 | })); 38 | if (printMessages.isNotEmpty) { 39 | response.output = '${response.output}$printMessages'; 40 | } 41 | } catch (e, s) { 42 | response = WorkResponse( 43 | exitCode: EXIT_CODE_ERROR, 44 | output: '$e\n$s', 45 | ); 46 | } 47 | 48 | connection.writeResponse(response); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/src/worker/async_worker_loop.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | 7 | import '../constants.dart'; 8 | import '../worker_protocol.pb.dart'; 9 | import 'worker_connection.dart'; 10 | import 'worker_loop.dart'; 11 | 12 | /// Persistent Bazel worker loop. 13 | /// 14 | /// Extend this class and implement the `performRequest` method. 15 | abstract class AsyncWorkerLoop implements WorkerLoop { 16 | final AsyncWorkerConnection connection; 17 | 18 | AsyncWorkerLoop({AsyncWorkerConnection? connection}) 19 | : connection = connection ?? StdAsyncWorkerConnection(); 20 | 21 | /// Perform a single [WorkRequest], and return a [WorkResponse]. 22 | @override 23 | Future performRequest(WorkRequest request); 24 | 25 | /// Run the worker loop. The returned [Future] doesn't complete until 26 | /// [connection#readRequest] returns `null`. 27 | @override 28 | Future run() async { 29 | while (true) { 30 | late WorkResponse response; 31 | try { 32 | var request = await connection.readRequest(); 33 | if (request == null) break; 34 | var printMessages = StringBuffer(); 35 | response = await runZoned(() => performRequest(request), 36 | zoneSpecification: 37 | ZoneSpecification(print: (self, parent, zone, message) { 38 | printMessages.writeln(); 39 | printMessages.write(message); 40 | })); 41 | if (printMessages.isNotEmpty) { 42 | response.output = '${response.output}$printMessages'; 43 | } 44 | } catch (e, s) { 45 | response = WorkResponse( 46 | exitCode: EXIT_CODE_ERROR, 47 | output: '$e\n$s', 48 | ); 49 | } 50 | 51 | connection.writeResponse(response); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /benchmark/benchmark.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:bazel_worker/bazel_worker.dart'; 5 | import 'package:bazel_worker/src/async_message_grouper.dart'; 6 | 7 | /// Benchmark for `AsyncMessageGrouper`. 8 | Future main() async { 9 | // Create a large work request with 10,000 inputs. 10 | var workRequest = WorkRequest(); 11 | for (var i = 0; i != 10000; ++i) { 12 | var path = 'blaze-bin/some/path/to/a/file/that/is/an/input/$i'; 13 | workRequest 14 | ..arguments.add('--input=$path') 15 | ..inputs.add(Input( 16 | path: '', 17 | digest: List.filled(70, 0x11), 18 | )); 19 | } 20 | 21 | // Serialize it. 22 | var requestBytes = workRequest.writeToBuffer(); 23 | var length = requestBytes.length; 24 | print('Request has $length requestBytes.'); 25 | 26 | // Add the length in front base 128 encoded as in the worker protocol. 27 | requestBytes = 28 | Uint8List.fromList(requestBytes.toList()..insertAll(0, _varInt(length))); 29 | 30 | // Split into 10000 byte chunks. 31 | var lists = []; 32 | for (var i = 0; i < requestBytes.length; i += 10000) { 33 | lists.add(Uint8List.sublistView( 34 | requestBytes, i, min(i + 10000, requestBytes.length))); 35 | } 36 | 37 | // Time `AsyncMessageGrouper` and deserialization. 38 | for (var i = 0; i != 30; ++i) { 39 | var stopwatch = Stopwatch()..start(); 40 | var asyncGrouper = AsyncMessageGrouper(Stream.fromIterable(lists)); 41 | var message = (await asyncGrouper.next)!; 42 | print('Grouped in ${stopwatch.elapsedMilliseconds}ms'); 43 | stopwatch.reset(); 44 | WorkRequest.fromBuffer(message); 45 | print('Deserialized in ${stopwatch.elapsedMilliseconds}ms'); 46 | } 47 | } 48 | 49 | Uint8List _varInt(int value) { 50 | var result = []; 51 | while (value >= 0x80) { 52 | result.add(0x80 | (value & 0x7f)); 53 | value >>= 7; 54 | } 55 | result.add(value); 56 | return Uint8List.fromList(result); 57 | } 58 | -------------------------------------------------------------------------------- /e2e_test/test/e2e_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:io'; 7 | 8 | import 'package:bazel_worker/driver.dart'; 9 | import 'package:cli_util/cli_util.dart'; 10 | import 'package:path/path.dart' as p; 11 | import 'package:test/test.dart'; 12 | 13 | void main() { 14 | var sdkPath = getSdkPath(); 15 | var dart = p.join(sdkPath, 'bin', 'dart'); 16 | runE2eTestForWorker('sync worker', 17 | () => Process.start(dart, [p.join('bin', 'sync_worker.dart')])); 18 | runE2eTestForWorker('async worker', 19 | () => Process.start(dart, [p.join('bin', 'async_worker.dart')])); 20 | runE2eTestForWorker( 21 | 'async worker in isolate', 22 | () => 23 | Process.start(dart, [p.join('bin', 'async_worker_in_isolate.dart')])); 24 | } 25 | 26 | void runE2eTestForWorker(String groupName, SpawnWorker spawnWorker) { 27 | late BazelWorkerDriver driver; 28 | group(groupName, () { 29 | setUp(() { 30 | driver = BazelWorkerDriver(spawnWorker); 31 | }); 32 | 33 | tearDown(() async { 34 | await driver.terminateWorkers(); 35 | }); 36 | 37 | test('single work request', () async { 38 | await _doRequests(driver, count: 1); 39 | }); 40 | 41 | test('lots of requests', () async { 42 | await _doRequests(driver, count: 1000); 43 | }); 44 | }); 45 | } 46 | 47 | /// Runs [count] work requests through [driver], and asserts that they all 48 | /// completed with the correct response. 49 | Future _doRequests(BazelWorkerDriver driver, {int? count}) async { 50 | count ??= 100; 51 | var requests = List.generate(count, (requestNum) { 52 | var request = WorkRequest( 53 | arguments: List.generate(requestNum, (argNum) => '$argNum'), 54 | ); 55 | return request; 56 | }); 57 | var responses = await Future.wait(requests.map(driver.doWork)); 58 | for (var i = 0; i < responses.length; i++) { 59 | var request = requests[i]; 60 | var response = responses[i]; 61 | expect(response.exitCode, EXIT_CODE_OK); 62 | expect(response.output, request.arguments.join('\n')); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!IMPORTANT] 2 | > This repo has moved to https://github.com/dart-lang/tools/tree/main/pkgs/bazel_worker 3 | 4 | Tools for creating a persistent worker loop for [bazel](https://bazel.build/). 5 | 6 | ## Usage 7 | 8 | There are two abstract classes provided by this package, `AsyncWorkerLoop` and 9 | `SyncWorkerLoop`. These each have a `performRequest` method which you must 10 | implement. 11 | 12 | Lets look at a simple example of a `SyncWorkerLoop` implementation: 13 | 14 | ```dart 15 | import 'dart:io'; 16 | import 'package:bazel_worker/bazel_worker.dart'; 17 | 18 | void main() { 19 | // Blocks until it gets an EOF from stdin. 20 | SyncSimpleWorker().run(); 21 | } 22 | 23 | class SyncSimpleWorker extends SyncWorkerLoop { 24 | /// Must synchronously return a [WorkResponse], since this is a 25 | /// [SyncWorkerLoop]. 26 | WorkResponse performRequest(WorkRequest request) { 27 | File('hello.txt').writeAsStringSync('hello world!'); 28 | return WorkResponse()..exitCode = EXIT_CODE_OK; 29 | } 30 | } 31 | ``` 32 | 33 | And now the same thing, implemented as an `AsyncWorkerLoop`: 34 | 35 | ```dart 36 | import 'dart:io'; 37 | import 'package:bazel_worker/bazel_worker.dart'; 38 | 39 | void main() { 40 | // Doesn't block, runs tasks async as they are received on stdin. 41 | AsyncSimpleWorker().run(); 42 | } 43 | 44 | class AsyncSimpleWorker extends AsyncWorkerLoop { 45 | /// Must return a [Future], since this is an 46 | /// [AsyncWorkerLoop]. 47 | Future performRequest(WorkRequest request) async { 48 | await File('hello.txt').writeAsString('hello world!'); 49 | return WorkResponse()..exitCode = EXIT_CODE_OK; 50 | } 51 | } 52 | ``` 53 | 54 | As you can see, these are nearly identical, it mostly comes down to the 55 | constraints on your package and personal preference which one you choose to 56 | implement. 57 | 58 | ## Testing 59 | 60 | A `package:bazel_worker/testing.dart` file is also provided, which can greatly 61 | assist with writing unit tests for your worker. See the 62 | `test/worker_loop_test.dart` test included in this package for an example of how 63 | the helpers can be used. 64 | 65 | ## Features and bugs 66 | 67 | Please file feature requests and bugs at the [issue tracker][tracker]. 68 | 69 | [tracker]: https://github.com/dart-lang/bazel_worker/issues 70 | -------------------------------------------------------------------------------- /test/message_grouper_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:io'; 6 | 7 | import 'package:bazel_worker/bazel_worker.dart'; 8 | import 'package:bazel_worker/testing.dart'; 9 | import 'package:test/test.dart'; 10 | 11 | void main() { 12 | group('AsyncMessageGrouper', () { 13 | runTests(TestStdinAsync.new, AsyncMessageGrouper.new); 14 | }); 15 | 16 | group('SyncMessageGrouper', () { 17 | runTests(TestStdinSync.new, SyncMessageGrouper.new); 18 | }); 19 | } 20 | 21 | void runTests(TestStdin Function() stdinFactory, 22 | MessageGrouper Function(Stdin) messageGrouperFactory) { 23 | late MessageGrouper messageGrouper; 24 | 25 | late TestStdin stdinStream; 26 | 27 | setUp(() { 28 | stdinStream = stdinFactory(); 29 | messageGrouper = messageGrouperFactory(stdinStream); 30 | }); 31 | 32 | /// Check that if the message grouper produces the [expectedOutput] in 33 | /// response to the corresponding [input]. 34 | Future check(List input, List> expectedOutput) async { 35 | stdinStream.addInputBytes(input); 36 | for (var chunk in expectedOutput) { 37 | expect(await messageGrouper.next, equals(chunk)); 38 | } 39 | } 40 | 41 | /// Make a simple message having the given [length] 42 | List makeMessage(int length) { 43 | var result = []; 44 | for (var i = 0; i < length; i++) { 45 | result.add(i & 0xff); 46 | } 47 | return result; 48 | } 49 | 50 | test('Empty message', () async { 51 | await check([0], [[]]); 52 | }); 53 | 54 | test('Short message', () async { 55 | await check([ 56 | 5, 57 | 10, 58 | 20, 59 | 30, 60 | 40, 61 | 50 62 | ], [ 63 | [10, 20, 30, 40, 50] 64 | ]); 65 | }); 66 | 67 | test('Message with 2-byte length', () async { 68 | var len = 0x155; 69 | var msg = makeMessage(len); 70 | var encodedLen = [0xd5, 0x02]; 71 | await check([...encodedLen, ...msg], [msg]); 72 | }); 73 | 74 | test('Message with 3-byte length', () async { 75 | var len = 0x4103; 76 | var msg = makeMessage(len); 77 | var encodedLen = [0x83, 0x82, 0x01]; 78 | await check([...encodedLen, ...msg], [msg]); 79 | }); 80 | 81 | test('Multiple messages', () async { 82 | await check([ 83 | 2, 84 | 10, 85 | 20, 86 | 2, 87 | 30, 88 | 40 89 | ], [ 90 | [10, 20], 91 | [30, 40] 92 | ]); 93 | }); 94 | 95 | test('Empty message at start', () async { 96 | await check([ 97 | 0, 98 | 2, 99 | 10, 100 | 20 101 | ], [ 102 | [], 103 | [10, 20] 104 | ]); 105 | }); 106 | 107 | test('Empty message at end', () async { 108 | await check([ 109 | 2, 110 | 10, 111 | 20, 112 | 0 113 | ], [ 114 | [10, 20], 115 | [] 116 | ]); 117 | }); 118 | 119 | test('Empty message in the middle', () async { 120 | await check([ 121 | 2, 122 | 10, 123 | 20, 124 | 0, 125 | 2, 126 | 30, 127 | 40 128 | ], [ 129 | [10, 20], 130 | [], 131 | [30, 40] 132 | ]); 133 | }); 134 | 135 | test('Handles the case when stdin gives an error instead of EOF', () async { 136 | if (stdinStream is TestStdinSync) { 137 | // Reading will now cause an error as pendingBytes is empty. 138 | (stdinStream as TestStdinSync).pendingBytes.clear(); 139 | expect(messageGrouper.next, isNull); 140 | } else if (stdinStream is TestStdinAsync) { 141 | (stdinStream as TestStdinAsync).controller.addError('Error!'); 142 | expect(await messageGrouper.next, isNull); 143 | } 144 | }); 145 | } 146 | -------------------------------------------------------------------------------- /lib/src/message_grouper_state.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:typed_data'; 6 | 7 | import 'package:protobuf/protobuf.dart'; 8 | 9 | import 'message_grouper.dart'; 10 | 11 | /// State held by the [MessageGrouper] while waiting for additional data to 12 | /// arrive. 13 | class MessageGrouperState { 14 | /// Reads the initial length. 15 | var _lengthReader = _LengthReader(); 16 | 17 | /// Reads messages from a stream of bytes. 18 | _MessageReader? _messageReader; 19 | 20 | /// Handle one byte at a time. 21 | /// 22 | /// Returns a [List] of message bytes if [byte] was the last byte in a 23 | /// message, otherwise returns `null`. 24 | List? handleInput(int byte) { 25 | if (!_lengthReader.done) { 26 | _lengthReader.readByte(byte); 27 | if (_lengthReader.done) { 28 | _messageReader = _MessageReader(_lengthReader.length); 29 | } 30 | } else { 31 | assert(_messageReader != null); 32 | _messageReader!.readByte(byte); 33 | } 34 | 35 | if (_lengthReader.done && _messageReader!.done) { 36 | var message = _messageReader!.message; 37 | reset(); 38 | return message; 39 | } 40 | 41 | return null; 42 | } 43 | 44 | /// Reset the state so that we are ready to receive the next message. 45 | void reset() { 46 | _lengthReader = _LengthReader(); 47 | _messageReader = null; 48 | } 49 | } 50 | 51 | /// Reads a length one byte at a time. 52 | /// 53 | /// The base-128 encoding is in little-endian order, with the high bit set on 54 | /// all bytes but the last. This was chosen since it's the same as the 55 | /// base-128 encoding used by protobufs, so it allows a modest amount of code 56 | /// reuse at the other end of the protocol. 57 | class _LengthReader { 58 | /// Whether or not we are done reading the length. 59 | bool get done => _done; 60 | bool _done = false; 61 | 62 | /// If [_done] is `true`, the decoded value of the length bytes received so 63 | /// far (if any), otherwise unitialized. 64 | late int _length; 65 | 66 | /// The length read in. You are only allowed to read this if [_done] is 67 | /// `true`. 68 | int get length { 69 | assert(_done); 70 | return _length; 71 | } 72 | 73 | final List _buffer = []; 74 | 75 | /// Read a single byte into [_length]. 76 | void readByte(int byte) { 77 | assert(!_done); 78 | _buffer.add(byte); 79 | 80 | // Check for the last byte in the length, and then read it. 81 | if ((byte & 0x80) == 0) { 82 | _done = true; 83 | var reader = CodedBufferReader(_buffer); 84 | _length = reader.readInt32(); 85 | } 86 | } 87 | } 88 | 89 | /// Reads some number of bytes from a stream, one byte at a time. 90 | class _MessageReader { 91 | /// Whether or not we are done reading bytes from the stream. 92 | bool get done => _done; 93 | bool _done; 94 | 95 | /// The total length of the message to be read. 96 | final int _length; 97 | 98 | /// A [Uint8List] which holds the message data. You are only allowed to read 99 | /// this if [_done] is `true`. 100 | Uint8List get message { 101 | assert(_done); 102 | return _message; 103 | } 104 | 105 | final Uint8List _message; 106 | 107 | /// If [_done] is `false`, the number of message bytes that have been received 108 | /// so far. Otherwise zero. 109 | int _numMessageBytesReceived = 0; 110 | 111 | _MessageReader(int length) 112 | : _message = Uint8List(length), 113 | _length = length, 114 | _done = length == 0; 115 | 116 | /// Reads [byte] into [_message]. 117 | void readByte(int byte) { 118 | assert(!done); 119 | 120 | _message[_numMessageBytesReceived++] = byte; 121 | if (_numMessageBytesReceived == _length) _done = true; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /lib/src/worker/worker_connection.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:io'; 7 | import 'dart:isolate'; 8 | import 'dart:typed_data'; 9 | 10 | import '../async_message_grouper.dart'; 11 | import '../sync_message_grouper.dart'; 12 | import '../utils.dart'; 13 | import '../worker_protocol.pb.dart'; 14 | 15 | /// A connection from a worker to a driver (driver could be bazel, a dart 16 | /// program using `BazelWorkerDriver`, or any other process that speaks the 17 | /// protocol). 18 | abstract class WorkerConnection { 19 | /// Reads a [WorkRequest] or returns `null` if there are none left. 20 | /// 21 | /// See [AsyncWorkerConnection] and [SyncWorkerConnection] for more narrow 22 | /// interfaces. 23 | FutureOr readRequest(); 24 | 25 | void writeResponse(WorkResponse response); 26 | } 27 | 28 | abstract class AsyncWorkerConnection implements WorkerConnection { 29 | /// Creates a [StdAsyncWorkerConnection] with the specified [inputStream] 30 | /// and [outputStream], unless [sendPort] is specified, in which case 31 | /// creates a [SendPortAsyncWorkerConnection]. 32 | factory AsyncWorkerConnection( 33 | {Stream>? inputStream, 34 | StreamSink>? outputStream, 35 | SendPort? sendPort}) => 36 | sendPort == null 37 | ? StdAsyncWorkerConnection( 38 | inputStream: inputStream, outputStream: outputStream) 39 | : SendPortAsyncWorkerConnection(sendPort); 40 | 41 | @override 42 | Future readRequest(); 43 | } 44 | 45 | abstract class SyncWorkerConnection implements WorkerConnection { 46 | @override 47 | WorkRequest? readRequest(); 48 | } 49 | 50 | /// Default implementation of [AsyncWorkerConnection] that works with [Stdin] 51 | /// and [Stdout]. 52 | class StdAsyncWorkerConnection implements AsyncWorkerConnection { 53 | final AsyncMessageGrouper _messageGrouper; 54 | final StreamSink> _outputStream; 55 | 56 | StdAsyncWorkerConnection( 57 | {Stream>? inputStream, StreamSink>? outputStream}) 58 | : _messageGrouper = AsyncMessageGrouper(inputStream ?? stdin), 59 | _outputStream = outputStream ?? stdout; 60 | 61 | @override 62 | Future readRequest() async { 63 | var buffer = await _messageGrouper.next; 64 | if (buffer == null) return null; 65 | 66 | return WorkRequest.fromBuffer(buffer); 67 | } 68 | 69 | @override 70 | void writeResponse(WorkResponse response) { 71 | _outputStream.add(protoToDelimitedBuffer(response)); 72 | } 73 | } 74 | 75 | /// Implementation of [AsyncWorkerConnection] for running in an isolate. 76 | class SendPortAsyncWorkerConnection implements AsyncWorkerConnection { 77 | final ReceivePort receivePort; 78 | final StreamIterator receivePortIterator; 79 | final SendPort sendPort; 80 | 81 | factory SendPortAsyncWorkerConnection(SendPort sendPort) { 82 | var receivePort = ReceivePort(); 83 | sendPort.send(receivePort.sendPort); 84 | return SendPortAsyncWorkerConnection._(receivePort, sendPort); 85 | } 86 | 87 | SendPortAsyncWorkerConnection._(this.receivePort, this.sendPort) 88 | : receivePortIterator = StreamIterator(receivePort.cast()); 89 | 90 | @override 91 | Future readRequest() async { 92 | if (!await receivePortIterator.moveNext()) return null; 93 | return WorkRequest.fromBuffer(receivePortIterator.current); 94 | } 95 | 96 | @override 97 | void writeResponse(WorkResponse response) { 98 | sendPort.send(response.writeToBuffer()); 99 | } 100 | } 101 | 102 | /// Default implementation of [SyncWorkerConnection] that works with [Stdin] and 103 | /// [Stdout]. 104 | class StdSyncWorkerConnection implements SyncWorkerConnection { 105 | final SyncMessageGrouper _messageGrouper; 106 | final Stdout _stdoutStream; 107 | 108 | StdSyncWorkerConnection({Stdin? stdinStream, Stdout? stdoutStream}) 109 | : _messageGrouper = SyncMessageGrouper(stdinStream ?? stdin), 110 | _stdoutStream = stdoutStream ?? stdout; 111 | 112 | @override 113 | WorkRequest? readRequest() { 114 | var buffer = _messageGrouper.next; 115 | if (buffer == null) return null; 116 | 117 | return WorkRequest.fromBuffer(buffer); 118 | } 119 | 120 | @override 121 | void writeResponse(WorkResponse response) { 122 | _stdoutStream.add(protoToDelimitedBuffer(response)); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /test/worker_loop_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:io'; 7 | 8 | import 'package:bazel_worker/bazel_worker.dart'; 9 | import 'package:bazel_worker/testing.dart'; 10 | import 'package:test/test.dart'; 11 | 12 | void main() { 13 | group('SyncWorkerLoop', () { 14 | runTests(TestStdinSync.new, TestSyncWorkerConnection.new, 15 | TestSyncWorkerLoop.new); 16 | }); 17 | 18 | group('AsyncWorkerLoop', () { 19 | runTests(TestStdinAsync.new, TestAsyncWorkerConnection.new, 20 | TestAsyncWorkerLoop.new); 21 | }); 22 | 23 | group('SyncWorkerLoopWithPrint', () { 24 | runTests( 25 | TestStdinSync.new, 26 | TestSyncWorkerConnection.new, 27 | (TestSyncWorkerConnection connection) => 28 | TestSyncWorkerLoop(connection, printMessage: 'Goodbye!')); 29 | }); 30 | 31 | group('AsyncWorkerLoopWithPrint', () { 32 | runTests( 33 | TestStdinAsync.new, 34 | TestAsyncWorkerConnection.new, 35 | (TestAsyncWorkerConnection connection) => 36 | TestAsyncWorkerLoop(connection, printMessage: 'Goodbye!')); 37 | }); 38 | } 39 | 40 | void runTests( 41 | TestStdin Function() stdinFactory, 42 | T Function(Stdin, Stdout) workerConnectionFactory, 43 | TestWorkerLoop Function(T) workerLoopFactory) { 44 | late TestStdin stdinStream; 45 | late TestStdoutStream stdoutStream; 46 | late T connection; 47 | late TestWorkerLoop workerLoop; 48 | 49 | setUp(() { 50 | stdinStream = stdinFactory(); 51 | stdoutStream = TestStdoutStream(); 52 | connection = workerConnectionFactory(stdinStream, stdoutStream); 53 | workerLoop = workerLoopFactory(connection); 54 | }); 55 | 56 | test('basic', () async { 57 | var request = WorkRequest(arguments: ['--foo=bar']); 58 | stdinStream.addInputBytes(protoToDelimitedBuffer(request)); 59 | stdinStream.close(); 60 | 61 | var response = WorkResponse(output: 'Hello World'); 62 | workerLoop.enqueueResponse(response); 63 | 64 | // Make sure `print` never gets called in the parent zone. 65 | var printMessages = []; 66 | await runZoned(() => workerLoop.run(), zoneSpecification: 67 | ZoneSpecification(print: (self, parent, zone, message) { 68 | printMessages.add(message); 69 | })); 70 | expect(printMessages, isEmpty, 71 | reason: 'The worker loop should hide all print calls from the parent ' 72 | 'zone.'); 73 | 74 | expect(connection.responses, hasLength(1)); 75 | expect(connection.responses[0], response); 76 | if (workerLoop.printMessage != null) { 77 | expect(response.output, endsWith(workerLoop.printMessage!), 78 | reason: 'Print messages should get appended to the response output.'); 79 | } 80 | 81 | // Check that a serialized version was written to std out. 82 | expect(stdoutStream.writes, hasLength(1)); 83 | expect(stdoutStream.writes[0], protoToDelimitedBuffer(response)); 84 | }); 85 | 86 | test('Exception in the worker.', () async { 87 | var request = WorkRequest(arguments: ['--foo=bar']); 88 | stdinStream.addInputBytes(protoToDelimitedBuffer(request)); 89 | stdinStream.close(); 90 | 91 | // Didn't enqueue a response, so this will throw inside of `performRequest`. 92 | await workerLoop.run(); 93 | 94 | expect(connection.responses, hasLength(1)); 95 | var response = connection.responses[0]; 96 | expect(response.exitCode, EXIT_CODE_ERROR); 97 | 98 | // Check that a serialized version was written to std out. 99 | expect(stdoutStream.writes, hasLength(1)); 100 | expect(stdoutStream.writes[0], protoToDelimitedBuffer(response)); 101 | }); 102 | 103 | test('Stops at EOF', () async { 104 | stdinStream.addInputBytes([-1]); 105 | stdinStream.close(); 106 | await workerLoop.run(); 107 | }); 108 | 109 | test('Stops if stdin gives an error instead of EOF', () async { 110 | if (stdinStream is TestStdinSync) { 111 | // Reading will now cause an error as pendingBytes is empty. 112 | (stdinStream as TestStdinSync).pendingBytes.clear(); 113 | await workerLoop.run(); 114 | } else if (stdinStream is TestStdinAsync) { 115 | var done = Completer(); 116 | // ignore: avoid_dynamic_calls 117 | workerLoop.run().then((_) => done.complete(null)); 118 | (stdinStream as TestStdinAsync).controller.addError('Error!!'); 119 | await done.future; 120 | } 121 | }); 122 | } 123 | -------------------------------------------------------------------------------- /lib/src/driver/driver_connection.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:convert'; 7 | import 'dart:io'; 8 | import 'dart:isolate'; 9 | 10 | import '../async_message_grouper.dart'; 11 | import '../constants.dart'; 12 | import '../message_grouper.dart'; 13 | import '../utils.dart'; 14 | import '../worker_protocol.pb.dart'; 15 | 16 | /// A connection from a `BazelWorkerDriver` to a worker. 17 | /// 18 | /// Unlike `WorkerConnection` there is no synchronous version of this class. 19 | /// This is because drivers talk to multiple workers, so they should never block 20 | /// when waiting for the response of any individual worker. 21 | abstract class DriverConnection { 22 | Future readResponse(); 23 | 24 | void writeRequest(WorkRequest request); 25 | 26 | Future cancel(); 27 | } 28 | 29 | /// Default implementation of [DriverConnection] that works with [Stdin] 30 | /// and [Stdout]. 31 | class StdDriverConnection implements DriverConnection { 32 | final AsyncMessageGrouper _messageGrouper; 33 | final StreamSink> _outputStream; 34 | 35 | Future get done => _messageGrouper.done; 36 | 37 | StdDriverConnection( 38 | {Stream>? inputStream, StreamSink>? outputStream}) 39 | : _messageGrouper = AsyncMessageGrouper(inputStream ?? stdin), 40 | _outputStream = outputStream ?? stdout; 41 | 42 | factory StdDriverConnection.forWorker(Process worker) => StdDriverConnection( 43 | inputStream: worker.stdout, outputStream: worker.stdin); 44 | 45 | /// Note: This will attempts to recover from invalid proto messages by parsing 46 | /// them as strings. This is a common error case for workers (they print a 47 | /// message to stdout on accident). This isn't perfect however as it only 48 | /// happens if the parsing throws, you can still hang indefinitely if the 49 | /// [MessageGrouper] doesn't find what it thinks is the end of a proto 50 | /// message. 51 | @override 52 | Future readResponse() async { 53 | var buffer = await _messageGrouper.next; 54 | if (buffer == null) { 55 | return WorkResponse( 56 | exitCode: EXIT_CODE_BROKEN_PIPE, 57 | output: 'Connection to worker closed', 58 | ); 59 | } 60 | 61 | WorkResponse response; 62 | try { 63 | response = WorkResponse.fromBuffer(buffer); 64 | } catch (_) { 65 | try { 66 | // Try parsing the message as a string and set that as the output. 67 | var output = utf8.decode(buffer); 68 | var response = WorkResponse( 69 | exitCode: EXIT_CODE_ERROR, 70 | output: 'Worker sent an invalid response:\n$output', 71 | ); 72 | return response; 73 | } catch (_) { 74 | // Fall back to original exception and rethrow if we fail to parse as 75 | // a string. 76 | } 77 | rethrow; 78 | } 79 | return response; 80 | } 81 | 82 | @override 83 | void writeRequest(WorkRequest request) { 84 | _outputStream.add(protoToDelimitedBuffer(request)); 85 | } 86 | 87 | @override 88 | Future cancel() async { 89 | await _outputStream.close(); 90 | await _messageGrouper.cancel(); 91 | } 92 | } 93 | 94 | /// [DriverConnection] that works with an isolate via a [SendPort]. 95 | class IsolateDriverConnection implements DriverConnection { 96 | final StreamIterator _receivePortIterator; 97 | final SendPort _sendPort; 98 | 99 | IsolateDriverConnection._(this._receivePortIterator, this._sendPort); 100 | 101 | /// Creates a driver connection for a worker in an isolate. Provide the 102 | /// [receivePort] attached to the [SendPort] that the isolate was created 103 | /// with. 104 | static Future create(ReceivePort receivePort) async { 105 | var receivePortIterator = StreamIterator(receivePort); 106 | await receivePortIterator.moveNext(); 107 | var sendPort = receivePortIterator.current as SendPort; 108 | return IsolateDriverConnection._(receivePortIterator, sendPort); 109 | } 110 | 111 | @override 112 | Future readResponse() async { 113 | if (!await _receivePortIterator.moveNext()) { 114 | return WorkResponse( 115 | exitCode: EXIT_CODE_BROKEN_PIPE, 116 | output: 'Connection to worker closed.', 117 | ); 118 | } 119 | return WorkResponse.fromBuffer(_receivePortIterator.current as List); 120 | } 121 | 122 | @override 123 | void writeRequest(WorkRequest request) { 124 | _sendPort.send(request.writeToBuffer()); 125 | } 126 | 127 | @override 128 | Future cancel() async {} 129 | } 130 | -------------------------------------------------------------------------------- /lib/src/async_message_grouper.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:math'; 7 | import 'dart:typed_data'; 8 | 9 | import 'package:async/async.dart'; 10 | import 'package:protobuf/protobuf.dart'; 11 | 12 | import 'message_grouper.dart'; 13 | 14 | /// Collects stream data into messages by interpreting it as 15 | /// base-128 encoded lengths interleaved with raw data. 16 | class AsyncMessageGrouper implements MessageGrouper { 17 | /// The input stream. 18 | final StreamQueue> _inputQueue; 19 | 20 | /// The current input buffer. 21 | List _inputBuffer = []; 22 | 23 | /// Position in the current input buffer. 24 | int _inputBufferPos = 0; 25 | 26 | /// Completes after [cancel] is called or `inputStream` is closed. 27 | Future get done => _done.future; 28 | final _done = Completer(); 29 | 30 | /// Whether currently reading length or raw data. 31 | bool _readingLength = true; 32 | 33 | /// If reading length, buffer to build up length one byte at a time until 34 | /// done. 35 | List _lengthBuffer = []; 36 | 37 | /// If reading raw data, buffer for the data. 38 | Uint8List _message = Uint8List(0); 39 | 40 | /// If reading raw data, position in the buffer. 41 | int _messagePos = 0; 42 | 43 | AsyncMessageGrouper(Stream> inputStream) 44 | : _inputQueue = StreamQueue(inputStream); 45 | 46 | /// Returns the next full message that is received, or null if none are left. 47 | @override 48 | Future?> get next async { 49 | try { 50 | // Loop while there is data in the input buffer or the input stream. 51 | while ( 52 | _inputBufferPos != _inputBuffer.length || await _inputQueue.hasNext) { 53 | // If the input buffer is empty fill it from the input stream. 54 | if (_inputBufferPos == _inputBuffer.length) { 55 | _inputBuffer = await _inputQueue.next; 56 | _inputBufferPos = 0; 57 | } 58 | 59 | // Loop over the input buffer. Might return without reading the full 60 | // buffer if a message completes. Then, this is tracked in 61 | // `_inputBufferPos`. 62 | while (_inputBufferPos != _inputBuffer.length) { 63 | if (_readingLength) { 64 | // Reading message length byte by byte. 65 | var byte = _inputBuffer[_inputBufferPos++]; 66 | _lengthBuffer.add(byte); 67 | // Check for the last byte in the length, and then read it. 68 | if ((byte & 0x80) == 0) { 69 | var reader = CodedBufferReader(_lengthBuffer); 70 | var length = reader.readInt32(); 71 | _lengthBuffer = []; 72 | 73 | // Special case: don't keep reading an empty message, return it 74 | // and `_readingLength` stays true. 75 | if (length == 0) { 76 | return Uint8List(0); 77 | } 78 | 79 | // Switch to reading raw data. Allocate message buffer and reset 80 | // `_messagePos`. 81 | _readingLength = false; 82 | _message = Uint8List(length); 83 | _messagePos = 0; 84 | } 85 | } else { 86 | // Copy as much as possible from the input buffer. Limit is the 87 | // smaller of the remaining length to fill in the message and the 88 | // remaining length in the buffer. 89 | var lengthToCopy = min(_message.length - _messagePos, 90 | _inputBuffer.length - _inputBufferPos); 91 | _message.setRange( 92 | _messagePos, 93 | _messagePos + lengthToCopy, 94 | _inputBuffer.sublist( 95 | _inputBufferPos, _inputBufferPos + lengthToCopy)); 96 | _messagePos += lengthToCopy; 97 | _inputBufferPos += lengthToCopy; 98 | 99 | // If there is a complete message to return, return it and switch 100 | // back to reading length. 101 | if (_messagePos == _message.length) { 102 | var result = _message; 103 | // Don't keep a reference to the message. 104 | _message = Uint8List(0); 105 | _readingLength = true; 106 | return result; 107 | } 108 | } 109 | } 110 | } 111 | 112 | // If there is nothing left in the queue then cancel the subscription. 113 | unawaited(cancel()); 114 | } catch (e) { 115 | // It appears we sometimes get an exception instead of -1 as expected when 116 | // stdin closes, this handles that in the same way (returning a null 117 | // message) 118 | return null; 119 | } 120 | return null; 121 | } 122 | 123 | /// Stop listening to the stream for further updates. 124 | Future cancel() { 125 | if (!_done.isCompleted) { 126 | _done.complete(null); 127 | return _inputQueue.cancel()!; 128 | } 129 | return done; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.1.2-wip 2 | 3 | * Require Dart SDK `^3.1.0`. 4 | 5 | ## 1.1.1 6 | 7 | * Fix a bug where if spawnWorker threw an error, work requests would hang 8 | forever instead of failing. 9 | 10 | ## 1.1.0 11 | 12 | * Add constructors with named parameters to 13 | the generated worker protocol messages. 14 | * Include comments on the generated worker protocol API. 15 | 16 | ## 1.0.3 17 | 18 | * Require `package:protobuf` >= 3.0.0. 19 | * Require Dart SDK >= 2.19.0 20 | 21 | ## 1.0.2 22 | 23 | * Expand pub documentation to improve pub score. 24 | 25 | ## 1.0.1 26 | 27 | * Require Dart SDK >=2.14.0 28 | * Drop dependency on `package:pedantic`. 29 | 30 | ## 1.0.0 31 | 32 | * Improve `AsyncMessageGrouper` performance. 33 | * Add `benchmark/benchmark.dart` measuring `AsyncMessageGrouper` performance. 34 | 35 | ## 1.0.0-nullsafety.0 36 | 37 | * Migrate to null safety. 38 | * Use `WorkResponse` with `exitCode` set to `EXIT_CODE_BROKEN_PIPE` instead of 39 | `null` responses. 40 | 41 | ## 0.1.25+1-dev 42 | 43 | * Regenerate proto code and fix some new analysis hints. 44 | 45 | ## 0.1.25 46 | 47 | * Add `isBroadcast` implementation to `TestStdin` classes. 48 | 49 | ## 0.1.24 50 | 51 | * Check for closed port when trying to read a response in 52 | `IsolateDriverConnection` and return `null` if there is nothing to be read. 53 | 54 | ## 0.1.23+1 55 | 56 | * Don't rely on `exitCode` to know when a worker terminates, instead wait for 57 | the input stream to close. 58 | * The SDK may also start throwing instead of returning a `null` here, so this 59 | pre-emptively guards against that. 60 | 61 | ## 0.1.23 62 | 63 | * Support protobuf `1.x`. 64 | * Added a tool for updating generated proto files and updated them 65 | using the latest version of the protoc_plugin package. 66 | * This required a lower bound bump of the `protobuf` package to `0.14.4`. 67 | 68 | ## 0.1.22 69 | 70 | * Require protobuf 0.14.0. 71 | 72 | ## 0.1.21+1 73 | 74 | * Don't rely on `exitCode` to know when a worker terminates, instead wait for 75 | the input stream to close. Backport of fix in `0.1.23+1` in a version that 76 | does not require a newer protobuf. 77 | 78 | ## 0.1.21 79 | 80 | * Make `TestStdinAsync` behave like a `Stream` 81 | 82 | ## 0.1.20 83 | 84 | * Close worker `outputStream` on `cancel`. 85 | 86 | ## 0.1.19 87 | 88 | * Work around https://github.com/dart-lang/sdk/issues/35874. 89 | 90 | ## 0.1.18 91 | 92 | * Add a `trackWork` optional named argument to `BazelDriver.doWork`. This allows 93 | the caller to know when a work request is actually sent to a worker. 94 | 95 | ## 0.1.17 96 | 97 | * Allow protobuf 0.13.0. 98 | 99 | ## 0.1.16 100 | 101 | * Update the worker_protocol.pb.dart file with the latest proto generator. 102 | * Require protobuf 0.11.0. 103 | 104 | ## 0.1.15 105 | 106 | * Update the worker_protocol.pb.dart file with the latest proto generator. 107 | * Require protobuf 0.10.4. 108 | 109 | ## 0.1.14 110 | 111 | * Allow workers to support running in isolates. To support running in isolates, 112 | workers must modify their `main` method to accept a `SendPort` then use it 113 | when creating the `AsyncWorkerConnection`. See `async_worker` in `e2e_test`. 114 | 115 | ## 0.1.13 116 | 117 | * Support protobuf 0.10.0. 118 | 119 | ## 0.1.12 120 | 121 | * Set max SDK version to `<3.0.0`. 122 | 123 | ## 0.1.11 124 | 125 | * Added support for protobuf 0.9.0. 126 | 127 | ## 0.1.10 128 | 129 | * Update the SDK dependency to 2.0.0-dev.17.0. 130 | * Update to protobuf version 0.8.0 131 | * Remove usages of deprecated upper-case constants from the SDK. 132 | 133 | ## 0.1.9 134 | 135 | * Update the worker_protocol.pb.dart file with the latest proto generator. 136 | 137 | ## 0.1.8 138 | 139 | * Add `Future cancel()` method to `DriverConnection`, which in the case of a 140 | `StdDriverConnection` closes the input stream. 141 | * The `terminateWorkers` method on `BazelWorkerDriver` now calls `cancel` on 142 | all worker connections to ensure the vm can exit correctly. 143 | 144 | ## 0.1.7 145 | 146 | * Update the `BazelWorkerDriver` class to handle worker crashes, and retry work 147 | requests. The number of retries is configurable with the new `int maxRetries` 148 | optional arg to the `BazelWorkerDriver` constructor. 149 | 150 | ## 0.1.6 151 | 152 | * Update the worker_protocol.pb.dart file with the latest proto generator. 153 | * Add support for package:async 2.x and package:protobuf 6.x. 154 | 155 | ## 0.1.5 156 | 157 | * Change TestStdinAsync.controller to StreamController> (instead of 158 | using dynamic as the type argument). 159 | 160 | ## 0.1.4 161 | 162 | * Added `BazelWorkerDriver` class, which can be used to implement the bazel side 163 | of the protocol. This allows you to speak to any process which knows the bazel 164 | protocol from your own process. 165 | * Changed `WorkerConnection#readRequest` to return a `FutureOr` 166 | instead of dynamic. 167 | 168 | ## 0.1.3 169 | 170 | * Add automatic intercepting of print calls and append them to 171 | `response.output`. This makes more libraries work out of the box, as printing 172 | would previously cause an error due to communication over stdin/stdout. 173 | * Note that using stdin/stdout directly will still cause an error, but that is 174 | less common. 175 | 176 | ## 0.1.2 177 | 178 | * Add better handling for the case where stdin gives an error instead of an EOF. 179 | 180 | ## 0.1.1 181 | 182 | * Export `AsyncMessageGrouper` and `SyncMessageGrouper` as part of the testing 183 | library. These can assist when writing e2e tests and communicating with a 184 | worker process. 185 | 186 | ## 0.1.0 187 | 188 | * Initial version. 189 | -------------------------------------------------------------------------------- /lib/testing.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:collection'; 7 | import 'dart:io'; 8 | import 'dart:typed_data'; 9 | 10 | import 'bazel_worker.dart'; 11 | 12 | export 'src/async_message_grouper.dart'; 13 | export 'src/sync_message_grouper.dart'; 14 | export 'src/utils.dart' show protoToDelimitedBuffer; 15 | 16 | /// Interface for a mock [Stdin] object that allows you to add bytes manually. 17 | abstract class TestStdin implements Stdin { 18 | void addInputBytes(List bytes); 19 | 20 | void close(); 21 | } 22 | 23 | /// A [Stdin] mock object which only implements `readByteSync`. 24 | class TestStdinSync implements TestStdin { 25 | /// Pending bytes to be delivered synchronously. 26 | final Queue pendingBytes = Queue(); 27 | 28 | /// Adds all the [bytes] to this stream. 29 | @override 30 | void addInputBytes(List bytes) { 31 | pendingBytes.addAll(bytes); 32 | } 33 | 34 | /// Add a -1 to signal EOF. 35 | @override 36 | void close() { 37 | pendingBytes.add(-1); 38 | } 39 | 40 | @override 41 | int readByteSync() { 42 | return pendingBytes.removeFirst(); 43 | } 44 | 45 | @override 46 | bool get isBroadcast => false; 47 | 48 | @override 49 | void noSuchMethod(Invocation invocation) { 50 | throw StateError('Unexpected invocation ${invocation.memberName}.'); 51 | } 52 | } 53 | 54 | /// A mock [Stdin] object which only implements `listen`. 55 | /// 56 | /// Note: You must call [close] in order for the loop to exit properly. 57 | class TestStdinAsync implements TestStdin { 58 | /// Controls the stream for async delivery of bytes. 59 | final StreamController _controller = StreamController(); 60 | StreamController get controller => _controller; 61 | 62 | /// Adds all the [bytes] to this stream. 63 | @override 64 | void addInputBytes(List bytes) { 65 | _controller.add(Uint8List.fromList(bytes)); 66 | } 67 | 68 | /// Closes this stream. This is necessary for the [AsyncWorkerLoop] to exit. 69 | @override 70 | void close() { 71 | _controller.close(); 72 | } 73 | 74 | @override 75 | StreamSubscription listen(void Function(Uint8List bytes)? onData, 76 | {Function? onError, void Function()? onDone, bool? cancelOnError}) { 77 | return _controller.stream.listen(onData, 78 | onError: onError, onDone: onDone, cancelOnError: cancelOnError); 79 | } 80 | 81 | @override 82 | bool get isBroadcast => false; 83 | 84 | @override 85 | void noSuchMethod(Invocation invocation) { 86 | throw StateError('Unexpected invocation ${invocation.memberName}.'); 87 | } 88 | } 89 | 90 | /// A [Stdout] mock object. 91 | class TestStdoutStream implements Stdout { 92 | final List> writes = >[]; 93 | 94 | @override 95 | void add(List bytes) { 96 | writes.add(bytes); 97 | } 98 | 99 | @override 100 | void noSuchMethod(Invocation invocation) { 101 | throw StateError('Unexpected invocation ${invocation.memberName}.'); 102 | } 103 | } 104 | 105 | /// Interface for a [TestWorkerConnection] which records its responses 106 | abstract class TestWorkerConnection implements WorkerConnection { 107 | List get responses; 108 | } 109 | 110 | /// Interface for a [TestWorkerLoop] which allows you to enqueue responses. 111 | abstract class TestWorkerLoop implements WorkerLoop { 112 | void enqueueResponse(WorkResponse response); 113 | 114 | /// If set, this message will be printed during the call to `performRequest`. 115 | String? get printMessage; 116 | } 117 | 118 | /// A [StdSyncWorkerConnection] which records its responses. 119 | class TestSyncWorkerConnection extends StdSyncWorkerConnection 120 | implements TestWorkerConnection { 121 | @override 122 | final List responses = []; 123 | 124 | TestSyncWorkerConnection(Stdin stdinStream, Stdout stdoutStream) 125 | : super(stdinStream: stdinStream, stdoutStream: stdoutStream); 126 | 127 | @override 128 | void writeResponse(WorkResponse response) { 129 | super.writeResponse(response); 130 | responses.add(response); 131 | } 132 | } 133 | 134 | /// A [SyncWorkerLoop] for testing. 135 | class TestSyncWorkerLoop extends SyncWorkerLoop implements TestWorkerLoop { 136 | final List requests = []; 137 | final Queue _responses = Queue(); 138 | 139 | @override 140 | final String? printMessage; 141 | 142 | TestSyncWorkerLoop(SyncWorkerConnection connection, {this.printMessage}) 143 | : super(connection: connection); 144 | 145 | @override 146 | WorkResponse performRequest(WorkRequest request) { 147 | requests.add(request); 148 | if (printMessage != null) print(printMessage); 149 | return _responses.removeFirst(); 150 | } 151 | 152 | /// Adds [response] to the queue. These will be returned from 153 | /// [performRequest] in the order they are added, otherwise it will throw 154 | /// if the queue is empty. 155 | @override 156 | void enqueueResponse(WorkResponse response) { 157 | _responses.addLast(response); 158 | } 159 | } 160 | 161 | /// A [StdAsyncWorkerConnection] which records its responses. 162 | class TestAsyncWorkerConnection extends StdAsyncWorkerConnection 163 | implements TestWorkerConnection { 164 | @override 165 | final List responses = []; 166 | 167 | TestAsyncWorkerConnection( 168 | Stream> inputStream, StreamSink> outputStream) 169 | : super(inputStream: inputStream, outputStream: outputStream); 170 | 171 | @override 172 | void writeResponse(WorkResponse response) { 173 | super.writeResponse(response); 174 | responses.add(response); 175 | } 176 | } 177 | 178 | /// A [AsyncWorkerLoop] for testing. 179 | class TestAsyncWorkerLoop extends AsyncWorkerLoop implements TestWorkerLoop { 180 | final List requests = []; 181 | final Queue _responses = Queue(); 182 | 183 | @override 184 | final String? printMessage; 185 | 186 | TestAsyncWorkerLoop(AsyncWorkerConnection connection, {this.printMessage}) 187 | : super(connection: connection); 188 | 189 | @override 190 | Future performRequest(WorkRequest request) async { 191 | requests.add(request); 192 | if (printMessage != null) print(printMessage); 193 | return _responses.removeFirst(); 194 | } 195 | 196 | /// Adds [response] to the queue. These will be returned from 197 | /// [performRequest] in the order they are added, otherwise it will throw 198 | /// if the queue is empty. 199 | @override 200 | void enqueueResponse(WorkResponse response) { 201 | _responses.addLast(response); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /lib/src/driver/driver.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:collection'; 7 | import 'dart:io'; 8 | 9 | import '../constants.dart'; 10 | import '../worker_protocol.pb.dart'; 11 | import 'driver_connection.dart'; 12 | 13 | typedef SpawnWorker = Future Function(); 14 | 15 | /// A driver for talking to a bazel worker. 16 | /// 17 | /// This allows you to use any binary that supports the bazel worker protocol in 18 | /// the same way that bazel would, but from another dart process instead. 19 | class BazelWorkerDriver { 20 | /// Idle worker processes. 21 | final _idleWorkers = []; 22 | 23 | /// The maximum number of idle workers at any given time. 24 | final int _maxIdleWorkers; 25 | 26 | /// The maximum number of times to retry a [_WorkAttempt] if there is an error. 27 | final int _maxRetries; 28 | 29 | /// The maximum number of concurrent workers to run at any given time. 30 | final int _maxWorkers; 31 | 32 | /// The number of currently active workers. 33 | int get _numWorkers => _readyWorkers.length + _spawningWorkers.length; 34 | 35 | /// All workers that are fully spawned and ready to handle work. 36 | final _readyWorkers = []; 37 | 38 | /// All workers that are in the process of being spawned. 39 | final _spawningWorkers = >[]; 40 | 41 | /// Work requests that haven't been started yet. 42 | final _workQueue = Queue<_WorkAttempt>(); 43 | 44 | /// Factory method that spawns a worker process. 45 | final SpawnWorker _spawnWorker; 46 | 47 | BazelWorkerDriver(this._spawnWorker, 48 | {int? maxIdleWorkers, int? maxWorkers, int? maxRetries}) 49 | : _maxIdleWorkers = maxIdleWorkers ?? 4, 50 | _maxWorkers = maxWorkers ?? 4, 51 | _maxRetries = maxRetries ?? 4; 52 | 53 | /// Waits for an available worker, and then sends [WorkRequest] to it. 54 | /// 55 | /// If [trackWork] is provided it will be invoked with a [Future] once the 56 | /// [request] has been actually sent to the worker. This allows the caller 57 | /// to determine when actual work is being done versus just waiting for an 58 | /// available worker. 59 | Future doWork(WorkRequest request, 60 | {void Function(Future)? trackWork}) { 61 | var attempt = _WorkAttempt(request, trackWork: trackWork); 62 | _workQueue.add(attempt); 63 | _runWorkQueue(); 64 | return attempt.response; 65 | } 66 | 67 | /// Calls `kill` on all worker processes. 68 | Future terminateWorkers() async { 69 | for (var worker in _readyWorkers.toList()) { 70 | _killWorker(worker); 71 | } 72 | await Future.wait(_spawningWorkers.map((worker) async { 73 | _killWorker(await worker); 74 | })); 75 | } 76 | 77 | /// Runs as many items in [_workQueue] as possible given the number of 78 | /// available workers. 79 | /// 80 | /// Will spawn additional workers until [_maxWorkers] has been reached. 81 | /// 82 | /// This method synchronously drains the [_workQueue] and [_idleWorkers], but 83 | /// some tasks may not actually start right away if they need to wait for a 84 | /// worker to spin up. 85 | void _runWorkQueue() { 86 | // Bail out conditions, we will continue to call ourselves indefinitely 87 | // until one of these is met. 88 | if (_workQueue.isEmpty) return; 89 | if (_numWorkers == _maxWorkers && _idleWorkers.isEmpty) return; 90 | if (_numWorkers > _maxWorkers) { 91 | throw StateError('Internal error, created to many workers. Please ' 92 | 'file a bug at https://github.com/dart-lang/bazel_worker/issues/new'); 93 | } 94 | 95 | // At this point we definitely want to run a task, we just need to decide 96 | // whether or not we need to start up a new worker. 97 | var attempt = _workQueue.removeFirst(); 98 | if (_idleWorkers.isNotEmpty) { 99 | _runWorker(_idleWorkers.removeLast(), attempt); 100 | } else { 101 | // No need to block here, we want to continue to synchronously drain the 102 | // work queue. 103 | var futureWorker = _spawnWorker(); 104 | _spawningWorkers.add(futureWorker); 105 | futureWorker.then((worker) { 106 | _spawningWorkers.remove(futureWorker); 107 | _readyWorkers.add(worker); 108 | var connection = StdDriverConnection.forWorker(worker); 109 | _workerConnections[worker] = connection; 110 | _runWorker(worker, attempt); 111 | 112 | // When the worker exits we should retry running the work queue in case 113 | // there is more work to be done. This is primarily just a defensive 114 | // thing but is cheap to do. 115 | // 116 | // We don't use `exitCode` because it is null for detached processes ( 117 | // which is common for workers). 118 | connection.done.then((_) { 119 | _idleWorkers.remove(worker); 120 | _readyWorkers.remove(worker); 121 | _runWorkQueue(); 122 | }); 123 | }).onError((e, s) { 124 | _spawningWorkers.remove(futureWorker); 125 | if (attempt.responseCompleter.isCompleted) return; 126 | attempt.responseCompleter.completeError(e, s); 127 | }); 128 | } 129 | // Recursively calls itself until one of the bail out conditions are met. 130 | _runWorkQueue(); 131 | } 132 | 133 | /// Sends [attempt] to [worker]. 134 | /// 135 | /// Once the worker responds then it will be added back to the pool of idle 136 | /// workers. 137 | void _runWorker(Process worker, _WorkAttempt attempt) { 138 | var rescheduled = false; 139 | 140 | runZonedGuarded(() async { 141 | var connection = _workerConnections[worker]!; 142 | 143 | connection.writeRequest(attempt.request); 144 | var responseFuture = connection.readResponse(); 145 | if (attempt.trackWork != null) { 146 | attempt.trackWork!(responseFuture); 147 | } 148 | var response = await responseFuture; 149 | 150 | // It is possible for us to complete with an error response due to an 151 | // unhandled async error before we get here. 152 | if (!attempt.responseCompleter.isCompleted) { 153 | if (response.exitCode == EXIT_CODE_BROKEN_PIPE) { 154 | rescheduled = _tryReschedule(attempt); 155 | if (rescheduled) return; 156 | stderr.writeln('Failed to run request ${attempt.request}'); 157 | response = WorkResponse( 158 | exitCode: EXIT_CODE_ERROR, 159 | output: 160 | 'Invalid response from worker, this probably means it wrote ' 161 | 'invalid output or died.', 162 | ); 163 | } 164 | attempt.responseCompleter.complete(response); 165 | _cleanUp(worker); 166 | } 167 | }, (e, s) { 168 | // Note that we don't need to do additional cleanup here on failures. If 169 | // the worker dies that is already handled in a generic fashion, we just 170 | // need to make sure we complete with a valid response. 171 | if (!attempt.responseCompleter.isCompleted) { 172 | rescheduled = _tryReschedule(attempt); 173 | if (rescheduled) return; 174 | var response = WorkResponse( 175 | exitCode: EXIT_CODE_ERROR, 176 | output: 'Error running worker:\n$e\n$s', 177 | ); 178 | attempt.responseCompleter.complete(response); 179 | _cleanUp(worker); 180 | } 181 | }); 182 | } 183 | 184 | /// Performs post-work cleanup for [worker]. 185 | void _cleanUp(Process worker) { 186 | // If the worker crashes, it won't be in `_readyWorkers` any more, and 187 | // we don't want to add it to _idleWorkers. 188 | if (_readyWorkers.contains(worker)) { 189 | _idleWorkers.add(worker); 190 | } 191 | 192 | // Do additional work if available. 193 | _runWorkQueue(); 194 | 195 | // If the worker wasn't immediately used we might have to many idle 196 | // workers now, kill one if necessary. 197 | if (_idleWorkers.length > _maxIdleWorkers) { 198 | // Note that whenever we spawn a worker we listen for its exit code 199 | // and clean it up so we don't need to do that here. 200 | var worker = _idleWorkers.removeLast(); 201 | _killWorker(worker); 202 | } 203 | } 204 | 205 | /// Attempts to reschedule a failed [attempt]. 206 | /// 207 | /// Returns whether or not the job was successfully rescheduled. 208 | bool _tryReschedule(_WorkAttempt attempt) { 209 | if (attempt.timesRetried >= _maxRetries) return false; 210 | stderr.writeln('Rescheduling failed request...'); 211 | attempt.timesRetried++; 212 | _workQueue.add(attempt); 213 | _runWorkQueue(); 214 | return true; 215 | } 216 | 217 | void _killWorker(Process worker) { 218 | _workerConnections[worker]!.cancel(); 219 | _readyWorkers.remove(worker); 220 | _idleWorkers.remove(worker); 221 | worker.kill(); 222 | } 223 | } 224 | 225 | /// Encapsulates an attempt to fulfill a [WorkRequest], a completer for the 226 | /// [WorkResponse], and the number of times it has been retried. 227 | class _WorkAttempt { 228 | final WorkRequest request; 229 | final responseCompleter = Completer(); 230 | final void Function(Future)? trackWork; 231 | 232 | Future get response => responseCompleter.future; 233 | 234 | int timesRetried = 0; 235 | 236 | _WorkAttempt(this.request, {this.trackWork}); 237 | } 238 | 239 | final _workerConnections = Expando('connection'); 240 | -------------------------------------------------------------------------------- /test/driver_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:collection'; 7 | import 'dart:io'; 8 | 9 | import 'package:bazel_worker/bazel_worker.dart'; 10 | import 'package:bazel_worker/driver.dart'; 11 | import 'package:test/test.dart'; 12 | 13 | void main() { 14 | BazelWorkerDriver? driver; 15 | final disconnectedResponse = WorkResponse( 16 | exitCode: EXIT_CODE_BROKEN_PIPE, 17 | output: 'Connection closed', 18 | )..freeze(); 19 | 20 | group('basic driver', () { 21 | test('can run a single request', () async { 22 | await _doRequests(count: 1); 23 | await _doRequests(count: 1); 24 | }); 25 | 26 | test('can run multiple batches of requests through multiple workers', 27 | () async { 28 | var maxWorkers = 4; 29 | var maxIdleWorkers = 2; 30 | driver = BazelWorkerDriver(MockWorker.spawn, 31 | maxWorkers: maxWorkers, maxIdleWorkers: maxIdleWorkers); 32 | for (var i = 0; i < 10; i++) { 33 | await _doRequests(driver: driver); 34 | expect(MockWorker.liveWorkers.length, maxIdleWorkers); 35 | // No workers should be killed while there is ongoing work, but they 36 | // should be cleaned up once there isn't any more work to do. 37 | expect(MockWorker.deadWorkers.length, 38 | (maxWorkers - maxIdleWorkers) * (i + 1)); 39 | } 40 | }); 41 | 42 | test('can run multiple requests through one worker', () async { 43 | var maxWorkers = 1; 44 | var maxIdleWorkers = 1; 45 | driver = BazelWorkerDriver(MockWorker.spawn, 46 | maxWorkers: maxWorkers, maxIdleWorkers: maxIdleWorkers); 47 | for (var i = 0; i < 10; i++) { 48 | await _doRequests(driver: driver); 49 | expect(MockWorker.liveWorkers.length, 1); 50 | expect(MockWorker.deadWorkers.length, 0); 51 | } 52 | }); 53 | 54 | test('can run one request through multiple workers', () async { 55 | driver = 56 | BazelWorkerDriver(MockWorker.spawn, maxWorkers: 4, maxIdleWorkers: 4); 57 | for (var i = 0; i < 10; i++) { 58 | await _doRequests(driver: driver, count: 1); 59 | expect(MockWorker.liveWorkers.length, 1); 60 | expect(MockWorker.deadWorkers.length, 0); 61 | } 62 | }); 63 | 64 | test('can run with maxIdleWorkers == 0', () async { 65 | var maxWorkers = 4; 66 | driver = BazelWorkerDriver(MockWorker.spawn, 67 | maxWorkers: maxWorkers, maxIdleWorkers: 0); 68 | for (var i = 0; i < 10; i++) { 69 | await _doRequests(driver: driver); 70 | expect(MockWorker.liveWorkers.length, 0); 71 | expect(MockWorker.deadWorkers.length, maxWorkers * (i + 1)); 72 | } 73 | }); 74 | 75 | test('trackWork gets invoked when a worker is actually ready', () async { 76 | var maxWorkers = 2; 77 | driver = BazelWorkerDriver(MockWorker.spawn, maxWorkers: maxWorkers); 78 | var tracking = []; 79 | await _doRequests( 80 | driver: driver, 81 | count: 10, 82 | trackWork: (Future response) { 83 | // We should never be tracking more than `maxWorkers` jobs at a time. 84 | expect(tracking.length, lessThan(maxWorkers)); 85 | tracking.add(response); 86 | response.then((_) => tracking.remove(response)); 87 | }); 88 | }); 89 | 90 | group('failing workers', () { 91 | /// A driver which spawns [numBadWorkers] failing workers and then good 92 | /// ones after that, and which will retry [maxRetries] times. 93 | void createDriver({int maxRetries = 2, int numBadWorkers = 2}) { 94 | var numSpawned = 0; 95 | driver = BazelWorkerDriver( 96 | () async => MockWorker(workerLoopFactory: (MockWorker worker) { 97 | var connection = StdAsyncWorkerConnection( 98 | inputStream: worker._stdinController.stream, 99 | outputStream: worker._stdoutController.sink); 100 | if (numSpawned < numBadWorkers) { 101 | numSpawned++; 102 | return ThrowingMockWorkerLoop( 103 | worker, MockWorker.responseQueue, connection); 104 | } else { 105 | return MockWorkerLoop(MockWorker.responseQueue, 106 | connection: connection); 107 | } 108 | }), 109 | maxRetries: maxRetries); 110 | } 111 | 112 | test('should retry up to maxRetries times', () async { 113 | createDriver(); 114 | var expectedResponse = WorkResponse(); 115 | MockWorker.responseQueue.addAll( 116 | [disconnectedResponse, disconnectedResponse, expectedResponse]); 117 | var actualResponse = await driver!.doWork(WorkRequest()); 118 | // The first 2 null responses are thrown away, and we should get the 119 | // third one. 120 | expect(actualResponse, expectedResponse); 121 | 122 | expect(MockWorker.deadWorkers.length, 2); 123 | expect(MockWorker.liveWorkers.length, 1); 124 | }); 125 | 126 | test('should fail if it exceeds maxRetries failures', () async { 127 | createDriver(maxRetries: 2, numBadWorkers: 3); 128 | MockWorker.responseQueue.addAll( 129 | [disconnectedResponse, disconnectedResponse, WorkResponse()]); 130 | var actualResponse = await driver!.doWork(WorkRequest()); 131 | // Should actually get a bad response. 132 | expect(actualResponse.exitCode, 15); 133 | expect( 134 | actualResponse.output, 135 | 'Invalid response from worker, this probably means it wrote ' 136 | 'invalid output or died.'); 137 | 138 | expect(MockWorker.deadWorkers.length, 3); 139 | }); 140 | }); 141 | 142 | test('handles spawnWorker failures', () async { 143 | driver = BazelWorkerDriver(() async => throw StateError('oh no!'), 144 | maxRetries: 0); 145 | expect(driver!.doWork(WorkRequest()), throwsA(isA())); 146 | }); 147 | 148 | tearDown(() async { 149 | await driver?.terminateWorkers(); 150 | expect(MockWorker.liveWorkers, isEmpty); 151 | MockWorker.deadWorkers.clear(); 152 | MockWorker.responseQueue.clear(); 153 | }); 154 | }); 155 | } 156 | 157 | /// Runs [count] of fake work requests through [driver], and asserts that they 158 | /// all completed. 159 | Future _doRequests( 160 | {BazelWorkerDriver? driver, 161 | int count = 100, 162 | void Function(Future)? trackWork}) async { 163 | // If we create a driver, we need to make sure and terminate it. 164 | var terminateDriver = driver == null; 165 | driver ??= BazelWorkerDriver(MockWorker.spawn); 166 | var requests = List.generate(count, (_) => WorkRequest()); 167 | var responses = List.generate(count, (_) => WorkResponse()); 168 | MockWorker.responseQueue.addAll(responses); 169 | var actualResponses = await Future.wait( 170 | requests.map((request) => driver!.doWork(request, trackWork: trackWork))); 171 | expect(actualResponses, unorderedEquals(responses)); 172 | if (terminateDriver) await driver.terminateWorkers(); 173 | } 174 | 175 | /// A mock worker loop that returns work responses from the provided list. 176 | /// 177 | /// Throws if it runs out of responses. 178 | class MockWorkerLoop extends AsyncWorkerLoop { 179 | final Queue _responseQueue; 180 | 181 | MockWorkerLoop(this._responseQueue, {super.connection}); 182 | 183 | @override 184 | Future performRequest(WorkRequest request) async { 185 | print('Performing request $request'); 186 | return _responseQueue.removeFirst(); 187 | } 188 | } 189 | 190 | /// A mock worker loop with a custom `run` function that throws. 191 | class ThrowingMockWorkerLoop extends MockWorkerLoop { 192 | final MockWorker _mockWorker; 193 | 194 | ThrowingMockWorkerLoop(this._mockWorker, Queue responseQueue, 195 | AsyncWorkerConnection connection) 196 | : super(responseQueue, connection: connection); 197 | 198 | /// Run the worker loop. The returned [Future] doesn't complete until 199 | /// [connection#readRequest] returns `null`. 200 | @override 201 | Future run() async { 202 | while (true) { 203 | var request = await connection.readRequest(); 204 | if (request == null) break; 205 | await performRequest(request); 206 | _mockWorker.kill(); 207 | } 208 | } 209 | } 210 | 211 | /// A mock worker process. 212 | /// 213 | /// Items in [responseQueue] will be returned in order based on requests. 214 | /// 215 | /// If there are no items left in [responseQueue] then it will throw. 216 | class MockWorker implements Process { 217 | /// Spawns a new [MockWorker]. 218 | static Future spawn() async => MockWorker(); 219 | 220 | /// Static queue of pending responses, these are shared by all workers. 221 | /// 222 | /// If this is empty and a request is received then it will throw. 223 | static final responseQueue = Queue(); 224 | 225 | /// Static list of all live workers. 226 | static final liveWorkers = []; 227 | 228 | /// Static list of all the dead workers. 229 | static final deadWorkers = []; 230 | 231 | /// Standard constructor, creates a [WorkerLoop] from [workerLoopFactory] or 232 | /// a [MockWorkerLoop] if no factory is provided. 233 | MockWorker({WorkerLoop Function(MockWorker)? workerLoopFactory}) { 234 | liveWorkers.add(this); 235 | var workerLoop = workerLoopFactory != null 236 | ? workerLoopFactory(this) 237 | : MockWorkerLoop(responseQueue, 238 | connection: StdAsyncWorkerConnection( 239 | inputStream: _stdinController.stream, 240 | outputStream: _stdoutController.sink)); 241 | workerLoop.run(); 242 | } 243 | 244 | @override 245 | Future get exitCode => throw UnsupportedError('Not needed.'); 246 | 247 | @override 248 | Stream> get stdout => _stdoutController.stream; 249 | final _stdoutController = StreamController>(); 250 | 251 | @override 252 | Stream> get stderr => _stderrController.stream; 253 | final _stderrController = StreamController>(); 254 | 255 | @override 256 | late final IOSink stdin = IOSink(_stdinController.sink); 257 | final _stdinController = StreamController>(); 258 | 259 | @override 260 | int get pid => throw UnsupportedError('Not needed.'); 261 | 262 | @override 263 | bool kill( 264 | [ProcessSignal processSignal = ProcessSignal.sigterm, int exitCode = 0]) { 265 | if (_killed) return false; 266 | () async { 267 | await _stdoutController.close(); 268 | await _stderrController.close(); 269 | await _stdinController.close(); 270 | }(); 271 | deadWorkers.add(this); 272 | liveWorkers.remove(this); 273 | return true; 274 | } 275 | 276 | final _killed = false; 277 | } 278 | -------------------------------------------------------------------------------- /lib/src/worker_protocol.pb.dart: -------------------------------------------------------------------------------- 1 | // 2 | // Generated code. Do not modify. 3 | // source: worker_protocol.proto 4 | // 5 | // @dart = 2.12 6 | 7 | // ignore_for_file: annotate_overrides, camel_case_types, comment_references 8 | // ignore_for_file: constant_identifier_names, library_prefixes 9 | // ignore_for_file: non_constant_identifier_names, prefer_final_fields 10 | // ignore_for_file: unnecessary_import, unnecessary_this, unused_import 11 | 12 | import 'dart:core' as $core; 13 | 14 | import 'package:protobuf/protobuf.dart' as $pb; 15 | 16 | /// An input file. 17 | class Input extends $pb.GeneratedMessage { 18 | factory Input({ 19 | $core.String? path, 20 | $core.List<$core.int>? digest, 21 | }) { 22 | final $result = create(); 23 | if (path != null) { 24 | $result.path = path; 25 | } 26 | if (digest != null) { 27 | $result.digest = digest; 28 | } 29 | return $result; 30 | } 31 | Input._() : super(); 32 | factory Input.fromBuffer($core.List<$core.int> i, 33 | [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => 34 | create()..mergeFromBuffer(i, r); 35 | factory Input.fromJson($core.String i, 36 | [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => 37 | create()..mergeFromJson(i, r); 38 | 39 | static final $pb.BuilderInfo _i = $pb.BuilderInfo( 40 | _omitMessageNames ? '' : 'Input', 41 | package: const $pb.PackageName(_omitMessageNames ? '' : 'blaze.worker'), 42 | createEmptyInstance: create) 43 | ..aOS(1, _omitFieldNames ? '' : 'path') 44 | ..a<$core.List<$core.int>>( 45 | 2, _omitFieldNames ? '' : 'digest', $pb.PbFieldType.OY) 46 | ..hasRequiredFields = false; 47 | 48 | @$core.Deprecated('Using this can add significant overhead to your binary. ' 49 | 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' 50 | 'Will be removed in next major version') 51 | Input clone() => Input()..mergeFromMessage(this); 52 | @$core.Deprecated('Using this can add significant overhead to your binary. ' 53 | 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' 54 | 'Will be removed in next major version') 55 | Input copyWith(void Function(Input) updates) => 56 | super.copyWith((message) => updates(message as Input)) as Input; 57 | 58 | $pb.BuilderInfo get info_ => _i; 59 | 60 | @$core.pragma('dart2js:noInline') 61 | static Input create() => Input._(); 62 | Input createEmptyInstance() => create(); 63 | static $pb.PbList createRepeated() => $pb.PbList(); 64 | @$core.pragma('dart2js:noInline') 65 | static Input getDefault() => 66 | _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); 67 | static Input? _defaultInstance; 68 | 69 | /// The path in the file system where to read this input artifact from. This is 70 | /// either a path relative to the execution root (the worker process is 71 | /// launched with the working directory set to the execution root), or an 72 | /// absolute path. 73 | @$pb.TagNumber(1) 74 | $core.String get path => $_getSZ(0); 75 | @$pb.TagNumber(1) 76 | set path($core.String v) { 77 | $_setString(0, v); 78 | } 79 | 80 | @$pb.TagNumber(1) 81 | $core.bool hasPath() => $_has(0); 82 | @$pb.TagNumber(1) 83 | void clearPath() => clearField(1); 84 | 85 | /// A hash-value of the contents. The format of the contents is unspecified and 86 | /// the digest should be treated as an opaque token. This can be empty in some 87 | /// cases. 88 | @$pb.TagNumber(2) 89 | $core.List<$core.int> get digest => $_getN(1); 90 | @$pb.TagNumber(2) 91 | set digest($core.List<$core.int> v) { 92 | $_setBytes(1, v); 93 | } 94 | 95 | @$pb.TagNumber(2) 96 | $core.bool hasDigest() => $_has(1); 97 | @$pb.TagNumber(2) 98 | void clearDigest() => clearField(2); 99 | } 100 | 101 | /// This represents a single work unit that Blaze sends to the worker. 102 | class WorkRequest extends $pb.GeneratedMessage { 103 | factory WorkRequest({ 104 | $core.Iterable<$core.String>? arguments, 105 | $core.Iterable? inputs, 106 | $core.int? requestId, 107 | $core.bool? cancel, 108 | $core.int? verbosity, 109 | $core.String? sandboxDir, 110 | }) { 111 | final $result = create(); 112 | if (arguments != null) { 113 | $result.arguments.addAll(arguments); 114 | } 115 | if (inputs != null) { 116 | $result.inputs.addAll(inputs); 117 | } 118 | if (requestId != null) { 119 | $result.requestId = requestId; 120 | } 121 | if (cancel != null) { 122 | $result.cancel = cancel; 123 | } 124 | if (verbosity != null) { 125 | $result.verbosity = verbosity; 126 | } 127 | if (sandboxDir != null) { 128 | $result.sandboxDir = sandboxDir; 129 | } 130 | return $result; 131 | } 132 | WorkRequest._() : super(); 133 | factory WorkRequest.fromBuffer($core.List<$core.int> i, 134 | [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => 135 | create()..mergeFromBuffer(i, r); 136 | factory WorkRequest.fromJson($core.String i, 137 | [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => 138 | create()..mergeFromJson(i, r); 139 | 140 | static final $pb.BuilderInfo _i = $pb.BuilderInfo( 141 | _omitMessageNames ? '' : 'WorkRequest', 142 | package: const $pb.PackageName(_omitMessageNames ? '' : 'blaze.worker'), 143 | createEmptyInstance: create) 144 | ..pPS(1, _omitFieldNames ? '' : 'arguments') 145 | ..pc(2, _omitFieldNames ? '' : 'inputs', $pb.PbFieldType.PM, 146 | subBuilder: Input.create) 147 | ..a<$core.int>(3, _omitFieldNames ? '' : 'requestId', $pb.PbFieldType.O3) 148 | ..aOB(4, _omitFieldNames ? '' : 'cancel') 149 | ..a<$core.int>(5, _omitFieldNames ? '' : 'verbosity', $pb.PbFieldType.O3) 150 | ..aOS(6, _omitFieldNames ? '' : 'sandboxDir') 151 | ..hasRequiredFields = false; 152 | 153 | @$core.Deprecated('Using this can add significant overhead to your binary. ' 154 | 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' 155 | 'Will be removed in next major version') 156 | WorkRequest clone() => WorkRequest()..mergeFromMessage(this); 157 | @$core.Deprecated('Using this can add significant overhead to your binary. ' 158 | 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' 159 | 'Will be removed in next major version') 160 | WorkRequest copyWith(void Function(WorkRequest) updates) => 161 | super.copyWith((message) => updates(message as WorkRequest)) 162 | as WorkRequest; 163 | 164 | $pb.BuilderInfo get info_ => _i; 165 | 166 | @$core.pragma('dart2js:noInline') 167 | static WorkRequest create() => WorkRequest._(); 168 | WorkRequest createEmptyInstance() => create(); 169 | static $pb.PbList createRepeated() => $pb.PbList(); 170 | @$core.pragma('dart2js:noInline') 171 | static WorkRequest getDefault() => _defaultInstance ??= 172 | $pb.GeneratedMessage.$_defaultFor(create); 173 | static WorkRequest? _defaultInstance; 174 | 175 | @$pb.TagNumber(1) 176 | $core.List<$core.String> get arguments => $_getList(0); 177 | 178 | /// The inputs that the worker is allowed to read during execution of this 179 | /// request. 180 | @$pb.TagNumber(2) 181 | $core.List get inputs => $_getList(1); 182 | 183 | /// Each WorkRequest must have either a unique 184 | /// request_id or request_id = 0. If request_id is 0, this WorkRequest must be 185 | /// processed alone (singleplex), otherwise the worker may process multiple 186 | /// WorkRequests in parallel (multiplexing). As an exception to the above, if 187 | /// the cancel field is true, the request_id must be the same as a previously 188 | /// sent WorkRequest. The request_id must be attached unchanged to the 189 | /// corresponding WorkResponse. Only one singleplex request may be sent to a 190 | /// worker at a time. 191 | @$pb.TagNumber(3) 192 | $core.int get requestId => $_getIZ(2); 193 | @$pb.TagNumber(3) 194 | set requestId($core.int v) { 195 | $_setSignedInt32(2, v); 196 | } 197 | 198 | @$pb.TagNumber(3) 199 | $core.bool hasRequestId() => $_has(2); 200 | @$pb.TagNumber(3) 201 | void clearRequestId() => clearField(3); 202 | 203 | /// EXPERIMENTAL: When true, this is a cancel request, indicating that a 204 | /// previously sent WorkRequest with the same request_id should be cancelled. 205 | /// The arguments and inputs fields must be empty and should be ignored. 206 | @$pb.TagNumber(4) 207 | $core.bool get cancel => $_getBF(3); 208 | @$pb.TagNumber(4) 209 | set cancel($core.bool v) { 210 | $_setBool(3, v); 211 | } 212 | 213 | @$pb.TagNumber(4) 214 | $core.bool hasCancel() => $_has(3); 215 | @$pb.TagNumber(4) 216 | void clearCancel() => clearField(4); 217 | 218 | /// Values greater than 0 indicate that the worker may output extra debug 219 | /// information to stderr (which will go into the worker log). Setting the 220 | /// --worker_verbose flag for Bazel makes this flag default to 10. 221 | @$pb.TagNumber(5) 222 | $core.int get verbosity => $_getIZ(4); 223 | @$pb.TagNumber(5) 224 | set verbosity($core.int v) { 225 | $_setSignedInt32(4, v); 226 | } 227 | 228 | @$pb.TagNumber(5) 229 | $core.bool hasVerbosity() => $_has(4); 230 | @$pb.TagNumber(5) 231 | void clearVerbosity() => clearField(5); 232 | 233 | /// The relative directory inside the workers working directory where the 234 | /// inputs and outputs are placed, for sandboxing purposes. For singleplex 235 | /// workers, this is unset, as they can use their working directory as sandbox. 236 | /// For multiplex workers, this will be set when the 237 | /// --experimental_worker_multiplex_sandbox flag is set _and_ the execution 238 | /// requirements for the worker includes 'supports-multiplex-sandbox'. 239 | /// The paths in `inputs` will not contain this prefix, but the actual files 240 | /// will be placed/must be written relative to this directory. The worker 241 | /// implementation is responsible for resolving the file paths. 242 | @$pb.TagNumber(6) 243 | $core.String get sandboxDir => $_getSZ(5); 244 | @$pb.TagNumber(6) 245 | set sandboxDir($core.String v) { 246 | $_setString(5, v); 247 | } 248 | 249 | @$pb.TagNumber(6) 250 | $core.bool hasSandboxDir() => $_has(5); 251 | @$pb.TagNumber(6) 252 | void clearSandboxDir() => clearField(6); 253 | } 254 | 255 | /// The worker sends this message to Blaze when it finished its work on the 256 | /// WorkRequest message. 257 | class WorkResponse extends $pb.GeneratedMessage { 258 | factory WorkResponse({ 259 | $core.int? exitCode, 260 | $core.String? output, 261 | $core.int? requestId, 262 | $core.bool? wasCancelled, 263 | }) { 264 | final $result = create(); 265 | if (exitCode != null) { 266 | $result.exitCode = exitCode; 267 | } 268 | if (output != null) { 269 | $result.output = output; 270 | } 271 | if (requestId != null) { 272 | $result.requestId = requestId; 273 | } 274 | if (wasCancelled != null) { 275 | $result.wasCancelled = wasCancelled; 276 | } 277 | return $result; 278 | } 279 | WorkResponse._() : super(); 280 | factory WorkResponse.fromBuffer($core.List<$core.int> i, 281 | [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => 282 | create()..mergeFromBuffer(i, r); 283 | factory WorkResponse.fromJson($core.String i, 284 | [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => 285 | create()..mergeFromJson(i, r); 286 | 287 | static final $pb.BuilderInfo _i = $pb.BuilderInfo( 288 | _omitMessageNames ? '' : 'WorkResponse', 289 | package: const $pb.PackageName(_omitMessageNames ? '' : 'blaze.worker'), 290 | createEmptyInstance: create) 291 | ..a<$core.int>(1, _omitFieldNames ? '' : 'exitCode', $pb.PbFieldType.O3) 292 | ..aOS(2, _omitFieldNames ? '' : 'output') 293 | ..a<$core.int>(3, _omitFieldNames ? '' : 'requestId', $pb.PbFieldType.O3) 294 | ..aOB(4, _omitFieldNames ? '' : 'wasCancelled') 295 | ..hasRequiredFields = false; 296 | 297 | @$core.Deprecated('Using this can add significant overhead to your binary. ' 298 | 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' 299 | 'Will be removed in next major version') 300 | WorkResponse clone() => WorkResponse()..mergeFromMessage(this); 301 | @$core.Deprecated('Using this can add significant overhead to your binary. ' 302 | 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' 303 | 'Will be removed in next major version') 304 | WorkResponse copyWith(void Function(WorkResponse) updates) => 305 | super.copyWith((message) => updates(message as WorkResponse)) 306 | as WorkResponse; 307 | 308 | $pb.BuilderInfo get info_ => _i; 309 | 310 | @$core.pragma('dart2js:noInline') 311 | static WorkResponse create() => WorkResponse._(); 312 | WorkResponse createEmptyInstance() => create(); 313 | static $pb.PbList createRepeated() => 314 | $pb.PbList(); 315 | @$core.pragma('dart2js:noInline') 316 | static WorkResponse getDefault() => _defaultInstance ??= 317 | $pb.GeneratedMessage.$_defaultFor(create); 318 | static WorkResponse? _defaultInstance; 319 | 320 | @$pb.TagNumber(1) 321 | $core.int get exitCode => $_getIZ(0); 322 | @$pb.TagNumber(1) 323 | set exitCode($core.int v) { 324 | $_setSignedInt32(0, v); 325 | } 326 | 327 | @$pb.TagNumber(1) 328 | $core.bool hasExitCode() => $_has(0); 329 | @$pb.TagNumber(1) 330 | void clearExitCode() => clearField(1); 331 | 332 | /// This is printed to the user after the WorkResponse has been received and is 333 | /// supposed to contain compiler warnings / errors etc. - thus we'll use a 334 | /// string type here, which gives us UTF-8 encoding. 335 | @$pb.TagNumber(2) 336 | $core.String get output => $_getSZ(1); 337 | @$pb.TagNumber(2) 338 | set output($core.String v) { 339 | $_setString(1, v); 340 | } 341 | 342 | @$pb.TagNumber(2) 343 | $core.bool hasOutput() => $_has(1); 344 | @$pb.TagNumber(2) 345 | void clearOutput() => clearField(2); 346 | 347 | /// This field must be set to the same request_id as the WorkRequest it is a 348 | /// response to. Since worker processes which support multiplex worker will 349 | /// handle multiple WorkRequests in parallel, this ID will be used to 350 | /// determined which WorkerProxy does this WorkResponse belong to. 351 | @$pb.TagNumber(3) 352 | $core.int get requestId => $_getIZ(2); 353 | @$pb.TagNumber(3) 354 | set requestId($core.int v) { 355 | $_setSignedInt32(2, v); 356 | } 357 | 358 | @$pb.TagNumber(3) 359 | $core.bool hasRequestId() => $_has(2); 360 | @$pb.TagNumber(3) 361 | void clearRequestId() => clearField(3); 362 | 363 | /// EXPERIMENTAL When true, indicates that this response was sent due to 364 | /// receiving a cancel request. The exit_code and output fields should be empty 365 | /// and will be ignored. Exactly one WorkResponse must be sent for each 366 | /// non-cancelling WorkRequest received by the worker, but if the worker 367 | /// received a cancel request, it doesn't matter if it replies with a regular 368 | /// WorkResponse or with one where was_cancelled = true. 369 | @$pb.TagNumber(4) 370 | $core.bool get wasCancelled => $_getBF(3); 371 | @$pb.TagNumber(4) 372 | set wasCancelled($core.bool v) { 373 | $_setBool(3, v); 374 | } 375 | 376 | @$pb.TagNumber(4) 377 | $core.bool hasWasCancelled() => $_has(3); 378 | @$pb.TagNumber(4) 379 | void clearWasCancelled() => clearField(4); 380 | } 381 | 382 | const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names'); 383 | const _omitMessageNames = 384 | $core.bool.fromEnvironment('protobuf.omit_message_names'); 385 | --------------------------------------------------------------------------------