bulklink 0.2.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.
- bulklink-0.2.0/LICENSE +21 -0
- bulklink-0.2.0/PKG-INFO +246 -0
- bulklink-0.2.0/README.md +213 -0
- bulklink-0.2.0/pyproject.toml +80 -0
- bulklink-0.2.0/setup.cfg +4 -0
- bulklink-0.2.0/src/bulklink/__init__.py +43 -0
- bulklink-0.2.0/src/bulklink/_internal/__init__.py +4 -0
- bulklink-0.2.0/src/bulklink/_internal/cancellation.py +34 -0
- bulklink-0.2.0/src/bulklink/_internal/coordinator.py +620 -0
- bulklink-0.2.0/src/bulklink/_internal/diagnostics.py +254 -0
- bulklink-0.2.0/src/bulklink/_internal/events.py +69 -0
- bulklink-0.2.0/src/bulklink/_internal/models.py +61 -0
- bulklink-0.2.0/src/bulklink/_internal/slot.py +63 -0
- bulklink-0.2.0/src/bulklink/_internal/validation.py +51 -0
- bulklink-0.2.0/src/bulklink/bulkhead.py +161 -0
- bulklink-0.2.0/src/bulklink/capacity.py +154 -0
- bulklink-0.2.0/src/bulklink/errors.py +53 -0
- bulklink-0.2.0/src/bulklink/events.py +44 -0
- bulklink-0.2.0/src/bulklink/py.typed +0 -0
- bulklink-0.2.0/src/bulklink/registry.py +242 -0
- bulklink-0.2.0/src/bulklink/status.py +100 -0
- bulklink-0.2.0/src/bulklink/typing.py +8 -0
- bulklink-0.2.0/src/bulklink.egg-info/PKG-INFO +246 -0
- bulklink-0.2.0/src/bulklink.egg-info/SOURCES.txt +25 -0
- bulklink-0.2.0/src/bulklink.egg-info/dependency_links.txt +1 -0
- bulklink-0.2.0/src/bulklink.egg-info/requires.txt +8 -0
- bulklink-0.2.0/src/bulklink.egg-info/top_level.txt +1 -0
bulklink-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bulklink Contributors
|
|
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.
|
bulklink-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bulklink
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Predictable bulkhead isolation and bounded concurrency for Python asyncio.
|
|
5
|
+
Author: Bulklink Contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/igors93/bulklink
|
|
8
|
+
Project-URL: Repository, https://github.com/igors93/bulklink
|
|
9
|
+
Project-URL: Documentation, https://github.com/igors93/bulklink/tree/main/docs
|
|
10
|
+
Project-URL: Issues, https://github.com/igors93/bulklink/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/igors93/bulklink/blob/main/CHANGELOG.md
|
|
12
|
+
Keywords: asyncio,bulkhead,concurrency,backpressure,resilience,load-shedding
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
30
|
+
Requires-Dist: ruff>=0.11; extra == "dev"
|
|
31
|
+
Requires-Dist: mypy>=1.15; extra == "dev"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
<div align="center">
|
|
35
|
+
|
|
36
|
+
# Bulklink
|
|
37
|
+
|
|
38
|
+
**Simple admission control. Strong isolation. Predictable behavior under load.**
|
|
39
|
+
|
|
40
|
+
Bulklink is a small, typed, zero-dependency library for bulkhead isolation and
|
|
41
|
+
bounded concurrency in Python `asyncio` applications.
|
|
42
|
+
|
|
43
|
+
Current package version: **0.2.0**. The documented `0.2.x` public contract is stable.
|
|
44
|
+
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
## What problem does it solve?
|
|
48
|
+
|
|
49
|
+
Without admission control, one slow dependency can attract hundreds of concurrent
|
|
50
|
+
operations, consume connections and memory, and damage unrelated parts of an
|
|
51
|
+
application.
|
|
52
|
+
|
|
53
|
+
Bulklink creates independent compartments:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from bulklink import AsyncBulkhead
|
|
57
|
+
|
|
58
|
+
payments = AsyncBulkhead(
|
|
59
|
+
label="payments",
|
|
60
|
+
parallelism=10,
|
|
61
|
+
waiting_room=50,
|
|
62
|
+
wait_limit=2.0,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
reports = AsyncBulkhead(
|
|
66
|
+
label="reports",
|
|
67
|
+
parallelism=2,
|
|
68
|
+
waiting_room=5,
|
|
69
|
+
)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Slow reports can use at most two execution slots. They cannot consume the ten slots
|
|
73
|
+
reserved for payments.
|
|
74
|
+
|
|
75
|
+
## Quick start
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
async def send_payment(order: object) -> object:
|
|
79
|
+
async with payments.slot():
|
|
80
|
+
return await payment_api.send(order)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Or:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
result = await payments.execute(payment_api.send, order)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Reject instead of waiting when immediate capacity is required:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
result = await payments.execute_now(payment_api.send, order)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Use a shorter limit for one call without extending the bulkhead default:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
result = await payments.execute_within(0.25, payment_api.send, order)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Or decorate an async function:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
@payments
|
|
105
|
+
async def send_payment(order: object) -> object:
|
|
106
|
+
return await payment_api.send(order)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Behavior
|
|
110
|
+
|
|
111
|
+
For each bulkhead:
|
|
112
|
+
|
|
113
|
+
1. up to `parallelism` operations may execute;
|
|
114
|
+
2. up to `waiting_room` operations may wait in FIFO order;
|
|
115
|
+
3. an operation is rejected immediately when both areas are full;
|
|
116
|
+
4. a waiting operation is rejected when `wait_limit` expires;
|
|
117
|
+
5. exceptions and task cancellation release capacity safely;
|
|
118
|
+
6. `close()` rejects queued and future operations without interrupting active work;
|
|
119
|
+
7. `wait_closed()` waits until all active operations have released their slots;
|
|
120
|
+
8. `resize()` changes capacity without cancelling active work or bypassing FIFO order.
|
|
121
|
+
|
|
122
|
+
## Graceful shutdown
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
await payments.close_and_wait()
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`close_and_wait()` stops new admission, rejects queued work, and waits for operations
|
|
129
|
+
already running to finish. Cancelling the caller does not cancel protected operations.
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
## Observe state transitions
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from bulklink import BulkheadEvent
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def record_event(event: BulkheadEvent) -> None:
|
|
139
|
+
print(event.kind.value, event.in_flight, event.waiting)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
payments.add_event_handler(record_event)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Handlers are synchronous, run outside the coordinator lock, and receive immutable
|
|
146
|
+
metadata only. They never receive operation arguments, results, or exceptions. Handler
|
|
147
|
+
failures are reported through the event loop exception handler without changing
|
|
148
|
+
bulkhead state.
|
|
149
|
+
|
|
150
|
+
## Diagnose capacity pressure
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
report = await payments.capacity_report()
|
|
154
|
+
|
|
155
|
+
print(report.summary)
|
|
156
|
+
for finding in report.findings:
|
|
157
|
+
print(finding.severity.value, finding.message)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The report combines the current snapshot with cumulative admission history. It is
|
|
161
|
+
immutable, conservative with small samples, and never changes the bulkhead
|
|
162
|
+
configuration.
|
|
163
|
+
|
|
164
|
+
## Change capacity safely
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
await payments.resize(20)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Increasing capacity admits queued operations in FIFO order. Reducing capacity never
|
|
171
|
+
cancels active work; existing operations drain naturally before admission resumes at
|
|
172
|
+
the new limit.
|
|
173
|
+
|
|
174
|
+
## Manage named bulkheads together
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
from bulklink import BulkheadRegistry
|
|
178
|
+
|
|
179
|
+
registry = BulkheadRegistry()
|
|
180
|
+
payments = registry.create("payments", parallelism=10, waiting_room=20)
|
|
181
|
+
reports = registry.create("reports", parallelism=2)
|
|
182
|
+
|
|
183
|
+
await registry.close_and_wait()
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The registry is optional. It enforces unique names, returns immutable ordered snapshots,
|
|
187
|
+
and coordinates shutdown without replacing direct `AsyncBulkhead` usage.
|
|
188
|
+
|
|
189
|
+
## Designed to coexist with Relinker
|
|
190
|
+
|
|
191
|
+
Bulklink and Relinker solve different stages:
|
|
192
|
+
|
|
193
|
+
- **Bulklink** decides whether one operation may start;
|
|
194
|
+
- **Relinker** decides whether a failed operation should be attempted again.
|
|
195
|
+
|
|
196
|
+
Bulklink deliberately uses `AsyncBulkhead`, `execute()`, `slot()`, and `status()`,
|
|
197
|
+
rather than Relinker's policy, retry, result, budget, `run_async()`, and `snapshot()`
|
|
198
|
+
terminology.
|
|
199
|
+
|
|
200
|
+
See [Using Bulklink with Relinker](docs/guides/with-relinker.md).
|
|
201
|
+
|
|
202
|
+
## Non-goals
|
|
203
|
+
|
|
204
|
+
Bulklink does not provide retries, backoff, jitter, circuit breakers, HTTP-specific
|
|
205
|
+
behavior, requests-per-second limits, or distributed coordination.
|
|
206
|
+
|
|
207
|
+
## Development
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
python -m pip install -e ".[dev]"
|
|
211
|
+
./scripts/ci.sh
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Documentation
|
|
215
|
+
|
|
216
|
+
- [Documentation index](docs/README.md)
|
|
217
|
+
- [Getting started](docs/guides/getting-started.md)
|
|
218
|
+
- [Using Bulklink with Relinker](docs/guides/with-relinker.md)
|
|
219
|
+
- [Production checklist](docs/guides/production-checklist.md)
|
|
220
|
+
- [Capacity diagnostics](docs/concepts/capacity-diagnostics.md)
|
|
221
|
+
- [Dynamic capacity](docs/concepts/dynamic-capacity.md)
|
|
222
|
+
- [Named bulkhead registry](docs/concepts/registry.md)
|
|
223
|
+
- [Architecture](docs/maintainers/architecture.md)
|
|
224
|
+
|
|
225
|
+
## License
|
|
226
|
+
|
|
227
|
+
MIT.
|
|
228
|
+
|
|
229
|
+
## Validation and benchmarks
|
|
230
|
+
|
|
231
|
+
The release candidate is checked on Python 3.10 through 3.14 on Linux, with additional
|
|
232
|
+
Windows and macOS validation. The suite includes deterministic race tests, generated
|
|
233
|
+
model-oriented sequences, adversarial stress, executable examples, clean-wheel
|
|
234
|
+
installation, and consumer-facing typing checks.
|
|
235
|
+
|
|
236
|
+
Run the complete local verification on Linux or macOS:
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
./scripts/ci.sh
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Record a local performance baseline without enforcing unstable timing thresholds:
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
python -m benchmarks.run --output benchmark-results.json
|
|
246
|
+
```
|
bulklink-0.2.0/README.md
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# Bulklink
|
|
4
|
+
|
|
5
|
+
**Simple admission control. Strong isolation. Predictable behavior under load.**
|
|
6
|
+
|
|
7
|
+
Bulklink is a small, typed, zero-dependency library for bulkhead isolation and
|
|
8
|
+
bounded concurrency in Python `asyncio` applications.
|
|
9
|
+
|
|
10
|
+
Current package version: **0.2.0**. The documented `0.2.x` public contract is stable.
|
|
11
|
+
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
## What problem does it solve?
|
|
15
|
+
|
|
16
|
+
Without admission control, one slow dependency can attract hundreds of concurrent
|
|
17
|
+
operations, consume connections and memory, and damage unrelated parts of an
|
|
18
|
+
application.
|
|
19
|
+
|
|
20
|
+
Bulklink creates independent compartments:
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from bulklink import AsyncBulkhead
|
|
24
|
+
|
|
25
|
+
payments = AsyncBulkhead(
|
|
26
|
+
label="payments",
|
|
27
|
+
parallelism=10,
|
|
28
|
+
waiting_room=50,
|
|
29
|
+
wait_limit=2.0,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
reports = AsyncBulkhead(
|
|
33
|
+
label="reports",
|
|
34
|
+
parallelism=2,
|
|
35
|
+
waiting_room=5,
|
|
36
|
+
)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Slow reports can use at most two execution slots. They cannot consume the ten slots
|
|
40
|
+
reserved for payments.
|
|
41
|
+
|
|
42
|
+
## Quick start
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
async def send_payment(order: object) -> object:
|
|
46
|
+
async with payments.slot():
|
|
47
|
+
return await payment_api.send(order)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Or:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
result = await payments.execute(payment_api.send, order)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Reject instead of waiting when immediate capacity is required:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
result = await payments.execute_now(payment_api.send, order)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Use a shorter limit for one call without extending the bulkhead default:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
result = await payments.execute_within(0.25, payment_api.send, order)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Or decorate an async function:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
@payments
|
|
72
|
+
async def send_payment(order: object) -> object:
|
|
73
|
+
return await payment_api.send(order)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Behavior
|
|
77
|
+
|
|
78
|
+
For each bulkhead:
|
|
79
|
+
|
|
80
|
+
1. up to `parallelism` operations may execute;
|
|
81
|
+
2. up to `waiting_room` operations may wait in FIFO order;
|
|
82
|
+
3. an operation is rejected immediately when both areas are full;
|
|
83
|
+
4. a waiting operation is rejected when `wait_limit` expires;
|
|
84
|
+
5. exceptions and task cancellation release capacity safely;
|
|
85
|
+
6. `close()` rejects queued and future operations without interrupting active work;
|
|
86
|
+
7. `wait_closed()` waits until all active operations have released their slots;
|
|
87
|
+
8. `resize()` changes capacity without cancelling active work or bypassing FIFO order.
|
|
88
|
+
|
|
89
|
+
## Graceful shutdown
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
await payments.close_and_wait()
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`close_and_wait()` stops new admission, rejects queued work, and waits for operations
|
|
96
|
+
already running to finish. Cancelling the caller does not cancel protected operations.
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
## Observe state transitions
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from bulklink import BulkheadEvent
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def record_event(event: BulkheadEvent) -> None:
|
|
106
|
+
print(event.kind.value, event.in_flight, event.waiting)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
payments.add_event_handler(record_event)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Handlers are synchronous, run outside the coordinator lock, and receive immutable
|
|
113
|
+
metadata only. They never receive operation arguments, results, or exceptions. Handler
|
|
114
|
+
failures are reported through the event loop exception handler without changing
|
|
115
|
+
bulkhead state.
|
|
116
|
+
|
|
117
|
+
## Diagnose capacity pressure
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
report = await payments.capacity_report()
|
|
121
|
+
|
|
122
|
+
print(report.summary)
|
|
123
|
+
for finding in report.findings:
|
|
124
|
+
print(finding.severity.value, finding.message)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The report combines the current snapshot with cumulative admission history. It is
|
|
128
|
+
immutable, conservative with small samples, and never changes the bulkhead
|
|
129
|
+
configuration.
|
|
130
|
+
|
|
131
|
+
## Change capacity safely
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
await payments.resize(20)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Increasing capacity admits queued operations in FIFO order. Reducing capacity never
|
|
138
|
+
cancels active work; existing operations drain naturally before admission resumes at
|
|
139
|
+
the new limit.
|
|
140
|
+
|
|
141
|
+
## Manage named bulkheads together
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from bulklink import BulkheadRegistry
|
|
145
|
+
|
|
146
|
+
registry = BulkheadRegistry()
|
|
147
|
+
payments = registry.create("payments", parallelism=10, waiting_room=20)
|
|
148
|
+
reports = registry.create("reports", parallelism=2)
|
|
149
|
+
|
|
150
|
+
await registry.close_and_wait()
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The registry is optional. It enforces unique names, returns immutable ordered snapshots,
|
|
154
|
+
and coordinates shutdown without replacing direct `AsyncBulkhead` usage.
|
|
155
|
+
|
|
156
|
+
## Designed to coexist with Relinker
|
|
157
|
+
|
|
158
|
+
Bulklink and Relinker solve different stages:
|
|
159
|
+
|
|
160
|
+
- **Bulklink** decides whether one operation may start;
|
|
161
|
+
- **Relinker** decides whether a failed operation should be attempted again.
|
|
162
|
+
|
|
163
|
+
Bulklink deliberately uses `AsyncBulkhead`, `execute()`, `slot()`, and `status()`,
|
|
164
|
+
rather than Relinker's policy, retry, result, budget, `run_async()`, and `snapshot()`
|
|
165
|
+
terminology.
|
|
166
|
+
|
|
167
|
+
See [Using Bulklink with Relinker](docs/guides/with-relinker.md).
|
|
168
|
+
|
|
169
|
+
## Non-goals
|
|
170
|
+
|
|
171
|
+
Bulklink does not provide retries, backoff, jitter, circuit breakers, HTTP-specific
|
|
172
|
+
behavior, requests-per-second limits, or distributed coordination.
|
|
173
|
+
|
|
174
|
+
## Development
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
python -m pip install -e ".[dev]"
|
|
178
|
+
./scripts/ci.sh
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Documentation
|
|
182
|
+
|
|
183
|
+
- [Documentation index](docs/README.md)
|
|
184
|
+
- [Getting started](docs/guides/getting-started.md)
|
|
185
|
+
- [Using Bulklink with Relinker](docs/guides/with-relinker.md)
|
|
186
|
+
- [Production checklist](docs/guides/production-checklist.md)
|
|
187
|
+
- [Capacity diagnostics](docs/concepts/capacity-diagnostics.md)
|
|
188
|
+
- [Dynamic capacity](docs/concepts/dynamic-capacity.md)
|
|
189
|
+
- [Named bulkhead registry](docs/concepts/registry.md)
|
|
190
|
+
- [Architecture](docs/maintainers/architecture.md)
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
MIT.
|
|
195
|
+
|
|
196
|
+
## Validation and benchmarks
|
|
197
|
+
|
|
198
|
+
The release candidate is checked on Python 3.10 through 3.14 on Linux, with additional
|
|
199
|
+
Windows and macOS validation. The suite includes deterministic race tests, generated
|
|
200
|
+
model-oriented sequences, adversarial stress, executable examples, clean-wheel
|
|
201
|
+
installation, and consumer-facing typing checks.
|
|
202
|
+
|
|
203
|
+
Run the complete local verification on Linux or macOS:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
./scripts/ci.sh
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Record a local performance baseline without enforcing unstable timing thresholds:
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
python -m benchmarks.run --output benchmark-results.json
|
|
213
|
+
```
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "bulklink"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Predictable bulkhead isolation and bounded concurrency for Python asyncio."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Bulklink Contributors" }]
|
|
14
|
+
keywords = ["asyncio", "bulkhead", "concurrency", "backpressure", "resilience", "load-shedding"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Programming Language :: Python :: 3.14",
|
|
24
|
+
"Typing :: Typed",
|
|
25
|
+
]
|
|
26
|
+
dependencies = []
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
dev = [
|
|
30
|
+
"build>=1.2",
|
|
31
|
+
"pytest>=8.0",
|
|
32
|
+
"pytest-asyncio>=0.24",
|
|
33
|
+
"pytest-cov>=5.0",
|
|
34
|
+
"ruff>=0.11",
|
|
35
|
+
"mypy>=1.15",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/igors93/bulklink"
|
|
40
|
+
Repository = "https://github.com/igors93/bulklink"
|
|
41
|
+
Documentation = "https://github.com/igors93/bulklink/tree/main/docs"
|
|
42
|
+
Issues = "https://github.com/igors93/bulklink/issues"
|
|
43
|
+
Changelog = "https://github.com/igors93/bulklink/blob/main/CHANGELOG.md"
|
|
44
|
+
|
|
45
|
+
[tool.setuptools.packages.find]
|
|
46
|
+
where = ["src"]
|
|
47
|
+
|
|
48
|
+
[tool.setuptools.package-data]
|
|
49
|
+
bulklink = ["py.typed"]
|
|
50
|
+
|
|
51
|
+
[tool.pytest.ini_options]
|
|
52
|
+
testpaths = ["tests"]
|
|
53
|
+
pythonpath = ["src"]
|
|
54
|
+
asyncio_mode = "auto"
|
|
55
|
+
addopts = ["--import-mode=importlib"]
|
|
56
|
+
markers = [
|
|
57
|
+
"stress: deterministic adversarial concurrency tests",
|
|
58
|
+
"model: generated model-oriented state transition tests",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
[tool.ruff]
|
|
62
|
+
line-length = 100
|
|
63
|
+
target-version = "py310"
|
|
64
|
+
|
|
65
|
+
[tool.ruff.lint]
|
|
66
|
+
select = ["E", "F", "I", "B", "UP", "SIM"]
|
|
67
|
+
|
|
68
|
+
[tool.mypy]
|
|
69
|
+
python_version = "3.10"
|
|
70
|
+
strict = true
|
|
71
|
+
mypy_path = "src"
|
|
72
|
+
|
|
73
|
+
[tool.coverage.run]
|
|
74
|
+
branch = true
|
|
75
|
+
source = ["bulklink"]
|
|
76
|
+
|
|
77
|
+
[tool.coverage.report]
|
|
78
|
+
fail_under = 90
|
|
79
|
+
show_missing = true
|
|
80
|
+
skip_covered = false
|
bulklink-0.2.0/setup.cfg
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Stable Bulklink public API."""
|
|
2
|
+
|
|
3
|
+
from bulklink.bulkhead import AsyncBulkhead
|
|
4
|
+
from bulklink.capacity import (
|
|
5
|
+
CapacityFinding,
|
|
6
|
+
CapacityFindingCode,
|
|
7
|
+
CapacityReport,
|
|
8
|
+
CapacitySeverity,
|
|
9
|
+
)
|
|
10
|
+
from bulklink.errors import (
|
|
11
|
+
BulkheadClosedError,
|
|
12
|
+
BulkheadQueueTimeoutError,
|
|
13
|
+
BulkheadSaturatedError,
|
|
14
|
+
BulklinkError,
|
|
15
|
+
)
|
|
16
|
+
from bulklink.events import BulkheadEvent, BulkheadEventHandler, BulkheadEventKind
|
|
17
|
+
from bulklink.registry import (
|
|
18
|
+
BulkheadRegistry,
|
|
19
|
+
BulkheadRegistryFailure,
|
|
20
|
+
BulkheadRegistryOperationError,
|
|
21
|
+
)
|
|
22
|
+
from bulklink.status import BulkheadStatus
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"AsyncBulkhead",
|
|
26
|
+
"CapacityFinding",
|
|
27
|
+
"CapacityFindingCode",
|
|
28
|
+
"CapacityReport",
|
|
29
|
+
"CapacitySeverity",
|
|
30
|
+
"BulkheadClosedError",
|
|
31
|
+
"BulkheadEvent",
|
|
32
|
+
"BulkheadEventHandler",
|
|
33
|
+
"BulkheadEventKind",
|
|
34
|
+
"BulkheadQueueTimeoutError",
|
|
35
|
+
"BulkheadRegistry",
|
|
36
|
+
"BulkheadRegistryFailure",
|
|
37
|
+
"BulkheadRegistryOperationError",
|
|
38
|
+
"BulkheadSaturatedError",
|
|
39
|
+
"BulkheadStatus",
|
|
40
|
+
"BulklinkError",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
__version__ = "0.2.0"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Helpers for completing critical cleanup during task cancellation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections.abc import Coroutine
|
|
7
|
+
from typing import Any, TypeVar
|
|
8
|
+
|
|
9
|
+
T = TypeVar("T")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def complete_cleanup(action: Coroutine[Any, Any, T]) -> T:
|
|
13
|
+
"""Finish one cleanup coroutine despite repeated cancellation requests.
|
|
14
|
+
|
|
15
|
+
Cancellation is remembered and propagated only after the cleanup task has
|
|
16
|
+
finished. An exception raised by the cleanup itself takes precedence because
|
|
17
|
+
it indicates that internal state may not have been restored.
|
|
18
|
+
"""
|
|
19
|
+
cleanup_task = asyncio.create_task(action)
|
|
20
|
+
cancellation: asyncio.CancelledError | None = None
|
|
21
|
+
|
|
22
|
+
while not cleanup_task.done():
|
|
23
|
+
try:
|
|
24
|
+
await asyncio.shield(cleanup_task)
|
|
25
|
+
except asyncio.CancelledError as error:
|
|
26
|
+
if cancellation is None:
|
|
27
|
+
cancellation = error
|
|
28
|
+
|
|
29
|
+
result = cleanup_task.result()
|
|
30
|
+
|
|
31
|
+
if cancellation is not None:
|
|
32
|
+
raise cancellation
|
|
33
|
+
|
|
34
|
+
return result
|