fluxera 0.0.1__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.
- fluxera-0.0.1/LICENSE +21 -0
- fluxera-0.0.1/PKG-INFO +199 -0
- fluxera-0.0.1/README.md +176 -0
- fluxera-0.0.1/fluxera/__init__.py +38 -0
- fluxera-0.0.1/fluxera/__main__.py +7 -0
- fluxera-0.0.1/fluxera/actor.py +176 -0
- fluxera-0.0.1/fluxera/admin.py +90 -0
- fluxera-0.0.1/fluxera/broker.py +140 -0
- fluxera-0.0.1/fluxera/brokers/__init__.py +6 -0
- fluxera-0.0.1/fluxera/brokers/redis.py +620 -0
- fluxera-0.0.1/fluxera/brokers/redis_lua/enqueue_or_deduplicate.lua +111 -0
- fluxera-0.0.1/fluxera/brokers/redis_lua/idem_begin.lua +71 -0
- fluxera-0.0.1/fluxera/brokers/redis_lua/idem_commit.lua +35 -0
- fluxera-0.0.1/fluxera/brokers/redis_lua/idem_heartbeat.lua +30 -0
- fluxera-0.0.1/fluxera/brokers/redis_lua/idem_release.lua +24 -0
- fluxera-0.0.1/fluxera/brokers/redis_lua/promote_due.lua +19 -0
- fluxera-0.0.1/fluxera/brokers/redis_lua/remove_dedupe_key_if_owner.lua +11 -0
- fluxera-0.0.1/fluxera/brokers/redis_scripts.py +291 -0
- fluxera-0.0.1/fluxera/brokers/stub.py +179 -0
- fluxera-0.0.1/fluxera/cli.py +153 -0
- fluxera-0.0.1/fluxera/encoder.py +104 -0
- fluxera-0.0.1/fluxera/errors.py +29 -0
- fluxera-0.0.1/fluxera/message.py +51 -0
- fluxera-0.0.1/fluxera/runtime/__init__.py +6 -0
- fluxera-0.0.1/fluxera/runtime/worker.py +876 -0
- fluxera-0.0.1/fluxera.egg-info/PKG-INFO +199 -0
- fluxera-0.0.1/fluxera.egg-info/SOURCES.txt +35 -0
- fluxera-0.0.1/fluxera.egg-info/dependency_links.txt +1 -0
- fluxera-0.0.1/fluxera.egg-info/entry_points.txt +2 -0
- fluxera-0.0.1/fluxera.egg-info/requires.txt +4 -0
- fluxera-0.0.1/fluxera.egg-info/top_level.txt +1 -0
- fluxera-0.0.1/pyproject.toml +45 -0
- fluxera-0.0.1/setup.cfg +4 -0
- fluxera-0.0.1/tests/test_actor.py +48 -0
- fluxera-0.0.1/tests/test_encoder.py +32 -0
- fluxera-0.0.1/tests/test_redis_broker.py +660 -0
- fluxera-0.0.1/tests/test_stub_broker.py +383 -0
fluxera-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 JaeWang Lee
|
|
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.
|
fluxera-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fluxera
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Async-native message processing inspired by Dramatiq.
|
|
5
|
+
Project-URL: Homepage, https://github.com/JaeWangL/fluxera
|
|
6
|
+
Project-URL: Repository, https://github.com/JaeWangL/fluxera
|
|
7
|
+
Keywords: asyncio,dramatiq,message-queue,redis,worker
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Framework :: AsyncIO
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: redis<8,>=5
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# Fluxera
|
|
25
|
+
|
|
26
|
+
Fluxera is an async-native Python task runtime inspired by Dramatiq.
|
|
27
|
+
|
|
28
|
+
It is built for workloads where a worker should keep a lot of I/O in flight without buying concurrency through large worker-thread pools, while still handling synchronous and CPU-bound work through dedicated execution lanes.
|
|
29
|
+
|
|
30
|
+
## Why Fluxera
|
|
31
|
+
|
|
32
|
+
- `async def` actors run as real `asyncio` tasks on the worker event loop.
|
|
33
|
+
- `def` actors still work through a bounded thread lane.
|
|
34
|
+
- CPU-heavy actors can be isolated in a separate process lane.
|
|
35
|
+
- Redis Streams is supported as an at-least-once transport with lease renewal, stale reclaim, deduplication, and idempotency primitives.
|
|
36
|
+
- Rolling deploys can hand off unstarted backlog between old and new worker revisions without rotating namespaces.
|
|
37
|
+
|
|
38
|
+
## Status
|
|
39
|
+
|
|
40
|
+
`0.0.1` is the first public alpha.
|
|
41
|
+
|
|
42
|
+
The runtime, Redis transport v2, revision management, benchmark harnesses, and release packaging are in place, but APIs may still change as the project hardens.
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install fluxera
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
For local Redis development:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
docker compose up -d
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Quick Start
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
import asyncio
|
|
60
|
+
|
|
61
|
+
import fluxera
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
broker = fluxera.RedisBroker(
|
|
65
|
+
"redis://127.0.0.1:6379/15",
|
|
66
|
+
namespace="hello-fluxera",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@fluxera.actor(broker=broker, queue_name="default")
|
|
71
|
+
async def fetch_user(user_id: str) -> None:
|
|
72
|
+
await asyncio.sleep(0.1)
|
|
73
|
+
print("fetched", user_id)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async def main() -> None:
|
|
77
|
+
async with fluxera.Worker(
|
|
78
|
+
broker,
|
|
79
|
+
concurrency=128,
|
|
80
|
+
thread_concurrency=16,
|
|
81
|
+
process_concurrency=4,
|
|
82
|
+
):
|
|
83
|
+
await fetch_user.send("user-123")
|
|
84
|
+
await broker.join(fetch_user.queue_name)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
asyncio.run(main())
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Execution Model
|
|
91
|
+
|
|
92
|
+
Fluxera has three execution lanes:
|
|
93
|
+
|
|
94
|
+
- `async`: default for `async def` actors
|
|
95
|
+
- `thread`: default for regular `def` actors
|
|
96
|
+
- `process`: opt-in for CPU-heavy actors
|
|
97
|
+
|
|
98
|
+
The process lane defaults to `spawn` for safe multithreaded startup. You can still override it through `Worker(process_start_method=...)` or `FLUXERA_PROCESS_START_METHOD` when needed.
|
|
99
|
+
|
|
100
|
+
Example CPU actor:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
import fluxera
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
broker = fluxera.RedisBroker("redis://127.0.0.1:6379/15", namespace="cpu-example")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def score_document(text: str) -> int:
|
|
110
|
+
return sum(ord(ch) for ch in text)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
score_document_actor = fluxera.actor(
|
|
114
|
+
broker=broker,
|
|
115
|
+
actor_name="score_document",
|
|
116
|
+
queue_name="cpu",
|
|
117
|
+
execution="process",
|
|
118
|
+
)(score_document)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Serving Revision Admin
|
|
122
|
+
|
|
123
|
+
Fluxera keeps `namespace` as the broker identity boundary and uses `worker_revision` and `serving_revision` for rollout control.
|
|
124
|
+
|
|
125
|
+
Read the current serving revision:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
fluxera revision get \
|
|
129
|
+
--redis-url redis://127.0.0.1:6379/15 \
|
|
130
|
+
--namespace hello-fluxera \
|
|
131
|
+
--queue default
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Promote a new serving revision with a CAS guard:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
fluxera revision promote \
|
|
138
|
+
--redis-url redis://127.0.0.1:6379/15 \
|
|
139
|
+
--namespace hello-fluxera \
|
|
140
|
+
--queue default \
|
|
141
|
+
--revision 20260329153000 \
|
|
142
|
+
--expected-revision 20260329140000
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Use `--format json` when the command is called by deployment automation.
|
|
146
|
+
|
|
147
|
+
## Delivery Semantics
|
|
148
|
+
|
|
149
|
+
- Transport delivery is at-least-once.
|
|
150
|
+
- Deduplication is an enqueue-time admission policy, not exactly-once execution.
|
|
151
|
+
- Effectively-once side effects require idempotency keys or application-level dedupe.
|
|
152
|
+
- Redis workers renew leases for long-running tasks and reclaim stale pending deliveries.
|
|
153
|
+
|
|
154
|
+
## Benchmark Snapshot
|
|
155
|
+
|
|
156
|
+
Latest local measurements were taken on `2026-03-29` on `macOS 26.3.1`, `Python 3.12.10`, `Apple M5 Pro (15 cores)`.
|
|
157
|
+
|
|
158
|
+
Benchmark label legend:
|
|
159
|
+
|
|
160
|
+
- `c=`: Fluxera worker concurrency setting used by the benchmark runner
|
|
161
|
+
- `t=`: Dramatiq `worker_threads`
|
|
162
|
+
|
|
163
|
+
Headline results against the current local Dramatiq checkout:
|
|
164
|
+
|
|
165
|
+
| Scenario | Fluxera | Dramatiq | Takeaway |
|
|
166
|
+
| --- | --- | --- | --- |
|
|
167
|
+
| production-shaped async fanout | `0.258s` | `0.385s (t=8)` / `0.319s (t=32)` | Fluxera is faster with `2` threads instead of `12` or `36` |
|
|
168
|
+
| single-worker CPU-bound | `1.270s` | `3.893s (t=8)` / `3.704s (t=32)` | process lane still gives Fluxera a large single-worker win |
|
|
169
|
+
| mixed long I/O + short work | `short_drain=0.040s` | `6.038s (t=8)` / `0.098s (t=32)` | long I/O does not starve short work |
|
|
170
|
+
| Redis mixed long/short | `wall=1.527s`, `short_drain=0.081s` | `3.176s`, `1.673s (t=8)` / `1.713s`, `0.089s (t=32)` | transport advantage remains on real Redis |
|
|
171
|
+
|
|
172
|
+
See [BENCHMARK.md](docs/BENCHMARK.md) for the full methodology and numbers.
|
|
173
|
+
|
|
174
|
+
One nuance matters: with the safer default `spawn` process policy, cluster-scale CPU throughput is no longer universally faster than Dramatiq. Fluxera's strongest advantage is still async-heavy and mixed I/O workloads.
|
|
175
|
+
|
|
176
|
+
## Verification
|
|
177
|
+
|
|
178
|
+
The current release candidate was checked with:
|
|
179
|
+
|
|
180
|
+
- `python3 -m unittest discover -s tests -v`
|
|
181
|
+
- `python3 benchmarks/production_compare.py --profile smoke`
|
|
182
|
+
- `python3 benchmarks/redis_transport_compare.py --repeat 3 --long-io-secs 1.5`
|
|
183
|
+
- `/tmp/fluxera-release-venv/bin/python -m build --sdist --wheel`
|
|
184
|
+
- `/tmp/fluxera-release-venv/bin/python -m twine check dist/*`
|
|
185
|
+
|
|
186
|
+
## Documentation
|
|
187
|
+
|
|
188
|
+
- [Getting Started](docs/GETTING_STARTED.md)
|
|
189
|
+
- [Benchmark Results](docs/BENCHMARK.md)
|
|
190
|
+
- [Revision Management](docs/REVISION_MANAGEMENT.md)
|
|
191
|
+
- [System Design](docs/SYSTEM_DESIGN.md)
|
|
192
|
+
- [Deduplication and Idempotency](docs/DEDUP_IDEMPOTENCY.md)
|
|
193
|
+
- [Redis Lua Contract](docs/REDIS_LUA_CONTRACT.md)
|
|
194
|
+
|
|
195
|
+
## Current Limits
|
|
196
|
+
|
|
197
|
+
- public APIs may still change during the alpha period
|
|
198
|
+
- result backends are not implemented yet
|
|
199
|
+
- message registry garbage collection is still intentionally simple
|
fluxera-0.0.1/README.md
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Fluxera
|
|
2
|
+
|
|
3
|
+
Fluxera is an async-native Python task runtime inspired by Dramatiq.
|
|
4
|
+
|
|
5
|
+
It is built for workloads where a worker should keep a lot of I/O in flight without buying concurrency through large worker-thread pools, while still handling synchronous and CPU-bound work through dedicated execution lanes.
|
|
6
|
+
|
|
7
|
+
## Why Fluxera
|
|
8
|
+
|
|
9
|
+
- `async def` actors run as real `asyncio` tasks on the worker event loop.
|
|
10
|
+
- `def` actors still work through a bounded thread lane.
|
|
11
|
+
- CPU-heavy actors can be isolated in a separate process lane.
|
|
12
|
+
- Redis Streams is supported as an at-least-once transport with lease renewal, stale reclaim, deduplication, and idempotency primitives.
|
|
13
|
+
- Rolling deploys can hand off unstarted backlog between old and new worker revisions without rotating namespaces.
|
|
14
|
+
|
|
15
|
+
## Status
|
|
16
|
+
|
|
17
|
+
`0.0.1` is the first public alpha.
|
|
18
|
+
|
|
19
|
+
The runtime, Redis transport v2, revision management, benchmark harnesses, and release packaging are in place, but APIs may still change as the project hardens.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install fluxera
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
For local Redis development:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
docker compose up -d
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import asyncio
|
|
37
|
+
|
|
38
|
+
import fluxera
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
broker = fluxera.RedisBroker(
|
|
42
|
+
"redis://127.0.0.1:6379/15",
|
|
43
|
+
namespace="hello-fluxera",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@fluxera.actor(broker=broker, queue_name="default")
|
|
48
|
+
async def fetch_user(user_id: str) -> None:
|
|
49
|
+
await asyncio.sleep(0.1)
|
|
50
|
+
print("fetched", user_id)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
async def main() -> None:
|
|
54
|
+
async with fluxera.Worker(
|
|
55
|
+
broker,
|
|
56
|
+
concurrency=128,
|
|
57
|
+
thread_concurrency=16,
|
|
58
|
+
process_concurrency=4,
|
|
59
|
+
):
|
|
60
|
+
await fetch_user.send("user-123")
|
|
61
|
+
await broker.join(fetch_user.queue_name)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
asyncio.run(main())
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Execution Model
|
|
68
|
+
|
|
69
|
+
Fluxera has three execution lanes:
|
|
70
|
+
|
|
71
|
+
- `async`: default for `async def` actors
|
|
72
|
+
- `thread`: default for regular `def` actors
|
|
73
|
+
- `process`: opt-in for CPU-heavy actors
|
|
74
|
+
|
|
75
|
+
The process lane defaults to `spawn` for safe multithreaded startup. You can still override it through `Worker(process_start_method=...)` or `FLUXERA_PROCESS_START_METHOD` when needed.
|
|
76
|
+
|
|
77
|
+
Example CPU actor:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
import fluxera
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
broker = fluxera.RedisBroker("redis://127.0.0.1:6379/15", namespace="cpu-example")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def score_document(text: str) -> int:
|
|
87
|
+
return sum(ord(ch) for ch in text)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
score_document_actor = fluxera.actor(
|
|
91
|
+
broker=broker,
|
|
92
|
+
actor_name="score_document",
|
|
93
|
+
queue_name="cpu",
|
|
94
|
+
execution="process",
|
|
95
|
+
)(score_document)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Serving Revision Admin
|
|
99
|
+
|
|
100
|
+
Fluxera keeps `namespace` as the broker identity boundary and uses `worker_revision` and `serving_revision` for rollout control.
|
|
101
|
+
|
|
102
|
+
Read the current serving revision:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
fluxera revision get \
|
|
106
|
+
--redis-url redis://127.0.0.1:6379/15 \
|
|
107
|
+
--namespace hello-fluxera \
|
|
108
|
+
--queue default
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Promote a new serving revision with a CAS guard:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
fluxera revision promote \
|
|
115
|
+
--redis-url redis://127.0.0.1:6379/15 \
|
|
116
|
+
--namespace hello-fluxera \
|
|
117
|
+
--queue default \
|
|
118
|
+
--revision 20260329153000 \
|
|
119
|
+
--expected-revision 20260329140000
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Use `--format json` when the command is called by deployment automation.
|
|
123
|
+
|
|
124
|
+
## Delivery Semantics
|
|
125
|
+
|
|
126
|
+
- Transport delivery is at-least-once.
|
|
127
|
+
- Deduplication is an enqueue-time admission policy, not exactly-once execution.
|
|
128
|
+
- Effectively-once side effects require idempotency keys or application-level dedupe.
|
|
129
|
+
- Redis workers renew leases for long-running tasks and reclaim stale pending deliveries.
|
|
130
|
+
|
|
131
|
+
## Benchmark Snapshot
|
|
132
|
+
|
|
133
|
+
Latest local measurements were taken on `2026-03-29` on `macOS 26.3.1`, `Python 3.12.10`, `Apple M5 Pro (15 cores)`.
|
|
134
|
+
|
|
135
|
+
Benchmark label legend:
|
|
136
|
+
|
|
137
|
+
- `c=`: Fluxera worker concurrency setting used by the benchmark runner
|
|
138
|
+
- `t=`: Dramatiq `worker_threads`
|
|
139
|
+
|
|
140
|
+
Headline results against the current local Dramatiq checkout:
|
|
141
|
+
|
|
142
|
+
| Scenario | Fluxera | Dramatiq | Takeaway |
|
|
143
|
+
| --- | --- | --- | --- |
|
|
144
|
+
| production-shaped async fanout | `0.258s` | `0.385s (t=8)` / `0.319s (t=32)` | Fluxera is faster with `2` threads instead of `12` or `36` |
|
|
145
|
+
| single-worker CPU-bound | `1.270s` | `3.893s (t=8)` / `3.704s (t=32)` | process lane still gives Fluxera a large single-worker win |
|
|
146
|
+
| mixed long I/O + short work | `short_drain=0.040s` | `6.038s (t=8)` / `0.098s (t=32)` | long I/O does not starve short work |
|
|
147
|
+
| Redis mixed long/short | `wall=1.527s`, `short_drain=0.081s` | `3.176s`, `1.673s (t=8)` / `1.713s`, `0.089s (t=32)` | transport advantage remains on real Redis |
|
|
148
|
+
|
|
149
|
+
See [BENCHMARK.md](docs/BENCHMARK.md) for the full methodology and numbers.
|
|
150
|
+
|
|
151
|
+
One nuance matters: with the safer default `spawn` process policy, cluster-scale CPU throughput is no longer universally faster than Dramatiq. Fluxera's strongest advantage is still async-heavy and mixed I/O workloads.
|
|
152
|
+
|
|
153
|
+
## Verification
|
|
154
|
+
|
|
155
|
+
The current release candidate was checked with:
|
|
156
|
+
|
|
157
|
+
- `python3 -m unittest discover -s tests -v`
|
|
158
|
+
- `python3 benchmarks/production_compare.py --profile smoke`
|
|
159
|
+
- `python3 benchmarks/redis_transport_compare.py --repeat 3 --long-io-secs 1.5`
|
|
160
|
+
- `/tmp/fluxera-release-venv/bin/python -m build --sdist --wheel`
|
|
161
|
+
- `/tmp/fluxera-release-venv/bin/python -m twine check dist/*`
|
|
162
|
+
|
|
163
|
+
## Documentation
|
|
164
|
+
|
|
165
|
+
- [Getting Started](docs/GETTING_STARTED.md)
|
|
166
|
+
- [Benchmark Results](docs/BENCHMARK.md)
|
|
167
|
+
- [Revision Management](docs/REVISION_MANAGEMENT.md)
|
|
168
|
+
- [System Design](docs/SYSTEM_DESIGN.md)
|
|
169
|
+
- [Deduplication and Idempotency](docs/DEDUP_IDEMPOTENCY.md)
|
|
170
|
+
- [Redis Lua Contract](docs/REDIS_LUA_CONTRACT.md)
|
|
171
|
+
|
|
172
|
+
## Current Limits
|
|
173
|
+
|
|
174
|
+
- public APIs may still change during the alpha period
|
|
175
|
+
- result backends are not implemented yet
|
|
176
|
+
- message registry garbage collection is still intentionally simple
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .admin import (
|
|
4
|
+
ServingRevisionPromotion,
|
|
5
|
+
ServingRevisionStatus,
|
|
6
|
+
ensure_serving_revision,
|
|
7
|
+
get_serving_revision,
|
|
8
|
+
promote_serving_revision,
|
|
9
|
+
)
|
|
10
|
+
from .actor import Actor, actor
|
|
11
|
+
from .broker import Broker, Consumer, Delivery, get_broker, set_broker
|
|
12
|
+
from .encoder import JSONMessageEncoder, PickleMessageEncoder
|
|
13
|
+
from .brokers.redis import RedisBroker
|
|
14
|
+
from .brokers.stub import StubBroker
|
|
15
|
+
from .message import Message
|
|
16
|
+
from .runtime.worker import TaskRecord, Worker
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"Actor",
|
|
20
|
+
"Broker",
|
|
21
|
+
"Consumer",
|
|
22
|
+
"Delivery",
|
|
23
|
+
"JSONMessageEncoder",
|
|
24
|
+
"Message",
|
|
25
|
+
"PickleMessageEncoder",
|
|
26
|
+
"RedisBroker",
|
|
27
|
+
"ServingRevisionPromotion",
|
|
28
|
+
"ServingRevisionStatus",
|
|
29
|
+
"StubBroker",
|
|
30
|
+
"TaskRecord",
|
|
31
|
+
"Worker",
|
|
32
|
+
"actor",
|
|
33
|
+
"ensure_serving_revision",
|
|
34
|
+
"get_broker",
|
|
35
|
+
"get_serving_revision",
|
|
36
|
+
"promote_serving_revision",
|
|
37
|
+
"set_broker",
|
|
38
|
+
]
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from inspect import iscoroutinefunction
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Optional, Protocol, TypeVar, Union, overload
|
|
6
|
+
|
|
7
|
+
from .broker import Broker, get_broker
|
|
8
|
+
from .message import Message
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .runtime.worker import Worker
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
P = TypeVar("P")
|
|
15
|
+
R = TypeVar("R")
|
|
16
|
+
ExecutionMode = str
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Actor:
|
|
20
|
+
"""Thin wrapper around callables that stores queueing metadata."""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
fn: Callable[..., Any],
|
|
25
|
+
*,
|
|
26
|
+
broker: Broker,
|
|
27
|
+
actor_name: str,
|
|
28
|
+
queue_name: str,
|
|
29
|
+
execution: Optional[ExecutionMode] = None,
|
|
30
|
+
options: Optional[dict[str, Any]] = None,
|
|
31
|
+
) -> None:
|
|
32
|
+
if actor_name in broker.actors:
|
|
33
|
+
raise ValueError(f"An actor named {actor_name!r} is already registered.")
|
|
34
|
+
|
|
35
|
+
inferred_execution = "async" if iscoroutinefunction(fn) else "thread"
|
|
36
|
+
self.fn = fn
|
|
37
|
+
self.broker = broker
|
|
38
|
+
self.actor_name = actor_name
|
|
39
|
+
self.queue_name = queue_name
|
|
40
|
+
self.execution = execution or inferred_execution
|
|
41
|
+
self.options = options or {}
|
|
42
|
+
|
|
43
|
+
if self.execution == "async" and not iscoroutinefunction(fn):
|
|
44
|
+
raise TypeError("Execution mode 'async' requires an async function.")
|
|
45
|
+
|
|
46
|
+
self.broker.declare_actor(self)
|
|
47
|
+
|
|
48
|
+
def message(self, *args: Any, **kwargs: Any) -> Message:
|
|
49
|
+
return self.message_with_options(args=args, kwargs=kwargs)
|
|
50
|
+
|
|
51
|
+
def message_with_options(
|
|
52
|
+
self,
|
|
53
|
+
*,
|
|
54
|
+
args: tuple[Any, ...] = (),
|
|
55
|
+
kwargs: Optional[dict[str, Any]] = None,
|
|
56
|
+
**options: Any,
|
|
57
|
+
) -> Message:
|
|
58
|
+
return Message(
|
|
59
|
+
queue_name=self.queue_name,
|
|
60
|
+
actor_name=self.actor_name,
|
|
61
|
+
args=args,
|
|
62
|
+
kwargs=(kwargs or {}).copy(),
|
|
63
|
+
options={**self.options, **options},
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
async def send(self, *args: Any, **kwargs: Any) -> Message:
|
|
67
|
+
return await self.send_with_options(args=args, kwargs=kwargs)
|
|
68
|
+
|
|
69
|
+
async def send_with_options(
|
|
70
|
+
self,
|
|
71
|
+
*,
|
|
72
|
+
args: tuple[Any, ...] = (),
|
|
73
|
+
kwargs: Optional[dict[str, Any]] = None,
|
|
74
|
+
delay: Optional[float] = None,
|
|
75
|
+
**options: Any,
|
|
76
|
+
) -> Message:
|
|
77
|
+
return await self.broker.send(
|
|
78
|
+
self.message_with_options(args=args, kwargs=kwargs, **options),
|
|
79
|
+
delay=delay,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def send_sync(self, *args: Any, **kwargs: Any) -> Message:
|
|
83
|
+
try:
|
|
84
|
+
asyncio.get_running_loop()
|
|
85
|
+
except RuntimeError:
|
|
86
|
+
return asyncio.run(self.send(*args, **kwargs))
|
|
87
|
+
|
|
88
|
+
raise RuntimeError("send_sync cannot be called while an event loop is already running.")
|
|
89
|
+
|
|
90
|
+
def send_with_options_sync(
|
|
91
|
+
self,
|
|
92
|
+
*,
|
|
93
|
+
args: tuple[Any, ...] = (),
|
|
94
|
+
kwargs: Optional[dict[str, Any]] = None,
|
|
95
|
+
delay: Optional[float] = None,
|
|
96
|
+
**options: Any,
|
|
97
|
+
) -> Message:
|
|
98
|
+
try:
|
|
99
|
+
asyncio.get_running_loop()
|
|
100
|
+
except RuntimeError:
|
|
101
|
+
return asyncio.run(self.send_with_options(args=args, kwargs=kwargs, delay=delay, **options))
|
|
102
|
+
|
|
103
|
+
raise RuntimeError("send_with_options_sync cannot be called while an event loop is already running.")
|
|
104
|
+
|
|
105
|
+
async def run(self, *args: Any, worker: Optional["Worker"] = None, **kwargs: Any) -> Any:
|
|
106
|
+
if self.execution == "async":
|
|
107
|
+
async_fn = self.fn
|
|
108
|
+
return await async_fn(*args, **kwargs)
|
|
109
|
+
|
|
110
|
+
if self.execution == "thread":
|
|
111
|
+
if worker is not None:
|
|
112
|
+
return await worker.run_in_thread(self.fn, *args, **kwargs)
|
|
113
|
+
return await asyncio.to_thread(self.fn, *args, **kwargs)
|
|
114
|
+
|
|
115
|
+
if self.execution == "process":
|
|
116
|
+
if worker is None:
|
|
117
|
+
raise RuntimeError("Process execution requires a worker runtime.")
|
|
118
|
+
return await worker.run_in_process(self.fn, *args, **kwargs)
|
|
119
|
+
|
|
120
|
+
raise RuntimeError(f"Unknown execution mode {self.execution!r}.")
|
|
121
|
+
|
|
122
|
+
def __call__(self, *args: Any, **kwargs: Any) -> Any:
|
|
123
|
+
return self.fn(*args, **kwargs)
|
|
124
|
+
|
|
125
|
+
def __repr__(self) -> str:
|
|
126
|
+
return (
|
|
127
|
+
"Actor(fn=%r, actor_name=%r, queue_name=%r, execution=%r)"
|
|
128
|
+
% (self.fn, self.actor_name, self.queue_name, self.execution)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class ActorDecorator(Protocol):
|
|
133
|
+
def __call__(self, fn: Callable[..., Any]) -> Actor: ...
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@overload
|
|
137
|
+
def actor(fn: Callable[..., Any]) -> Actor:
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@overload
|
|
142
|
+
def actor(
|
|
143
|
+
*,
|
|
144
|
+
broker: Optional[Broker] = None,
|
|
145
|
+
actor_name: Optional[str] = None,
|
|
146
|
+
queue_name: str = "default",
|
|
147
|
+
execution: Optional[ExecutionMode] = None,
|
|
148
|
+
**options: Any,
|
|
149
|
+
) -> ActorDecorator:
|
|
150
|
+
pass
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def actor(
|
|
154
|
+
fn: Optional[Callable[..., Any]] = None,
|
|
155
|
+
*,
|
|
156
|
+
broker: Optional[Broker] = None,
|
|
157
|
+
actor_name: Optional[str] = None,
|
|
158
|
+
queue_name: str = "default",
|
|
159
|
+
execution: Optional[ExecutionMode] = None,
|
|
160
|
+
**options: Any,
|
|
161
|
+
) -> Union[Actor, ActorDecorator]:
|
|
162
|
+
def decorator(inner_fn: Callable[..., Any]) -> Actor:
|
|
163
|
+
target_broker = broker or get_broker()
|
|
164
|
+
return Actor(
|
|
165
|
+
inner_fn,
|
|
166
|
+
broker=target_broker,
|
|
167
|
+
actor_name=actor_name or inner_fn.__name__,
|
|
168
|
+
queue_name=queue_name,
|
|
169
|
+
execution=execution,
|
|
170
|
+
options=options,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
if fn is None:
|
|
174
|
+
return decorator
|
|
175
|
+
|
|
176
|
+
return decorator(fn)
|