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.
- rivlet-0.1.0/Cargo.lock +182 -0
- rivlet-0.1.0/Cargo.toml +16 -0
- rivlet-0.1.0/LICENSE +21 -0
- rivlet-0.1.0/PKG-INFO +97 -0
- rivlet-0.1.0/README.md +73 -0
- rivlet-0.1.0/crates/action/Cargo.toml +9 -0
- rivlet-0.1.0/crates/action/src/executor.rs +268 -0
- rivlet-0.1.0/crates/action/src/lib.rs +87 -0
- rivlet-0.1.0/crates/action/src/process.rs +90 -0
- rivlet-0.1.0/crates/core/Cargo.toml +15 -0
- rivlet-0.1.0/crates/core/src/capability.rs +517 -0
- rivlet-0.1.0/crates/core/src/declaration.rs +939 -0
- rivlet-0.1.0/crates/core/src/execution.rs +677 -0
- rivlet-0.1.0/crates/core/src/lib.rs +29 -0
- rivlet-0.1.0/crates/core/src/lowering.rs +318 -0
- rivlet-0.1.0/crates/core/src/session.rs +843 -0
- rivlet-0.1.0/crates/native-graph/Cargo.toml +13 -0
- rivlet-0.1.0/crates/native-graph/src/lib.rs +201 -0
- rivlet-0.1.0/crates/python/Cargo.toml +21 -0
- rivlet-0.1.0/crates/python/src/declaration.rs +812 -0
- rivlet-0.1.0/crates/python/src/experiment.rs +110 -0
- rivlet-0.1.0/crates/python/src/lib.rs +412 -0
- rivlet-0.1.0/crates/python/src/mixed_execution.rs +340 -0
- rivlet-0.1.0/crates/python/src/native_graph.rs +64 -0
- rivlet-0.1.0/crates/tokio-runtime/Cargo.toml +14 -0
- rivlet-0.1.0/crates/tokio-runtime/src/lib.rs +171 -0
- rivlet-0.1.0/crates/topology/Cargo.toml +9 -0
- rivlet-0.1.0/crates/topology/src/lib.rs +342 -0
- rivlet-0.1.0/pyproject.toml +101 -0
- rivlet-0.1.0/src/rivlet/__init__.py +386 -0
- rivlet-0.1.0/src/rivlet/_native.pyi +142 -0
- rivlet-0.1.0/src/rivlet/capability.py +268 -0
- rivlet-0.1.0/src/rivlet/compile.py +815 -0
- rivlet-0.1.0/src/rivlet/errors.py +2 -0
- rivlet-0.1.0/src/rivlet/events.py +33 -0
- rivlet-0.1.0/src/rivlet/execution.py +216 -0
- rivlet-0.1.0/src/rivlet/execution_output.py +85 -0
- rivlet-0.1.0/src/rivlet/model.py +856 -0
- rivlet-0.1.0/src/rivlet/observe.py +42 -0
- rivlet-0.1.0/src/rivlet/option.py +49 -0
- rivlet-0.1.0/src/rivlet/py.typed +1 -0
- rivlet-0.1.0/src/rivlet/render/__init__.py +21 -0
- rivlet-0.1.0/src/rivlet/render/entry.py +58 -0
- rivlet-0.1.0/src/rivlet/render/execution.py +357 -0
- rivlet-0.1.0/src/rivlet/render/formatting.py +253 -0
- rivlet-0.1.0/src/rivlet/render/mermaid.py +148 -0
- rivlet-0.1.0/src/rivlet/render/tree.py +139 -0
- rivlet-0.1.0/src/rivlet/results.py +135 -0
- rivlet-0.1.0/src/rivlet/runtime/__init__.py +72 -0
- rivlet-0.1.0/src/rivlet/runtime/action_log.py +32 -0
- rivlet-0.1.0/src/rivlet/runtime/async_executor.py +99 -0
- rivlet-0.1.0/src/rivlet/runtime/batch_driver.py +170 -0
- rivlet-0.1.0/src/rivlet/runtime/compiled.py +277 -0
- rivlet-0.1.0/src/rivlet/runtime/control.py +68 -0
- rivlet-0.1.0/src/rivlet/runtime/executor_common.py +107 -0
- rivlet-0.1.0/src/rivlet/runtime/parallel_executor.py +74 -0
- rivlet-0.1.0/src/rivlet/runtime/python_actions.py +174 -0
- rivlet-0.1.0/src/rivlet/runtime/run.py +159 -0
- rivlet-0.1.0/src/rivlet/runtime/serial_executor.py +54 -0
- rivlet-0.1.0/src/rivlet/runtime/serial_session.py +124 -0
- rivlet-0.1.0/src/rivlet/runtime/session_tree.py +240 -0
- rivlet-0.1.0/src/rivlet/runtime/state.py +387 -0
- rivlet-0.1.0/src/rivlet/selection.py +52 -0
- rivlet-0.1.0/src/rivlet/static_condition.py +135 -0
- rivlet-0.1.0/src/rivlet/static_conditions.py +106 -0
- rivlet-0.1.0/src/rivlet/topology.py +135 -0
- rivlet-0.1.0/tests/test_action_invocation.py +110 -0
- rivlet-0.1.0/tests/test_capability.py +401 -0
- rivlet-0.1.0/tests/test_dynamic_mixed_budget.py +139 -0
- rivlet-0.1.0/tests/test_failure_isolation.py +390 -0
- rivlet-0.1.0/tests/test_graph_compile.py +995 -0
- rivlet-0.1.0/tests/test_graph_execution_label.py +42 -0
- rivlet-0.1.0/tests/test_graph_runtime.py +1747 -0
- rivlet-0.1.0/tests/test_result_access.py +88 -0
- rivlet-0.1.0/tests/test_result_bindings.py +375 -0
- rivlet-0.1.0/tests/test_selection.py +33 -0
- rivlet-0.1.0/tests/test_shared_state.py +99 -0
- rivlet-0.1.0/tests/test_static_conditions.py +229 -0
- rivlet-0.1.0/tests/test_step_graph.py +336 -0
- rivlet-0.1.0/tests/test_step_log_paths.py +94 -0
- rivlet-0.1.0/tests/test_topology.py +51 -0
- rivlet-0.1.0/tests/test_workflow_runtime.py +282 -0
rivlet-0.1.0/Cargo.lock
ADDED
|
@@ -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"
|
rivlet-0.1.0/Cargo.toml
ADDED
|
@@ -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,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
|
+
}
|