rivlet 0.1.0__tar.gz

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.
Files changed (82) hide show
  1. rivlet-0.1.0/Cargo.lock +182 -0
  2. rivlet-0.1.0/Cargo.toml +16 -0
  3. rivlet-0.1.0/LICENSE +21 -0
  4. rivlet-0.1.0/PKG-INFO +97 -0
  5. rivlet-0.1.0/README.md +73 -0
  6. rivlet-0.1.0/crates/action/Cargo.toml +9 -0
  7. rivlet-0.1.0/crates/action/src/executor.rs +268 -0
  8. rivlet-0.1.0/crates/action/src/lib.rs +87 -0
  9. rivlet-0.1.0/crates/action/src/process.rs +90 -0
  10. rivlet-0.1.0/crates/core/Cargo.toml +15 -0
  11. rivlet-0.1.0/crates/core/src/capability.rs +517 -0
  12. rivlet-0.1.0/crates/core/src/declaration.rs +939 -0
  13. rivlet-0.1.0/crates/core/src/execution.rs +677 -0
  14. rivlet-0.1.0/crates/core/src/lib.rs +29 -0
  15. rivlet-0.1.0/crates/core/src/lowering.rs +318 -0
  16. rivlet-0.1.0/crates/core/src/session.rs +843 -0
  17. rivlet-0.1.0/crates/native-graph/Cargo.toml +13 -0
  18. rivlet-0.1.0/crates/native-graph/src/lib.rs +201 -0
  19. rivlet-0.1.0/crates/python/Cargo.toml +21 -0
  20. rivlet-0.1.0/crates/python/src/declaration.rs +812 -0
  21. rivlet-0.1.0/crates/python/src/experiment.rs +110 -0
  22. rivlet-0.1.0/crates/python/src/lib.rs +412 -0
  23. rivlet-0.1.0/crates/python/src/mixed_execution.rs +340 -0
  24. rivlet-0.1.0/crates/python/src/native_graph.rs +64 -0
  25. rivlet-0.1.0/crates/tokio-runtime/Cargo.toml +14 -0
  26. rivlet-0.1.0/crates/tokio-runtime/src/lib.rs +171 -0
  27. rivlet-0.1.0/crates/topology/Cargo.toml +9 -0
  28. rivlet-0.1.0/crates/topology/src/lib.rs +342 -0
  29. rivlet-0.1.0/pyproject.toml +101 -0
  30. rivlet-0.1.0/src/rivlet/__init__.py +386 -0
  31. rivlet-0.1.0/src/rivlet/_native.pyi +142 -0
  32. rivlet-0.1.0/src/rivlet/capability.py +268 -0
  33. rivlet-0.1.0/src/rivlet/compile.py +815 -0
  34. rivlet-0.1.0/src/rivlet/errors.py +2 -0
  35. rivlet-0.1.0/src/rivlet/events.py +33 -0
  36. rivlet-0.1.0/src/rivlet/execution.py +216 -0
  37. rivlet-0.1.0/src/rivlet/execution_output.py +85 -0
  38. rivlet-0.1.0/src/rivlet/model.py +856 -0
  39. rivlet-0.1.0/src/rivlet/observe.py +42 -0
  40. rivlet-0.1.0/src/rivlet/option.py +49 -0
  41. rivlet-0.1.0/src/rivlet/py.typed +1 -0
  42. rivlet-0.1.0/src/rivlet/render/__init__.py +21 -0
  43. rivlet-0.1.0/src/rivlet/render/entry.py +58 -0
  44. rivlet-0.1.0/src/rivlet/render/execution.py +357 -0
  45. rivlet-0.1.0/src/rivlet/render/formatting.py +253 -0
  46. rivlet-0.1.0/src/rivlet/render/mermaid.py +148 -0
  47. rivlet-0.1.0/src/rivlet/render/tree.py +139 -0
  48. rivlet-0.1.0/src/rivlet/results.py +135 -0
  49. rivlet-0.1.0/src/rivlet/runtime/__init__.py +72 -0
  50. rivlet-0.1.0/src/rivlet/runtime/action_log.py +32 -0
  51. rivlet-0.1.0/src/rivlet/runtime/async_executor.py +99 -0
  52. rivlet-0.1.0/src/rivlet/runtime/batch_driver.py +170 -0
  53. rivlet-0.1.0/src/rivlet/runtime/compiled.py +277 -0
  54. rivlet-0.1.0/src/rivlet/runtime/control.py +68 -0
  55. rivlet-0.1.0/src/rivlet/runtime/executor_common.py +107 -0
  56. rivlet-0.1.0/src/rivlet/runtime/parallel_executor.py +74 -0
  57. rivlet-0.1.0/src/rivlet/runtime/python_actions.py +174 -0
  58. rivlet-0.1.0/src/rivlet/runtime/run.py +159 -0
  59. rivlet-0.1.0/src/rivlet/runtime/serial_executor.py +54 -0
  60. rivlet-0.1.0/src/rivlet/runtime/serial_session.py +124 -0
  61. rivlet-0.1.0/src/rivlet/runtime/session_tree.py +240 -0
  62. rivlet-0.1.0/src/rivlet/runtime/state.py +387 -0
  63. rivlet-0.1.0/src/rivlet/selection.py +52 -0
  64. rivlet-0.1.0/src/rivlet/static_condition.py +135 -0
  65. rivlet-0.1.0/src/rivlet/static_conditions.py +106 -0
  66. rivlet-0.1.0/src/rivlet/topology.py +135 -0
  67. rivlet-0.1.0/tests/test_action_invocation.py +110 -0
  68. rivlet-0.1.0/tests/test_capability.py +401 -0
  69. rivlet-0.1.0/tests/test_dynamic_mixed_budget.py +139 -0
  70. rivlet-0.1.0/tests/test_failure_isolation.py +390 -0
  71. rivlet-0.1.0/tests/test_graph_compile.py +995 -0
  72. rivlet-0.1.0/tests/test_graph_execution_label.py +42 -0
  73. rivlet-0.1.0/tests/test_graph_runtime.py +1747 -0
  74. rivlet-0.1.0/tests/test_result_access.py +88 -0
  75. rivlet-0.1.0/tests/test_result_bindings.py +375 -0
  76. rivlet-0.1.0/tests/test_selection.py +33 -0
  77. rivlet-0.1.0/tests/test_shared_state.py +99 -0
  78. rivlet-0.1.0/tests/test_static_conditions.py +229 -0
  79. rivlet-0.1.0/tests/test_step_graph.py +336 -0
  80. rivlet-0.1.0/tests/test_step_log_paths.py +94 -0
  81. rivlet-0.1.0/tests/test_topology.py +51 -0
  82. rivlet-0.1.0/tests/test_workflow_runtime.py +282 -0
@@ -0,0 +1,182 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 4
4
+
5
+ [[package]]
6
+ name = "action"
7
+ version = "0.1.0"
8
+
9
+ [[package]]
10
+ name = "heck"
11
+ version = "0.5.0"
12
+ source = "registry+https://github.com/rust-lang/crates.io-index"
13
+ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
14
+
15
+ [[package]]
16
+ name = "libc"
17
+ version = "0.2.189"
18
+ source = "registry+https://github.com/rust-lang/crates.io-index"
19
+ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
20
+
21
+ [[package]]
22
+ name = "native-graph"
23
+ version = "0.1.0"
24
+ dependencies = [
25
+ "action",
26
+ "rivlet",
27
+ ]
28
+
29
+ [[package]]
30
+ name = "once_cell"
31
+ version = "1.21.4"
32
+ source = "registry+https://github.com/rust-lang/crates.io-index"
33
+ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
34
+
35
+ [[package]]
36
+ name = "pin-project-lite"
37
+ version = "0.2.17"
38
+ source = "registry+https://github.com/rust-lang/crates.io-index"
39
+ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
40
+
41
+ [[package]]
42
+ name = "portable-atomic"
43
+ version = "1.15.0"
44
+ source = "registry+https://github.com/rust-lang/crates.io-index"
45
+ checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
46
+
47
+ [[package]]
48
+ name = "proc-macro2"
49
+ version = "1.0.107"
50
+ source = "registry+https://github.com/rust-lang/crates.io-index"
51
+ checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
52
+ dependencies = [
53
+ "unicode-ident",
54
+ ]
55
+
56
+ [[package]]
57
+ name = "pyo3"
58
+ version = "0.29.2"
59
+ source = "registry+https://github.com/rust-lang/crates.io-index"
60
+ checksum = "4688ddedf473e32662b9b067670129a8afb8c18e351482c70d62ba4a88171e8b"
61
+ dependencies = [
62
+ "libc",
63
+ "once_cell",
64
+ "portable-atomic",
65
+ "pyo3-build-config",
66
+ "pyo3-ffi",
67
+ "pyo3-macros",
68
+ ]
69
+
70
+ [[package]]
71
+ name = "pyo3-build-config"
72
+ version = "0.29.2"
73
+ source = "registry+https://github.com/rust-lang/crates.io-index"
74
+ checksum = "f41027e41b4bd03f6e60f9f417fe24a6341a6bb744edd62b6f709f2a52ea30e9"
75
+ dependencies = [
76
+ "target-lexicon",
77
+ ]
78
+
79
+ [[package]]
80
+ name = "pyo3-ffi"
81
+ version = "0.29.2"
82
+ source = "registry+https://github.com/rust-lang/crates.io-index"
83
+ checksum = "e591a95526fead067432c3b3a33fc74770b87b1e04e73671090d9c2055a2b327"
84
+ dependencies = [
85
+ "libc",
86
+ "pyo3-build-config",
87
+ ]
88
+
89
+ [[package]]
90
+ name = "pyo3-macros"
91
+ version = "0.29.2"
92
+ source = "registry+https://github.com/rust-lang/crates.io-index"
93
+ checksum = "73225868fc1cd84eef2c3c230ddb91273bf1de46aeb8a4248da76d32a0924a1c"
94
+ dependencies = [
95
+ "proc-macro2",
96
+ "pyo3-macros-backend",
97
+ "quote",
98
+ "syn",
99
+ ]
100
+
101
+ [[package]]
102
+ name = "pyo3-macros-backend"
103
+ version = "0.29.2"
104
+ source = "registry+https://github.com/rust-lang/crates.io-index"
105
+ checksum = "571575aa3749fa6216757dd47d2a3e7ef360f329a40f0666a9fbd14889024952"
106
+ dependencies = [
107
+ "heck",
108
+ "proc-macro2",
109
+ "quote",
110
+ "syn",
111
+ ]
112
+
113
+ [[package]]
114
+ name = "python"
115
+ version = "0.1.0"
116
+ dependencies = [
117
+ "action",
118
+ "native-graph",
119
+ "pyo3",
120
+ "rivlet",
121
+ "tokio-runtime",
122
+ ]
123
+
124
+ [[package]]
125
+ name = "quote"
126
+ version = "1.0.47"
127
+ source = "registry+https://github.com/rust-lang/crates.io-index"
128
+ checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
129
+ dependencies = [
130
+ "proc-macro2",
131
+ ]
132
+
133
+ [[package]]
134
+ name = "rivlet"
135
+ version = "0.1.0"
136
+ dependencies = [
137
+ "topology",
138
+ ]
139
+
140
+ [[package]]
141
+ name = "syn"
142
+ version = "2.0.119"
143
+ source = "registry+https://github.com/rust-lang/crates.io-index"
144
+ checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
145
+ dependencies = [
146
+ "proc-macro2",
147
+ "quote",
148
+ "unicode-ident",
149
+ ]
150
+
151
+ [[package]]
152
+ name = "target-lexicon"
153
+ version = "0.13.5"
154
+ source = "registry+https://github.com/rust-lang/crates.io-index"
155
+ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
156
+
157
+ [[package]]
158
+ name = "tokio"
159
+ version = "1.53.1"
160
+ source = "registry+https://github.com/rust-lang/crates.io-index"
161
+ checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
162
+ dependencies = [
163
+ "pin-project-lite",
164
+ ]
165
+
166
+ [[package]]
167
+ name = "tokio-runtime"
168
+ version = "0.1.0"
169
+ dependencies = [
170
+ "rivlet",
171
+ "tokio",
172
+ ]
173
+
174
+ [[package]]
175
+ name = "topology"
176
+ version = "0.1.0"
177
+
178
+ [[package]]
179
+ name = "unicode-ident"
180
+ version = "1.0.24"
181
+ source = "registry+https://github.com/rust-lang/crates.io-index"
182
+ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
@@ -0,0 +1,16 @@
1
+ [workspace]
2
+ members = ["crates/action", "crates/core", "crates/native-graph", "crates/python", "crates/topology", "crates/tokio-runtime"]
3
+ resolver = "3"
4
+
5
+ [workspace.package]
6
+ version = "0.1.0"
7
+ edition = "2024"
8
+ rust-version = "1.85"
9
+ license = "MIT"
10
+
11
+ [workspace.lints.rust]
12
+ unsafe_code = "forbid"
13
+
14
+ [workspace.lints.clippy]
15
+ all = "warn"
16
+ pedantic = "warn"
rivlet-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ririv
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
rivlet-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.4
2
+ Name: rivlet
3
+ Version: 0.1.0
4
+ Classifier: Development Status :: 3 - Alpha
5
+ Classifier: Programming Language :: Python :: 3
6
+ Classifier: Programming Language :: Python :: 3 :: Only
7
+ Classifier: Programming Language :: Python :: 3.13
8
+ Classifier: Programming Language :: Python :: 3.14
9
+ Classifier: Programming Language :: Python :: 3.15
10
+ Classifier: Programming Language :: Rust
11
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
12
+ License-File: LICENSE
13
+ Summary: Explicit, composable workflows with a Python API and a Rust execution core
14
+ Keywords: dag,workflow,task-graph,concurrency,rust
15
+ Author: ririv
16
+ License-Expression: MIT
17
+ Requires-Python: >=3.13
18
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
19
+ Project-URL: Documentation, https://github.com/ririv/rivlet/blob/main/docs/index.md
20
+ Project-URL: Homepage, https://github.com/ririv/rivlet
21
+ Project-URL: Issues, https://github.com/ririv/rivlet/issues
22
+ Project-URL: Repository, https://github.com/ririv/rivlet
23
+
24
+ # Rivlet
25
+
26
+ English | [简体中文](./README.zh-CN.md)
27
+
28
+ Rivlet is an explicit, composable DAG declaration and execution library
29
+ with a Python API and a Rust execution core.
30
+
31
+ It provides steps, subgraphs, conditional dependencies, thread-based and async
32
+ execution, result access, execution state, and graph rendering. Logging, terminal
33
+ integration, and request-level control belong to the host application and are
34
+ connected through explicit interfaces.
35
+
36
+ ## When to use it
37
+
38
+ - Compose build or automation tasks with explicit dependencies.
39
+ - Run independent data-processing or validation tasks concurrently, then join them.
40
+ - Reuse subgraphs inside a larger workflow while keeping execution observable.
41
+
42
+ This is an in-process task graph library, not a distributed workflow service.
43
+ Your application owns the actions and integration; the library handles dependency
44
+ ordering and execution state.
45
+
46
+ ## Architecture
47
+
48
+ On `main`, Rust implements declaration compilation, execution-graph validation,
49
+ and the scheduling state machine. Python retains graph declarations, arbitrary
50
+ callables, result objects, and host integration. Complete Rust-native subgraphs
51
+ can join the same execution graph and concurrency budget.
52
+
53
+ See [Rust architecture](./docs/rust-architecture.md) for implementation boundaries.
54
+
55
+ ## Local development
56
+
57
+ Install the published package with `pip install rivlet`. The Python import name
58
+ is also `rivlet`; there is no `rivetoo_graph` compatibility package.
59
+ See [release policy](./docs/releasing.md) for wheel coverage and publishing.
60
+
61
+ You need Python 3.13 or later, a Rust toolchain, and maturin. With a virtual
62
+ environment activated, install the native extension into that environment:
63
+
64
+ ```bash
65
+ maturin develop
66
+ ```
67
+
68
+ The native extension is required. There is no pure-Python execution fallback.
69
+
70
+ ## Quick example
71
+
72
+ ```python
73
+ from rivlet import GraphExecution, Step, ThreadGraphRunOptions
74
+
75
+ prepare = Step(lambda: print("prepare"), label="prepare")
76
+ validate = Step(lambda: print("validate"), label="validate")
77
+ test = Step(lambda: print("test"), label="test")
78
+ finish = Step(lambda: print("finish"), label="finish")
79
+
80
+ checks = (validate | test).graph(label="checks")
81
+ graph = (prepare >> checks >> finish).graph(label="example")
82
+ GraphExecution(graph=graph).run(ThreadGraphRunOptions(workers=2))
83
+ ```
84
+
85
+ After `prepare`, the `checks` subgraph can run `validate` and `test` concurrently.
86
+ `finish` waits for both to succeed; their print order is not guaranteed.
87
+
88
+ `>>` declares ordering; `|` composes independent branches. Run options determine
89
+ actual concurrency. An ordinary action failure blocks dependent work while
90
+ unrelated branches continue; the run ultimately reports the failures.
91
+
92
+ See the [documentation](./docs/index.md) for the model, composition API, and execution rules.
93
+
94
+ ## License
95
+
96
+ This project is licensed under the [MIT License](./LICENSE).
97
+
rivlet-0.1.0/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # Rivlet
2
+
3
+ English | [简体中文](./README.zh-CN.md)
4
+
5
+ Rivlet is an explicit, composable DAG declaration and execution library
6
+ with a Python API and a Rust execution core.
7
+
8
+ It provides steps, subgraphs, conditional dependencies, thread-based and async
9
+ execution, result access, execution state, and graph rendering. Logging, terminal
10
+ integration, and request-level control belong to the host application and are
11
+ connected through explicit interfaces.
12
+
13
+ ## When to use it
14
+
15
+ - Compose build or automation tasks with explicit dependencies.
16
+ - Run independent data-processing or validation tasks concurrently, then join them.
17
+ - Reuse subgraphs inside a larger workflow while keeping execution observable.
18
+
19
+ This is an in-process task graph library, not a distributed workflow service.
20
+ Your application owns the actions and integration; the library handles dependency
21
+ ordering and execution state.
22
+
23
+ ## Architecture
24
+
25
+ On `main`, Rust implements declaration compilation, execution-graph validation,
26
+ and the scheduling state machine. Python retains graph declarations, arbitrary
27
+ callables, result objects, and host integration. Complete Rust-native subgraphs
28
+ can join the same execution graph and concurrency budget.
29
+
30
+ See [Rust architecture](./docs/rust-architecture.md) for implementation boundaries.
31
+
32
+ ## Local development
33
+
34
+ Install the published package with `pip install rivlet`. The Python import name
35
+ is also `rivlet`; there is no `rivetoo_graph` compatibility package.
36
+ See [release policy](./docs/releasing.md) for wheel coverage and publishing.
37
+
38
+ You need Python 3.13 or later, a Rust toolchain, and maturin. With a virtual
39
+ environment activated, install the native extension into that environment:
40
+
41
+ ```bash
42
+ maturin develop
43
+ ```
44
+
45
+ The native extension is required. There is no pure-Python execution fallback.
46
+
47
+ ## Quick example
48
+
49
+ ```python
50
+ from rivlet import GraphExecution, Step, ThreadGraphRunOptions
51
+
52
+ prepare = Step(lambda: print("prepare"), label="prepare")
53
+ validate = Step(lambda: print("validate"), label="validate")
54
+ test = Step(lambda: print("test"), label="test")
55
+ finish = Step(lambda: print("finish"), label="finish")
56
+
57
+ checks = (validate | test).graph(label="checks")
58
+ graph = (prepare >> checks >> finish).graph(label="example")
59
+ GraphExecution(graph=graph).run(ThreadGraphRunOptions(workers=2))
60
+ ```
61
+
62
+ After `prepare`, the `checks` subgraph can run `validate` and `test` concurrently.
63
+ `finish` waits for both to succeed; their print order is not guaranteed.
64
+
65
+ `>>` declares ordering; `|` composes independent branches. Run options determine
66
+ actual concurrency. An ordinary action failure blocks dependent work while
67
+ unrelated branches continue; the run ultimately reports the failures.
68
+
69
+ See the [documentation](./docs/index.md) for the model, composition API, and execution rules.
70
+
71
+ ## License
72
+
73
+ This project is licensed under the [MIT License](./LICENSE).
@@ -0,0 +1,9 @@
1
+ [package]
2
+ name = "action"
3
+ version.workspace = true
4
+ edition.workspace = true
5
+ rust-version.workspace = true
6
+ license.workspace = true
7
+
8
+ [lints]
9
+ workspace = true
@@ -0,0 +1,268 @@
1
+ use std::collections::VecDeque;
2
+ use std::panic::{AssertUnwindSafe, catch_unwind};
3
+ use std::sync::{Arc, Condvar, Mutex, mpsc};
4
+ use std::thread::{self, JoinHandle};
5
+
6
+ use crate::{NativeActionContext, NativeActionError, NativeActionHandle};
7
+
8
+ struct NativeActionJob {
9
+ node: usize,
10
+ action: NativeActionHandle,
11
+ context: NativeActionContext,
12
+ }
13
+
14
+ /// One native action completion retained until its graph session consumes it.
15
+ #[derive(Debug)]
16
+ pub struct NativeActionCompletion {
17
+ node: usize,
18
+ result: Result<(), NativeActionError>,
19
+ }
20
+
21
+ impl NativeActionCompletion {
22
+ #[must_use]
23
+ pub const fn node(&self) -> usize {
24
+ self.node
25
+ }
26
+
27
+ /// Returns the concrete action result.
28
+ ///
29
+ /// # Errors
30
+ /// Returns the error produced by the native action.
31
+ pub fn into_result(self) -> Result<(), NativeActionError> {
32
+ self.result
33
+ }
34
+ }
35
+
36
+ #[derive(Default)]
37
+ struct CompletionState {
38
+ values: VecDeque<NativeActionCompletion>,
39
+ revision: u64,
40
+ }
41
+
42
+ #[derive(Default)]
43
+ struct CompletionQueue {
44
+ state: Mutex<CompletionState>,
45
+ ready: Condvar,
46
+ }
47
+
48
+ enum WaitState {
49
+ Ready,
50
+ Pending(u64),
51
+ }
52
+
53
+ /// A wait bound to the completion queue and revision observed at preparation.
54
+ pub struct NativeCompletionWait {
55
+ queue: Arc<CompletionQueue>,
56
+ state: WaitState,
57
+ }
58
+
59
+ impl NativeCompletionWait {
60
+ pub fn wait(self) {
61
+ match self.state {
62
+ WaitState::Ready => (),
63
+ WaitState::Pending(revision) => {
64
+ let mut state = self
65
+ .queue
66
+ .state
67
+ .lock()
68
+ .unwrap_or_else(std::sync::PoisonError::into_inner);
69
+ // 即使另一个 driver 已取走完成记录,序号变化也保留这次通知。
70
+ while state.values.is_empty() && state.revision == revision {
71
+ state = self
72
+ .queue
73
+ .ready
74
+ .wait(state)
75
+ .unwrap_or_else(std::sync::PoisonError::into_inner);
76
+ }
77
+ }
78
+ }
79
+ }
80
+ }
81
+
82
+ struct ExecutorInner {
83
+ jobs: mpsc::Sender<Option<NativeActionJob>>,
84
+ completions: Arc<CompletionQueue>,
85
+ workers: Vec<JoinHandle<()>>,
86
+ }
87
+
88
+ impl Drop for ExecutorInner {
89
+ fn drop(&mut self) {
90
+ for _ in &self.workers {
91
+ let _ = self.jobs.send(None);
92
+ }
93
+ for worker in self.workers.drain(..) {
94
+ let _ = worker.join();
95
+ }
96
+ }
97
+ }
98
+
99
+ /// A bounded Rust worker set used only for Rust-owned actions.
100
+ #[derive(Clone)]
101
+ pub struct NativeActionExecutor {
102
+ inner: Arc<ExecutorInner>,
103
+ }
104
+
105
+ impl NativeActionExecutor {
106
+ /// Creates a native worker set with an explicit positive capacity.
107
+ ///
108
+ /// # Errors
109
+ /// Returns an error when capacity is zero or an operating-system thread
110
+ /// cannot be created.
111
+ pub fn new(workers: usize) -> Result<Self, NativeActionError> {
112
+ if workers == 0 {
113
+ return Err(NativeActionError::new(
114
+ "native action workers must be greater than zero",
115
+ ));
116
+ }
117
+ let (jobs, receiver) = mpsc::channel::<Option<NativeActionJob>>();
118
+ let receiver = Arc::new(Mutex::new(receiver));
119
+ let completions = Arc::new(CompletionQueue::default());
120
+ let mut worker_handles = Vec::with_capacity(workers);
121
+ for index in 0..workers {
122
+ let receiver = Arc::clone(&receiver);
123
+ let completions = Arc::clone(&completions);
124
+ let worker = thread::Builder::new()
125
+ .name(format!("NativeGraphWorker-{index}"))
126
+ .spawn(move || worker_loop(&receiver, &completions))
127
+ .map_err(|error| {
128
+ NativeActionError::new(format!("failed to create native graph worker: {error}"))
129
+ })?;
130
+ worker_handles.push(worker);
131
+ }
132
+ Ok(Self {
133
+ inner: Arc::new(ExecutorInner {
134
+ jobs,
135
+ completions,
136
+ workers: worker_handles,
137
+ }),
138
+ })
139
+ }
140
+
141
+ /// Submits one action to the native worker set.
142
+ ///
143
+ /// # Errors
144
+ /// Returns an error when the executor has already stopped accepting work.
145
+ pub fn submit(
146
+ &self,
147
+ node: usize,
148
+ action: NativeActionHandle,
149
+ context: NativeActionContext,
150
+ ) -> Result<(), NativeActionError> {
151
+ self.inner
152
+ .jobs
153
+ .send(Some(NativeActionJob {
154
+ node,
155
+ action,
156
+ context,
157
+ }))
158
+ .map_err(|_| NativeActionError::new("native action executor is closed"))
159
+ }
160
+
161
+ /// Blocks until at least one completion can be drained.
162
+ pub fn wait_for_completion(&self) {
163
+ self.prepare_wait().wait();
164
+ }
165
+
166
+ #[must_use]
167
+ pub fn prepare_wait(&self) -> NativeCompletionWait {
168
+ let state = self
169
+ .inner
170
+ .completions
171
+ .state
172
+ .lock()
173
+ .unwrap_or_else(std::sync::PoisonError::into_inner);
174
+ NativeCompletionWait {
175
+ queue: Arc::clone(&self.inner.completions),
176
+ state: if state.values.is_empty() {
177
+ WaitState::Pending(state.revision)
178
+ } else {
179
+ WaitState::Ready
180
+ },
181
+ }
182
+ }
183
+
184
+ #[must_use]
185
+ pub fn drain_completions(&self) -> Vec<NativeActionCompletion> {
186
+ self.inner
187
+ .completions
188
+ .state
189
+ .lock()
190
+ .unwrap_or_else(std::sync::PoisonError::into_inner)
191
+ .values
192
+ .drain(..)
193
+ .collect()
194
+ }
195
+ }
196
+
197
+ fn worker_loop(
198
+ jobs: &Mutex<mpsc::Receiver<Option<NativeActionJob>>>,
199
+ completions: &CompletionQueue,
200
+ ) {
201
+ loop {
202
+ let job = jobs
203
+ .lock()
204
+ .unwrap_or_else(std::sync::PoisonError::into_inner)
205
+ .recv();
206
+ let Ok(Some(job)) = job else {
207
+ return;
208
+ };
209
+ let result = catch_unwind(AssertUnwindSafe(|| job.action.execute(&job.context)))
210
+ .unwrap_or_else(|_| Err(NativeActionError::new("native action panicked")));
211
+ let mut state = completions
212
+ .state
213
+ .lock()
214
+ .unwrap_or_else(std::sync::PoisonError::into_inner);
215
+ state.values.push_back(NativeActionCompletion {
216
+ node: job.node,
217
+ result,
218
+ });
219
+ state.revision = state
220
+ .revision
221
+ .checked_add(1)
222
+ .expect("native completion revision overflow");
223
+ drop(state);
224
+ completions.ready.notify_all();
225
+ }
226
+ }
227
+
228
+ #[cfg(test)]
229
+ mod tests {
230
+ use super::*;
231
+ use crate::NativeAction;
232
+ use std::sync::mpsc;
233
+ use std::time::Duration;
234
+
235
+ struct Noop;
236
+
237
+ impl NativeAction for Noop {
238
+ fn execute(&self, _context: &NativeActionContext) -> Result<(), NativeActionError> {
239
+ Ok(())
240
+ }
241
+ }
242
+
243
+ #[test]
244
+ fn prepared_wait_survives_completion_drain() {
245
+ let executor = NativeActionExecutor::new(1).unwrap();
246
+ let pending = executor.prepare_wait();
247
+ executor
248
+ .submit(
249
+ 0,
250
+ NativeActionHandle::new(Noop),
251
+ NativeActionContext::default(),
252
+ )
253
+ .unwrap();
254
+ executor.wait_for_completion();
255
+ let ready = executor.prepare_wait();
256
+ assert_eq!(executor.drain_completions().len(), 1);
257
+ let (sent, received) = mpsc::channel();
258
+ let waiter = thread::spawn(move || {
259
+ pending.wait();
260
+ ready.wait();
261
+ sent.send(()).unwrap();
262
+ });
263
+ received
264
+ .recv_timeout(Duration::from_secs(2))
265
+ .expect("completion notification was lost after drain");
266
+ waiter.join().unwrap();
267
+ }
268
+ }