render-workflows-dart 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +193 -0
- package/LICENSE +21 -0
- package/README.md +678 -0
- package/dart/generator/bin/generate.dart +419 -0
- package/dart/generator/pubspec.lock +149 -0
- package/dart/generator/pubspec.yaml +9 -0
- package/examples/README.md +56 -0
- package/examples/default/README.md +18 -0
- package/examples/default/gitignore +7 -0
- package/examples/default/index.js +2 -0
- package/examples/default/package.json +17 -0
- package/examples/default/pubspec.yaml +7 -0
- package/examples/default/tasks.dart +39 -0
- package/examples/http/README.md +26 -0
- package/examples/http/gitignore +7 -0
- package/examples/http/index.js +2 -0
- package/examples/http/package.json +18 -0
- package/examples/http/pubspec.yaml +13 -0
- package/examples/http/tasks.dart +100 -0
- package/examples/introspect/README.md +50 -0
- package/examples/introspect/gitignore +7 -0
- package/examples/introspect/index.js +2 -0
- package/examples/introspect/node_env.dart +35 -0
- package/examples/introspect/package.json +18 -0
- package/examples/introspect/pubspec.yaml +23 -0
- package/examples/introspect/tasks.dart +144 -0
- package/examples/native/README.md +32 -0
- package/examples/native/gitignore +7 -0
- package/examples/native/index.js +2 -0
- package/examples/native/native/tools_impl.dart +105 -0
- package/examples/native/package.json +23 -0
- package/examples/native/pubspec.yaml +7 -0
- package/examples/native/tasks.dart +35 -0
- package/examples/postgres/README.md +73 -0
- package/examples/postgres/gitignore +7 -0
- package/examples/postgres/index.js +2 -0
- package/examples/postgres/native/db_impl.dart +205 -0
- package/examples/postgres/package.json +26 -0
- package/examples/postgres/pubspec.yaml +14 -0
- package/examples/postgres/seed/bin/seed.dart +56 -0
- package/examples/postgres/seed/bin/show.dart +64 -0
- package/examples/postgres/seed/lib/src/connect.dart +93 -0
- package/examples/postgres/seed/lib/src/schema.dart +46 -0
- package/examples/postgres/seed/pubspec.yaml +17 -0
- package/examples/postgres/tasks.dart +66 -0
- package/package.json +51 -0
- package/runtime/AGENTS.md +141 -0
- package/runtime/CLAUDE.md +2 -0
- package/runtime/native_task.dart +115 -0
- package/runtime/render_dart.dart +428 -0
- package/src/cli.js +396 -0
- package/src/native-worker.js +196 -0
- package/src/node-bridge.js +118 -0
- package/src/runtime.js +108 -0
- package/src/toolchain/compile.js +153 -0
- package/src/toolchain/dart-sdk.js +216 -0
- package/src/toolchain/dart-version.js +113 -0
- package/src/toolchain/generate.js +112 -0
- package/src/toolchain/index.js +8 -0
- package/src/toolchain/native.js +217 -0
- package/src/web-shims.js +281 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import 'render_dart.dart';
|
|
2
|
+
|
|
3
|
+
/// Your Render Workflows tasks, in Dart.
|
|
4
|
+
///
|
|
5
|
+
/// Arguments and return values must be JSON-serialisable, and Render caps a
|
|
6
|
+
/// single invocation's input at 4 MB.
|
|
7
|
+
void main() {
|
|
8
|
+
// A leaf task: pure computation.
|
|
9
|
+
task('calculateSquare', (args) async {
|
|
10
|
+
final n = args[0]! as int;
|
|
11
|
+
print('calculateSquare($n)');
|
|
12
|
+
return n * n;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// A parent task. Every callTask becomes its own task run on its own
|
|
16
|
+
// instance, which is how you fan work out across Render.
|
|
17
|
+
task('sumSquares', (args) async {
|
|
18
|
+
final values = args[0]! as List<Object?>;
|
|
19
|
+
|
|
20
|
+
var total = 0;
|
|
21
|
+
for (final v in values) {
|
|
22
|
+
final square = await callTask('calculateSquare', [v]);
|
|
23
|
+
total += square! as int;
|
|
24
|
+
}
|
|
25
|
+
return total;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Options are typed and validated before they reach Render.
|
|
29
|
+
task(
|
|
30
|
+
'flaky',
|
|
31
|
+
(args) async {
|
|
32
|
+
throw StateError('this always fails, to show error reporting');
|
|
33
|
+
},
|
|
34
|
+
retry: const Retry(maxRetries: 2, waitDurationMs: 1000),
|
|
35
|
+
plan: TaskPlan.starter,
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
start();
|
|
39
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# http
|
|
2
|
+
|
|
3
|
+
Calling external APIs from a task. No native compilation needed.
|
|
4
|
+
|
|
5
|
+
`package:http` works under dart2js because its web implementation goes through
|
|
6
|
+
the platform's networking rather than `dart:io` sockets. `dart:io`'s
|
|
7
|
+
`HttpClient` does **not** — it compiles and then throws on first use, which is
|
|
8
|
+
why the build refuses a direct `dart:io` import.
|
|
9
|
+
|
|
10
|
+
| Task | Shows |
|
|
11
|
+
| --- | --- |
|
|
12
|
+
| `fetchRepo` | One request, with a timeout, and a real error on a bad status |
|
|
13
|
+
| `fetchWithRetry` | Retrying only what is worth retrying, with backoff |
|
|
14
|
+
| `fetchMany` | Overlapping requests — I/O concurrency needs no extra cores |
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install && npx render-dart dev
|
|
18
|
+
render workflows start fetchRepo --local --input='["dart-lang/sdk"]'
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Uses GitHub's public API, which is rate-limited without a token — fine for a
|
|
22
|
+
demonstration.
|
|
23
|
+
|
|
24
|
+
**Retry here, or Render's?** `Retry` on the task re-runs the whole thing, which
|
|
25
|
+
is right when a task fails. Retrying one request inside a task, as
|
|
26
|
+
`fetchWithRetry` does, avoids paying for another run.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "render-dart-http-example",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Calling external APIs from a Render Workflows task written in Dart",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "render-dart build",
|
|
8
|
+
"start": "node index.js",
|
|
9
|
+
"dev": "render-dart dev"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@renderinc/sdk": "^0.6.0",
|
|
13
|
+
"render-dart": "^0.7.1"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
name: render_dart_http_example
|
|
2
|
+
description: Calling external APIs from a Render Workflows task.
|
|
3
|
+
publish_to: none
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
|
|
6
|
+
environment:
|
|
7
|
+
sdk: ^3.12.0
|
|
8
|
+
|
|
9
|
+
dependencies:
|
|
10
|
+
# Works under dart2js because its web implementation goes through the
|
|
11
|
+
# browser's networking rather than dart:io sockets. This is the reason
|
|
12
|
+
# HTTP needs no native task.
|
|
13
|
+
http: ^1.2.0
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import 'dart:convert';
|
|
2
|
+
|
|
3
|
+
import 'package:http/http.dart' as http;
|
|
4
|
+
|
|
5
|
+
import 'render_dart.dart';
|
|
6
|
+
|
|
7
|
+
/// Calling an external API from a task.
|
|
8
|
+
///
|
|
9
|
+
/// `package:http` works under dart2js — its web implementation goes through
|
|
10
|
+
/// the browser's networking, which render-dart's runtime provides on Node. That
|
|
11
|
+
/// is why fetching a URL needs no native task, while touching a file or a
|
|
12
|
+
/// socket does.
|
|
13
|
+
///
|
|
14
|
+
/// `dart:io`'s `HttpClient` does **not** work: it compiles and then throws
|
|
15
|
+
/// `Unsupported operation` on first use. The build refuses a direct `dart:io`
|
|
16
|
+
/// import for exactly this reason.
|
|
17
|
+
void main() {
|
|
18
|
+
/// A single request, with a timeout.
|
|
19
|
+
///
|
|
20
|
+
/// Render bounds a task run anyway, but a per-request timeout turns a hung
|
|
21
|
+
/// dependency into a quick, legible failure instead of burning the run's
|
|
22
|
+
/// whole budget.
|
|
23
|
+
task('fetchRepo', (args) async {
|
|
24
|
+
final name = args.isEmpty ? 'dart-lang/sdk' : args[0]! as String;
|
|
25
|
+
|
|
26
|
+
final response = await http
|
|
27
|
+
.get(
|
|
28
|
+
Uri.https('api.github.com', '/repos/$name'),
|
|
29
|
+
headers: {'accept': 'application/vnd.github+json'},
|
|
30
|
+
)
|
|
31
|
+
.timeout(const Duration(seconds: 10));
|
|
32
|
+
|
|
33
|
+
if (response.statusCode != 200) {
|
|
34
|
+
// Throwing gives Render the real message in the run record.
|
|
35
|
+
throw StateError('GitHub answered ${response.statusCode} for $name');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
final json = jsonDecode(response.body) as Map<String, Object?>;
|
|
39
|
+
return {
|
|
40
|
+
'name': json['full_name'],
|
|
41
|
+
'stars': json['stargazers_count'],
|
|
42
|
+
'language': json['language'],
|
|
43
|
+
'pushedAt': json['pushed_at'],
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/// Retrying on the failures that are worth retrying.
|
|
48
|
+
///
|
|
49
|
+
/// Render's own `retry` option re-runs the whole task, which is the right
|
|
50
|
+
/// tool for a task that fails. This is the finer-grained version: retry one
|
|
51
|
+
/// request without paying for another run.
|
|
52
|
+
task('fetchWithRetry', (args) async {
|
|
53
|
+
final url = Uri.parse(args[0]! as String);
|
|
54
|
+
final attempts = args.length < 2 ? 3 : args[1]! as int;
|
|
55
|
+
|
|
56
|
+
for (var attempt = 1; ; attempt++) {
|
|
57
|
+
try {
|
|
58
|
+
final response = await http.get(url).timeout(const Duration(seconds: 10));
|
|
59
|
+
|
|
60
|
+
// 4xx will not improve by asking again; 5xx and timeouts might.
|
|
61
|
+
if (response.statusCode < 500) {
|
|
62
|
+
return {
|
|
63
|
+
'status': response.statusCode,
|
|
64
|
+
'attempts': attempt,
|
|
65
|
+
'bytes': response.bodyBytes.length,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (attempt >= attempts) {
|
|
69
|
+
throw StateError('$url still answering ${response.statusCode} '
|
|
70
|
+
'after $attempt attempt(s)');
|
|
71
|
+
}
|
|
72
|
+
} catch (e) {
|
|
73
|
+
if (attempt >= attempts) rethrow;
|
|
74
|
+
print('attempt $attempt failed: $e');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Exponential backoff, so a struggling service is not hammered.
|
|
78
|
+
await Future<void>.delayed(Duration(milliseconds: 250 * (1 << (attempt - 1))));
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
/// Fetching several URLs at once.
|
|
83
|
+
///
|
|
84
|
+
/// dart2js is single-threaded, but HTTP is I/O — these overlap happily. Work
|
|
85
|
+
/// that needs actual cores is the case for a native task with isolates.
|
|
86
|
+
task('fetchMany', (args) async {
|
|
87
|
+
final urls = (args[0]! as List<Object?>).cast<String>();
|
|
88
|
+
|
|
89
|
+
final responses = await Future.wait(
|
|
90
|
+
urls.map((u) => http.get(Uri.parse(u)).timeout(const Duration(seconds: 10))),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
return [
|
|
94
|
+
for (var i = 0; i < urls.length; i++)
|
|
95
|
+
{'url': urls[i], 'status': responses[i].statusCode, 'bytes': responses[i].bodyBytes.length},
|
|
96
|
+
];
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
start();
|
|
100
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# introspect
|
|
2
|
+
|
|
3
|
+
A workflow task that uses Render's own API — to inspect the workspace, and to
|
|
4
|
+
run a task in a *different* workflow.
|
|
5
|
+
|
|
6
|
+
No native compilation. `package:render_api` runs under dart2js because it uses
|
|
7
|
+
`package:http`, whose web implementation goes through the platform's
|
|
8
|
+
networking.
|
|
9
|
+
|
|
10
|
+
| Task | Shows |
|
|
11
|
+
| --- | --- |
|
|
12
|
+
| `auditDatabases` | Every Postgres instance, and how long a free one has left |
|
|
13
|
+
| `listServices` | The workflow services, with the root dir that governs autodeploy |
|
|
14
|
+
| `runElsewhere` | Starting a task in another workflow and waiting for it |
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install && npx render-dart dev
|
|
18
|
+
render workflows start auditDatabases --local --input='[]'
|
|
19
|
+
render workflows start runElsewhere --local \
|
|
20
|
+
--input='["some-workflow/someTask", [7]]'
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Needs `RENDER_API_KEY` on the service.
|
|
24
|
+
|
|
25
|
+
## Why this is not just a curiosity
|
|
26
|
+
|
|
27
|
+
`callTask` starts a **subtask of the current workflow**. Reaching a task in
|
|
28
|
+
another service has no bridge equivalent — it goes through the API. So
|
|
29
|
+
cross-workflow orchestration genuinely needs the SDK, and `runElsewhere` is
|
|
30
|
+
the shape of it.
|
|
31
|
+
|
|
32
|
+
The other natural use is scheduled operations: `auditDatabases` is one query
|
|
33
|
+
away from a task that warns before a free instance is deleted.
|
|
34
|
+
|
|
35
|
+
## Two things the web target forces
|
|
36
|
+
|
|
37
|
+
**There is no environment**, so `Platform.environment` is unavailable and a
|
|
38
|
+
client cannot pick up `RENDER_API_KEY` by itself. `node_env.dart` reaches
|
|
39
|
+
Node's `process.env` through the `require` that render-dart's runtime hoists,
|
|
40
|
+
and the token is passed explicitly.
|
|
41
|
+
|
|
42
|
+
**Server-sent events are unreliable there**, so `runElsewhere` polls.
|
|
43
|
+
|
|
44
|
+
`_awaitRun` polls the **run's own** status. A run has attempts, and an attempt
|
|
45
|
+
reaches a terminal state before the run does — watching `attempts[].status`
|
|
46
|
+
reports a run as finished while it is still going, which looks fine until a
|
|
47
|
+
retry happens. `package:render_workflows` has `waitFor`, which does this
|
|
48
|
+
properly; this example cannot depend on it yet, because that package depends on
|
|
49
|
+
`render_api` by path and a path dependency cannot resolve outside its own git
|
|
50
|
+
repository. That resolves when `render_api` reaches pub.dev.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/// Reading environment variables from a task.
|
|
2
|
+
///
|
|
3
|
+
/// dart2js selects the *web* implementation of anything platform-specific, and
|
|
4
|
+
/// the web has no environment — so `Platform.environment` is unavailable and
|
|
5
|
+
/// packages that read it find nothing. Node does have one; this reaches it
|
|
6
|
+
/// through the `require` that render-dart's runtime hoists.
|
|
7
|
+
///
|
|
8
|
+
/// This is why a task must pass an API token explicitly rather than letting a
|
|
9
|
+
/// client pick it up from the environment.
|
|
10
|
+
library;
|
|
11
|
+
|
|
12
|
+
import 'dart:js_interop';
|
|
13
|
+
import 'dart:js_interop_unsafe';
|
|
14
|
+
|
|
15
|
+
@JS('__require')
|
|
16
|
+
external JSObject _require(String id);
|
|
17
|
+
|
|
18
|
+
/// The value of [name] in the process environment, or null.
|
|
19
|
+
String? nodeEnv(String name) {
|
|
20
|
+
final env = _require('node:process')['env'] as JSObject;
|
|
21
|
+
final value = env[name];
|
|
22
|
+
return value.isUndefinedOrNull ? null : (value! as JSString).toDart;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// The value of [name], or a clear failure naming what to set.
|
|
26
|
+
String requireEnv(String name) {
|
|
27
|
+
final value = nodeEnv(name);
|
|
28
|
+
if (value == null || value.isEmpty) {
|
|
29
|
+
throw StateError(
|
|
30
|
+
'$name is not set on this service. Add it in the Dashboard under the '
|
|
31
|
+
"service's Environment, or pass it when creating the workflow.",
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "render-dart-introspect-example",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "A Render Workflows task that uses Render's own API and Workflows SDKs",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "render-dart build",
|
|
8
|
+
"start": "node index.js",
|
|
9
|
+
"dev": "render-dart dev"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@renderinc/sdk": "^0.6.0",
|
|
13
|
+
"render-dart": "^0.7.1"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: render_dart_introspect_example
|
|
2
|
+
description: A workflow task that inspects and orchestrates Render itself.
|
|
3
|
+
publish_to: none
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
|
|
6
|
+
environment:
|
|
7
|
+
sdk: ^3.12.0
|
|
8
|
+
|
|
9
|
+
dependencies:
|
|
10
|
+
# Runs under dart2js: it uses package:http, whose web implementation goes
|
|
11
|
+
# through the browser's networking rather than dart:io sockets. So these
|
|
12
|
+
# tasks need no native compilation.
|
|
13
|
+
#
|
|
14
|
+
# A git dependency while the package is unpublished; becomes a version
|
|
15
|
+
# constraint once it is on pub.dev.
|
|
16
|
+
#
|
|
17
|
+
# package:render_workflows would be the better tool for the waiting in
|
|
18
|
+
# runElsewhere, but it cannot be fetched this way yet: it depends on
|
|
19
|
+
# render_api by path, and a path dependency cannot resolve outside its own
|
|
20
|
+
# git repository. That resolves itself when render_api reaches pub.dev.
|
|
21
|
+
render_api:
|
|
22
|
+
git:
|
|
23
|
+
url: https://github.com/timmaffett/render_api_sdk.git
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import 'package:render_api/render_api.dart';
|
|
2
|
+
|
|
3
|
+
import 'node_env.dart';
|
|
4
|
+
import 'render_dart.dart';
|
|
5
|
+
|
|
6
|
+
/// Tasks that inspect and orchestrate Render itself.
|
|
7
|
+
///
|
|
8
|
+
/// Both SDKs run under dart2js — they use `package:http`, whose web
|
|
9
|
+
/// implementation goes through the browser's networking rather than dart:io
|
|
10
|
+
/// sockets — so none of this needs a native task.
|
|
11
|
+
///
|
|
12
|
+
/// Two things follow from running on the web target:
|
|
13
|
+
///
|
|
14
|
+
/// * There is no environment, so the API token must be passed explicitly.
|
|
15
|
+
/// `node_env.dart` reaches Node's `process.env` through the bridge.
|
|
16
|
+
/// * Server-sent events are unreliable there, so `runElsewhere` polls.
|
|
17
|
+
///
|
|
18
|
+
/// Needs `RENDER_API_KEY` set on the service. A workflow's environment can only
|
|
19
|
+
/// be set when it is created, so pass it then — `PATCH /workflows/{id}` cannot
|
|
20
|
+
/// add one afterwards.
|
|
21
|
+
void main() {
|
|
22
|
+
/// Reports every Postgres instance, and how long a free one has left.
|
|
23
|
+
///
|
|
24
|
+
/// A free instance is **deleted** 30 days after creation, which is the sort
|
|
25
|
+
/// of thing worth a scheduled task rather than a diary entry.
|
|
26
|
+
task('auditDatabases', (args) async {
|
|
27
|
+
final render = RenderApi(token: requireEnv('RENDER_API_KEY'));
|
|
28
|
+
try {
|
|
29
|
+
final instances = await render.listPostgres(limit: 50);
|
|
30
|
+
final now = DateTime.now();
|
|
31
|
+
|
|
32
|
+
return [
|
|
33
|
+
for (final entry in instances)
|
|
34
|
+
{
|
|
35
|
+
'name': entry.postgres.name,
|
|
36
|
+
'id': entry.postgres.id,
|
|
37
|
+
'plan': entry.postgres.plan.wireValue,
|
|
38
|
+
'region': entry.postgres.region.wireValue,
|
|
39
|
+
'status': entry.postgres.status.wireValue,
|
|
40
|
+
'expiresInDays': entry.postgres.expiresAt == null
|
|
41
|
+
? null
|
|
42
|
+
: entry.postgres.expiresAt!.difference(now).inDays,
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
} finally {
|
|
46
|
+
render.close();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/// Lists the workflow services in this workspace.
|
|
51
|
+
///
|
|
52
|
+
/// Root directory is the field worth reading: autodeploy fires only for
|
|
53
|
+
/// commits touching it.
|
|
54
|
+
task('listServices', (args) async {
|
|
55
|
+
final render = RenderApi(token: requireEnv('RENDER_API_KEY'));
|
|
56
|
+
try {
|
|
57
|
+
final workflows = await render.listWorkflows(limit: 50);
|
|
58
|
+
return [
|
|
59
|
+
for (final entry in workflows)
|
|
60
|
+
{
|
|
61
|
+
'name': entry.workflow.name,
|
|
62
|
+
'id': entry.workflow.id,
|
|
63
|
+
'rootDir': entry.workflow.buildConfig.rootDir ?? '(repo root)',
|
|
64
|
+
'autoDeploy': entry.workflow.autoDeployTrigger?.wireValue ?? 'off',
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
} finally {
|
|
68
|
+
render.close();
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
/// Runs a task in a **different** workflow, and waits for it.
|
|
73
|
+
///
|
|
74
|
+
/// This is the thing `callTask` cannot do. `callTask` starts a subtask of
|
|
75
|
+
/// *this* workflow; reaching another service means going through the API, so
|
|
76
|
+
/// cross-workflow orchestration needs the SDK rather than the bridge.
|
|
77
|
+
///
|
|
78
|
+
/// The slug is `workflow-slug/task-name`.
|
|
79
|
+
task('runElsewhere', (args) async {
|
|
80
|
+
final slug = args[0]! as String;
|
|
81
|
+
final input = args.length < 2 ? const <Object?>[] : args[1]! as List<Object?>;
|
|
82
|
+
|
|
83
|
+
final render = RenderApi(token: requireEnv('RENDER_API_KEY'));
|
|
84
|
+
try {
|
|
85
|
+
// Render calls this operation `createTask`, but it is "run task".
|
|
86
|
+
final started = await render.createTask(
|
|
87
|
+
body: CreateTaskRequest(task: slug, input: input),
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
final finished = await _awaitRun(render, started.id);
|
|
91
|
+
return {
|
|
92
|
+
'taskRunId': finished.id,
|
|
93
|
+
'status': finished.status.wireValue,
|
|
94
|
+
'result': finished.results.isEmpty ? null : finished.results.first,
|
|
95
|
+
'error': finished.error,
|
|
96
|
+
};
|
|
97
|
+
} finally {
|
|
98
|
+
render.close();
|
|
99
|
+
}
|
|
100
|
+
}, timeoutSeconds: 300);
|
|
101
|
+
|
|
102
|
+
start();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// Terminal states for a task run.
|
|
106
|
+
///
|
|
107
|
+
/// Both `completed` and `succeeded` exist in the API, which is exactly the
|
|
108
|
+
/// sort of detail that makes a hand-rolled status check wrong.
|
|
109
|
+
const _terminal = {
|
|
110
|
+
GetTaskRunStatus.completed,
|
|
111
|
+
GetTaskRunStatus.succeeded,
|
|
112
|
+
GetTaskRunStatus.failed,
|
|
113
|
+
GetTaskRunStatus.canceled,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/// Polls a task run until it finishes.
|
|
117
|
+
///
|
|
118
|
+
/// Watch the **run's own** status. A run has attempts, and an attempt reaches a
|
|
119
|
+
/// terminal state before the run does — polling `attempts[].status` reports a
|
|
120
|
+
/// run as finished while it is still going, which is a mistake that looks like
|
|
121
|
+
/// it works until a retry happens.
|
|
122
|
+
///
|
|
123
|
+
/// `package:render_workflows` has `waitFor`, which does this properly and
|
|
124
|
+
/// handles timeouts. Prefer it once that package is on pub.dev; this loop
|
|
125
|
+
/// exists because a git dependency on it cannot resolve yet.
|
|
126
|
+
Future<GetTaskRunResponse> _awaitRun(
|
|
127
|
+
RenderApi render,
|
|
128
|
+
String taskRunId, {
|
|
129
|
+
Duration pollInterval = const Duration(milliseconds: 500),
|
|
130
|
+
Duration timeout = const Duration(minutes: 5),
|
|
131
|
+
}) async {
|
|
132
|
+
final deadline = DateTime.now().add(timeout);
|
|
133
|
+
|
|
134
|
+
while (true) {
|
|
135
|
+
final run = await render.getTaskRun(taskRunId: taskRunId);
|
|
136
|
+
if (_terminal.contains(run.status)) return run;
|
|
137
|
+
|
|
138
|
+
if (DateTime.now().isAfter(deadline)) {
|
|
139
|
+
throw StateError('$taskRunId did not finish within ${timeout.inSeconds}s '
|
|
140
|
+
'(last status: ${run.status.wireValue})');
|
|
141
|
+
}
|
|
142
|
+
await Future<void>.delayed(pollInterval);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# native
|
|
2
|
+
|
|
3
|
+
`dart:io`, `dart:ffi` and real isolates — the things dart2js cannot do.
|
|
4
|
+
|
|
5
|
+
The implementation lives in `native/tools_impl.dart` and is compiled AOT.
|
|
6
|
+
`tasks.dart` imports the generated `native/tools.dart` and calls it by name;
|
|
7
|
+
nothing at the call site says it is native.
|
|
8
|
+
|
|
9
|
+
| Task | Shows |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| `readFile` | `dart:io` — impossible under dart2js |
|
|
12
|
+
| `runCommand` | Spawning a process |
|
|
13
|
+
| `platformInfo` | `dart:ffi`, and what the container reports |
|
|
14
|
+
| `parallelHash` | Isolates across cores, timed against the sequential version |
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install && npx render-dart dev
|
|
18
|
+
render workflows start parallelHash --local --input='[["a","b","c","d","e","f","g","h"]]'
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## What to take from parallelHash
|
|
22
|
+
|
|
23
|
+
It runs the same batch sequentially and across isolates, and returns both
|
|
24
|
+
timings — 86 ms against 24 ms for eight jobs on one laptop.
|
|
25
|
+
|
|
26
|
+
That is the honest case for native. It is **not** faster at plain arithmetic:
|
|
27
|
+
measured on Render, V8 matches Dart AOT and beats it on small inputs. What
|
|
28
|
+
native gives you is reach, and the ability to use more than one core.
|
|
29
|
+
|
|
30
|
+
`platformInfo` reports `numberOfProcessors`, which is the host's core count and
|
|
31
|
+
not the CPU share your container actually gets. Measure before sizing an
|
|
32
|
+
isolate pool by it.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Compiled AOT, so this gets the real Dart platform.
|
|
2
|
+
//
|
|
3
|
+
// Everything here is impossible under dart2js: files, subprocesses, FFI and
|
|
4
|
+
// isolates. That is the point of a native task — not speed, but reach.
|
|
5
|
+
import 'dart:ffi';
|
|
6
|
+
import 'dart:io';
|
|
7
|
+
import 'dart:isolate';
|
|
8
|
+
|
|
9
|
+
import '../native_task.dart';
|
|
10
|
+
|
|
11
|
+
/// Reads a file, which dart2js cannot do at all.
|
|
12
|
+
///
|
|
13
|
+
/// `dart:io` compiles under dart2js and then throws `Unsupported operation` on
|
|
14
|
+
/// first use, so `render-dart build` refuses a direct import in task code.
|
|
15
|
+
/// Here it is exactly what we want.
|
|
16
|
+
@nativeTask
|
|
17
|
+
Future<Map<String, Object?>> readFile(String path, {int previewLines = 3}) async {
|
|
18
|
+
final file = File(path);
|
|
19
|
+
if (!file.existsSync()) {
|
|
20
|
+
// Throwing gives the caller the real message, not an exit code.
|
|
21
|
+
throw ArgumentError.value(path, 'path', 'no such file');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
final lines = await file.readAsLines();
|
|
25
|
+
return {
|
|
26
|
+
'path': file.absolute.path,
|
|
27
|
+
'bytes': await file.length(),
|
|
28
|
+
'lines': lines.length,
|
|
29
|
+
'preview': lines.take(previewLines).toList(),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// Runs a command and reports what it did.
|
|
34
|
+
///
|
|
35
|
+
/// A task can also shell out through render-dart's `runProcess` without going
|
|
36
|
+
/// native. Doing it here instead keeps the whole operation on one side of the
|
|
37
|
+
/// boundary, which matters when the surrounding logic needs dart:io anyway.
|
|
38
|
+
@nativeTask
|
|
39
|
+
Future<Map<String, Object?>> runCommand(String command, List<String> args) async {
|
|
40
|
+
final result = await Process.run(command, args);
|
|
41
|
+
return {
|
|
42
|
+
'exitCode': result.exitCode,
|
|
43
|
+
'stdout': '${result.stdout}'.trim(),
|
|
44
|
+
'stderr': '${result.stderr}'.trim(),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// What the platform looks like from inside a native task.
|
|
49
|
+
@nativeTask
|
|
50
|
+
Map<String, Object?> platformInfo() => {
|
|
51
|
+
'os': Platform.operatingSystem,
|
|
52
|
+
'version': Platform.operatingSystemVersion,
|
|
53
|
+
'dart': Platform.version.split(' ').first,
|
|
54
|
+
'abi': Abi.current().toString(),
|
|
55
|
+
// dart:ffi — the size of a native pointer on this machine.
|
|
56
|
+
'pointerSize': sizeOf<IntPtr>(),
|
|
57
|
+
// Reports the host's core count, which is not the same as the CPU share
|
|
58
|
+
// this container actually gets. Measure before sizing anything by it.
|
|
59
|
+
'numberOfProcessors': Platform.numberOfProcessors,
|
|
60
|
+
'processId': pid,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/// Spreads work across isolates — real parallelism, on real cores.
|
|
64
|
+
///
|
|
65
|
+
/// This is the one thing native is genuinely *faster* at. dart2js inherits
|
|
66
|
+
/// JavaScript's single thread, so identical work there runs one job at a time.
|
|
67
|
+
/// For plain arithmetic on a single job, dart2js is as quick or quicker.
|
|
68
|
+
///
|
|
69
|
+
/// Runs the batch both ways so the difference is measured rather than claimed.
|
|
70
|
+
@nativeTask
|
|
71
|
+
Future<Map<String, Object?>> parallelHash(List<String> inputs) async {
|
|
72
|
+
final sequentialStarted = DateTime.now();
|
|
73
|
+
final sequential = [for (final s in inputs) _expensiveHash(s)];
|
|
74
|
+
final sequentialMs = DateTime.now().difference(sequentialStarted).inMilliseconds;
|
|
75
|
+
|
|
76
|
+
final parallelStarted = DateTime.now();
|
|
77
|
+
final parallel = await Future.wait(
|
|
78
|
+
inputs.map((s) => Isolate.run(() => _expensiveHash(s))),
|
|
79
|
+
);
|
|
80
|
+
final parallelMs = DateTime.now().difference(parallelStarted).inMilliseconds;
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
'jobs': inputs.length,
|
|
84
|
+
'sequentialMs': sequentialMs,
|
|
85
|
+
'parallelMs': parallelMs,
|
|
86
|
+
'speedup': parallelMs == 0 ? null : (sequentialMs / parallelMs * 10).round() / 10,
|
|
87
|
+
'agree': sequential.first == parallel.first,
|
|
88
|
+
'hashes': parallel,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// Deliberately slow, so the parallel version has something to show.
|
|
93
|
+
///
|
|
94
|
+
/// The loop count is what makes this a demonstration rather than a
|
|
95
|
+
/// measurement of isolate startup: too cheap, and spawning costs more than the
|
|
96
|
+
/// work itself.
|
|
97
|
+
int _expensiveHash(String input) {
|
|
98
|
+
var hash = 0;
|
|
99
|
+
for (var round = 0; round < 4000000; round++) {
|
|
100
|
+
for (final unit in input.codeUnits) {
|
|
101
|
+
hash = (hash * 31 + unit + round) & 0x7fffffff;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return hash;
|
|
105
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "render-dart-native-example",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "dart:io, dart:ffi and parallel isolates from a Render Workflows task",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "render-dart build",
|
|
8
|
+
"start": "node index.js",
|
|
9
|
+
"dev": "render-dart dev"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@renderinc/sdk": "^0.6.0",
|
|
13
|
+
"render-dart": "^0.7.1"
|
|
14
|
+
},
|
|
15
|
+
"renderDart": {
|
|
16
|
+
"native": [
|
|
17
|
+
"native/tools_impl.dart"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18"
|
|
22
|
+
}
|
|
23
|
+
}
|