resource-pressure 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.
- resource_pressure-0.1.0/.github/workflows/ci.yml +34 -0
- resource_pressure-0.1.0/CHANGELOG.md +13 -0
- resource_pressure-0.1.0/LICENSE +21 -0
- resource_pressure-0.1.0/MANIFEST.in +10 -0
- resource_pressure-0.1.0/PKG-INFO +385 -0
- resource_pressure-0.1.0/README.md +355 -0
- resource_pressure-0.1.0/SECURITY.md +20 -0
- resource_pressure-0.1.0/docs/ARCHITECTURE.md +123 -0
- resource_pressure-0.1.0/docs/PACKAGING_REPORT.md +17 -0
- resource_pressure-0.1.0/docs/SOURCES.md +31 -0
- resource_pressure-0.1.0/docs/TEST_REPORT.md +74 -0
- resource_pressure-0.1.0/docs/test-output.txt +7 -0
- resource_pressure-0.1.0/docs/test-results.xml +1 -0
- resource_pressure-0.1.0/examples/contained_command.py +20 -0
- resource_pressure-0.1.0/examples/contained_dask_launcher.py +28 -0
- resource_pressure-0.1.0/examples/dask_local.py +24 -0
- resource_pressure-0.1.0/examples/simulated_demo.py +31 -0
- resource_pressure-0.1.0/pyproject.toml +49 -0
- resource_pressure-0.1.0/setup.cfg +4 -0
- resource_pressure-0.1.0/src/resource_pressure/__init__.py +14 -0
- resource_pressure-0.1.0/src/resource_pressure/__main__.py +3 -0
- resource_pressure-0.1.0/src/resource_pressure/backends/__init__.py +25 -0
- resource_pressure-0.1.0/src/resource_pressure/backends/base.py +21 -0
- resource_pressure-0.1.0/src/resource_pressure/backends/linux.py +146 -0
- resource_pressure-0.1.0/src/resource_pressure/backends/macos.py +155 -0
- resource_pressure-0.1.0/src/resource_pressure/backends/windows.py +103 -0
- resource_pressure-0.1.0/src/resource_pressure/cli.py +44 -0
- resource_pressure-0.1.0/src/resource_pressure/governor.py +373 -0
- resource_pressure-0.1.0/src/resource_pressure/integrations/__init__.py +1 -0
- resource_pressure-0.1.0/src/resource_pressure/integrations/dask.py +157 -0
- resource_pressure-0.1.0/src/resource_pressure/integrations/processkit.py +143 -0
- resource_pressure-0.1.0/src/resource_pressure/model.py +42 -0
- resource_pressure-0.1.0/src/resource_pressure/py.typed +0 -0
- resource_pressure-0.1.0/src/resource_pressure/testing.py +34 -0
- resource_pressure-0.1.0/src/resource_pressure.egg-info/PKG-INFO +385 -0
- resource_pressure-0.1.0/src/resource_pressure.egg-info/SOURCES.txt +48 -0
- resource_pressure-0.1.0/src/resource_pressure.egg-info/dependency_links.txt +1 -0
- resource_pressure-0.1.0/src/resource_pressure.egg-info/entry_points.txt +2 -0
- resource_pressure-0.1.0/src/resource_pressure.egg-info/requires.txt +15 -0
- resource_pressure-0.1.0/src/resource_pressure.egg-info/top_level.txt +1 -0
- resource_pressure-0.1.0/tests/conftest.py +31 -0
- resource_pressure-0.1.0/tests/test_backends.py +208 -0
- resource_pressure-0.1.0/tests/test_cli.py +21 -0
- resource_pressure-0.1.0/tests/test_containment.py +138 -0
- resource_pressure-0.1.0/tests/test_dask_adapter.py +160 -0
- resource_pressure-0.1.0/tests/test_governor.py +260 -0
- resource_pressure-0.1.0/tests/test_native.py +17 -0
- resource_pressure-0.1.0/tests/test_real_dask.py +18 -0
- resource_pressure-0.1.0/tests/test_real_processkit.py +18 -0
- resource_pressure-0.1.0/tools/build_dist.py +20 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: tests
|
|
2
|
+
on: [push, pull_request, workflow_dispatch]
|
|
3
|
+
permissions:
|
|
4
|
+
contents: read
|
|
5
|
+
jobs:
|
|
6
|
+
unit:
|
|
7
|
+
strategy:
|
|
8
|
+
fail-fast: false
|
|
9
|
+
matrix:
|
|
10
|
+
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
11
|
+
python: ["3.10", "3.12", "3.13", "3.14"]
|
|
12
|
+
runs-on: ${{ matrix.os }}
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: actions/setup-python@v5
|
|
16
|
+
with:
|
|
17
|
+
python-version: ${{ matrix.python }}
|
|
18
|
+
- run: python -m pip install -e ".[dev]"
|
|
19
|
+
- run: python -m pytest -q -ra
|
|
20
|
+
- name: Native initialization and teardown (not induced pressure)
|
|
21
|
+
if: runner.os != 'Linux'
|
|
22
|
+
env:
|
|
23
|
+
RESOURCE_PRESSURE_NATIVE: "1"
|
|
24
|
+
run: python -m pytest tests/test_native.py -q -ra
|
|
25
|
+
- run: python -m build
|
|
26
|
+
dependency-compatibility:
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@v4
|
|
30
|
+
- uses: actions/setup-python@v5
|
|
31
|
+
with:
|
|
32
|
+
python-version: "3.13"
|
|
33
|
+
- run: python -m pip install -e ".[dev,all]"
|
|
34
|
+
- run: python -m pytest tests/test_real_dask.py tests/test_real_processkit.py -q -ra
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 — 2026-09-20
|
|
4
|
+
|
|
5
|
+
Initial alpha implementation: native Windows low/high memory-resource notification
|
|
6
|
+
backend; Linux PSI trigger backend with explicit stall-time policy and multiple
|
|
7
|
+
scopes; macOS libdispatch backend with native semantic state bootstrap; sync and
|
|
8
|
+
async admission leases; explicit sensor/containment failures; public-API Dask
|
|
9
|
+
submission and bounded streaming; optional processkit containment facade; CLI,
|
|
10
|
+
examples, deterministic tests, and opt-in real integration smoke tests.
|
|
11
|
+
|
|
12
|
+
This source/wheel delivery is not a published PyPI release. The test report
|
|
13
|
+
separates local deterministic validation from native/third-party validation.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Resource Pressure 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.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
include LICENSE README.md CHANGELOG.md SECURITY.md
|
|
2
|
+
recursive-include docs *.md *.txt *.xml
|
|
3
|
+
recursive-include examples *.py
|
|
4
|
+
recursive-include tests *.py
|
|
5
|
+
recursive-include tools *.py
|
|
6
|
+
include .github/workflows/ci.yml
|
|
7
|
+
recursive-include src/resource_pressure py.typed
|
|
8
|
+
prune build
|
|
9
|
+
prune dist
|
|
10
|
+
global-exclude *.pyc *.pyo __pycache__
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: resource-pressure
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Native memory-pressure signals, bounded admission, and optional kernel-backed process containment
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/adnahmed/resource-pressure
|
|
7
|
+
Project-URL: Repository, https://github.com/adnahmed/resource-pressure
|
|
8
|
+
Project-URL: Issues, https://github.com/adnahmed/resource-pressure/issues
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
12
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
13
|
+
Classifier: Operating System :: MacOS
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Provides-Extra: containment
|
|
19
|
+
Requires-Dist: processkit-py<2,>=1.0; extra == "containment"
|
|
20
|
+
Provides-Extra: dask
|
|
21
|
+
Requires-Dist: distributed>=2025.1; extra == "dask"
|
|
22
|
+
Provides-Extra: all
|
|
23
|
+
Requires-Dist: processkit-py<2,>=1.0; extra == "all"
|
|
24
|
+
Requires-Dist: distributed>=2025.1; extra == "all"
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
27
|
+
Requires-Dist: build>=1; extra == "dev"
|
|
28
|
+
Requires-Dist: ruff>=0.11; extra == "dev"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# resource-pressure
|
|
32
|
+
|
|
33
|
+
**Native memory-pressure signals → bounded task admission → optional native process containment.**
|
|
34
|
+
|
|
35
|
+
Python 3.10+. The monitoring/admission core uses only the standard library.
|
|
36
|
+
No `psutil`, RAM percentages, free-memory reserves, per-worker memory estimates,
|
|
37
|
+
or adaptive thread-pool algorithm. No GLib, PyGObject, MSYS2, or compiler is
|
|
38
|
+
needed for the core wheel.
|
|
39
|
+
|
|
40
|
+
**Version 0.1.0 is an alpha implementation, not a production-validated release.**
|
|
41
|
+
The supplied source and wheel have not been published to PyPI. Use the local
|
|
42
|
+
paths below; the distribution name is not a claim of ownership of a PyPI name.
|
|
43
|
+
See `docs/TEST_REPORT.md` for what actually ran and what remains unverified.
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
Core:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install resource-pressure
|
|
51
|
+
```
|
|
52
|
+
or:
|
|
53
|
+
```bash
|
|
54
|
+
uv add resource-pressure
|
|
55
|
+
```
|
|
56
|
+
With Dask + process containment:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install "resource-pressure[all]"
|
|
60
|
+
```
|
|
61
|
+
or:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
uv add "resource-pressure[all]"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
After extracting the archive, from its parent directory:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# Core native sensors and admission.
|
|
71
|
+
uv add ./resource-pressure
|
|
72
|
+
|
|
73
|
+
# Also install Dask and the processkit containment adapter dependencies.
|
|
74
|
+
uv add ./resource-pressure --extra all
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
For an editable source checkout:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
uv add --editable ./resource-pressure --extra all
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Or install the supplied universal core wheel:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
python -m pip install ./resource_pressure-0.1.0-py3-none-any.whl
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The wheel itself has no mandatory runtime dependencies. The optional
|
|
90
|
+
`containment` extra uses `processkit-py>=1.0,<2`; its own native wheel availability
|
|
91
|
+
and platform restrictions still apply. The `dask` extra installs `distributed`.
|
|
92
|
+
|
|
93
|
+
## The API
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
import asyncio
|
|
97
|
+
from resource_pressure import PressureGovernor
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
async def main():
|
|
101
|
+
async with PressureGovernor.auto(max_in_flight=4) as governor:
|
|
102
|
+
async with governor.slot():
|
|
103
|
+
await perform_one_heavy_operation()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# asyncio.run(main()) # Define your own perform_one_heavy_operation first.
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Synchronous code uses the same context-manager surface:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from resource_pressure import PressureGovernor
|
|
113
|
+
|
|
114
|
+
with PressureGovernor.auto(max_in_flight=4) as governor:
|
|
115
|
+
with governor.slot(timeout=30):
|
|
116
|
+
result = perform_one_heavy_operation()
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
A runnable simulation with no native prerequisites is included:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
uv run python examples/simulated_demo.py
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`max_in_flight` is a fixed ceiling on admitted operations, **not** a RAM threshold.
|
|
126
|
+
When omitted it defaults to the available process CPU count (or CPU count on
|
|
127
|
+
older Python). It is an operational concurrency limit, not a safe-memory estimate.
|
|
128
|
+
|
|
129
|
+
The admission rule is intentionally small:
|
|
130
|
+
|
|
131
|
+
```text
|
|
132
|
+
UNKNOWN → admit nothing while initializing
|
|
133
|
+
NORMAL → admit up to max_in_flight operations
|
|
134
|
+
PRESSURED → admit nothing new; existing operations finish
|
|
135
|
+
CRITICAL → admit nothing new; existing operations finish
|
|
136
|
+
sensor error / closed governor → wake waiters and raise
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Recovery reopens admission to the same ceiling. The package does not kill,
|
|
140
|
+
suspend, shrink, or restart already-running tasks on a pressure transition.
|
|
141
|
+
It does not calculate an optimal worker count.
|
|
142
|
+
|
|
143
|
+
For observation without reserving a slot:
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
await governor.wait_until_safe()
|
|
147
|
+
# Or governor.wait_until_safe_sync() in blocking code.
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**That call is a state check, not a reservation.** Use `slot()` or
|
|
151
|
+
`acquire()`/`acquire_sync()` when admitting work. Hold a lease until the work
|
|
152
|
+
finishes; releasing immediately after submission defeats the bound.
|
|
153
|
+
|
|
154
|
+
## What each platform actually does
|
|
155
|
+
|
|
156
|
+
| Platform | Pressure sensor | Native states and mapping | Containment extra |
|
|
157
|
+
|---|---|---|---|
|
|
158
|
+
| Windows | `CreateMemoryResourceNotification`, query, native wait | High → NORMAL; low → PRESSURED; no fabricated CRITICAL tier | Job Objects through processkit |
|
|
159
|
+
| Linux | PSI trigger registration and `poll(POLLPRI)` | Library maps `some` to PRESSURED and `full` to CRITICAL | cgroup v2 through processkit, when available |
|
|
160
|
+
| macOS | libdispatch memory-pressure source | Native normal/warn/critical → NORMAL/PRESSURED/CRITICAL | POSIX process groups only, explicit opt-in; no whole-tree memory cap |
|
|
161
|
+
|
|
162
|
+
OS details are documented in the primary references in `docs/SOURCES.md`.
|
|
163
|
+
The levels are useful common labels, **not calibrated equivalents across OSes**.
|
|
164
|
+
|
|
165
|
+
### Linux: honest policy, not pretend OS severity
|
|
166
|
+
|
|
167
|
+
PSI reports memory-related execution stalls and accepts user-specified stall-time
|
|
168
|
+
triggers. Linux does **not** label these as NORMAL/PRESSURED/CRITICAL for us.
|
|
169
|
+
The library registers these defaults, on separate file descriptors:
|
|
170
|
+
|
|
171
|
+
```text
|
|
172
|
+
some 150000 2000000
|
|
173
|
+
full 50000 2000000
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
These are **library-selected defaults**: 150 ms of some-stall or 50 ms of full-stall
|
|
177
|
+
within a two-second window. They are not percentages of RAM and are not asserted
|
|
178
|
+
to be optimal for every workload. PSI has no recovery event; this implementation
|
|
179
|
+
infers recovery after four seconds without either trigger. CRITICAL is retained
|
|
180
|
+
until its own quiet interval expires, even if less severe events arrive.
|
|
181
|
+
Initial admission waits through one observation window.
|
|
182
|
+
|
|
183
|
+
Application code needs no RAM policy, but the underlying Linux policy cannot be
|
|
184
|
+
eliminated. It is explicit and configurable:
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
from resource_pressure import PSIConfig, PressureGovernor
|
|
188
|
+
|
|
189
|
+
governor = PressureGovernor.auto(
|
|
190
|
+
psi=PSIConfig(
|
|
191
|
+
some_stall_us=150_000, full_stall_us=50_000, window_us=2_000_000, quiet_seconds=4.0
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Default scope is `/proc/pressure/memory` only. To monitor a deployment-provided
|
|
197
|
+
cgroup and the host, explicitly supply both paths:
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
governor = PressureGovernor.auto(
|
|
201
|
+
psi_paths=[
|
|
202
|
+
"/proc/pressure/memory",
|
|
203
|
+
"/sys/fs/cgroup/YOUR_DELEGATED_GROUP/memory.pressure",
|
|
204
|
+
]
|
|
205
|
+
)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
All requested paths must support writable PSI triggers. Merely reading PSI
|
|
209
|
+
counters is insufficient for this event-only backend. Missing PSI, read-only
|
|
210
|
+
mounts, permission failures, or disappearing cgroups raise errors; there is no
|
|
211
|
+
fallback to polling RAM or silently dropping a requested scope. Two-second
|
|
212
|
+
window multiples satisfy the kernel's unprivileged window rule, but do not grant
|
|
213
|
+
filesystem permissions. Administrators should provision access rather than
|
|
214
|
+
running an entire application as root just for the sensor.
|
|
215
|
+
|
|
216
|
+
**A system-wide signal does not guarantee warning before a cgroup-specific
|
|
217
|
+
memory limit is hit.** Per-cgroup sensing is explicit; the processkit adapter
|
|
218
|
+
does not discover or export its private cgroup path into the sensor automatically.
|
|
219
|
+
|
|
220
|
+
### Windows: use the OS's decision
|
|
221
|
+
|
|
222
|
+
The backend waits for low-memory while NORMAL, and for high-memory after
|
|
223
|
+
pressure. It does not wait repeatedly on an already-signalled handle. The
|
|
224
|
+
neutral region where neither object is signalled retains the previous decision;
|
|
225
|
+
at startup, a neutral state conservatively blocks until high is observed.
|
|
226
|
+
Timeouts only allow orderly thread shutdown; they do not sample RAM.
|
|
227
|
+
|
|
228
|
+
### macOS: native events, with a bootstrap qualification
|
|
229
|
+
|
|
230
|
+
libdispatch supplies normal, warning, and critical events. A native semantic
|
|
231
|
+
sysctl, `kern.memorystatus_vm_pressure_level`, bootstraps initial state and resolves
|
|
232
|
+
coalesced events whose ordering is ambiguous. This sysctl exists in Apple's
|
|
233
|
+
published XNU implementation, but is **not a promised stable public API**.
|
|
234
|
+
Failure to read it makes startup fail explicitly. The ctypes callback lifetime,
|
|
235
|
+
cancellation, and queue-barrier cleanup still require actual macOS validation.
|
|
236
|
+
|
|
237
|
+
## Dask
|
|
238
|
+
|
|
239
|
+
Keep admission in the **producer**, before submitting independent heavy tasks.
|
|
240
|
+
The supplied adapter uses public `Client.submit` and Future APIs, not scheduler
|
|
241
|
+
internals or worker monkey-patches. Keep Dask's existing memory protections.
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
from contextlib import closing
|
|
245
|
+
from distributed import Client, LocalCluster
|
|
246
|
+
from resource_pressure import PressureGovernor
|
|
247
|
+
from resource_pressure.integrations.dask import DaskAdmission
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def heavy_task(item):
|
|
251
|
+
return item * item
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def main():
|
|
255
|
+
with LocalCluster(n_workers=4, threads_per_worker=1) as cluster:
|
|
256
|
+
with Client(cluster) as client, PressureGovernor.auto(max_in_flight=4) as governor:
|
|
257
|
+
admitted = DaskAdmission(client, governor)
|
|
258
|
+
with closing(admitted.map_unordered(heavy_task, range(100))) as results:
|
|
259
|
+
for result in results:
|
|
260
|
+
print(result)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
if __name__ == "__main__":
|
|
264
|
+
main()
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
`map_unordered` is the recommended bounded streaming interface. It retains at
|
|
268
|
+
most `max_in_flight` submitted futures plus one input lookahead, drains completed
|
|
269
|
+
results even during pressure, and releases futures as results are consumed.
|
|
270
|
+
Closing the iterator cancels/releases the remaining futures and releases leases.
|
|
271
|
+
Large results retained by your application are still your application's memory.
|
|
272
|
+
Input iteration should be cheap; one lookahead can happen while admission is shut.
|
|
273
|
+
|
|
274
|
+
`submit()` and `await asubmit()` return ordinary Dask futures. Their logical lease
|
|
275
|
+
lasts until success, failure, or cancellation. They bound unfinished admitted
|
|
276
|
+
futures, **not** an ever-growing list of completed results retained by the caller.
|
|
277
|
+
Use the streaming API rather than building a giant list of futures.
|
|
278
|
+
|
|
279
|
+
Important boundaries:
|
|
280
|
+
|
|
281
|
+
- This sensor observes the producer's machine/configured PSI scopes, not every
|
|
282
|
+
remote worker. Multi-host global pressure aggregation is not implemented.
|
|
283
|
+
- It does not resize Dask workers, gate existing queued graphs, cover submissions
|
|
284
|
+
through another client, or govern arbitrary dependent graph nodes.
|
|
285
|
+
- Future cancellation does not guarantee that already-running Python code stops.
|
|
286
|
+
Thus the bound is on logical admissions, not an inviolable physical-work count
|
|
287
|
+
after cancellation or worker failure.
|
|
288
|
+
- A completed remote task can retain data. A pressure gate is not proof of freed
|
|
289
|
+
RAM; release results and keep Dask's spill/pause/termination mechanisms enabled.
|
|
290
|
+
|
|
291
|
+
Never put a blocking governor inside every Dask worker task indiscriminately;
|
|
292
|
+
blocked workers can occupy the very slots needed by dependencies that would
|
|
293
|
+
finish and free memory.
|
|
294
|
+
|
|
295
|
+
## Unified containment
|
|
296
|
+
|
|
297
|
+
The same governor can create a pressure-gated process runner:
|
|
298
|
+
|
|
299
|
+
```python
|
|
300
|
+
import sys
|
|
301
|
+
from resource_pressure import PressureGovernor
|
|
302
|
+
|
|
303
|
+
with PressureGovernor.auto(max_in_flight=2) as governor:
|
|
304
|
+
with governor.process_group(max_memory=512 * 1024 * 1024) as children:
|
|
305
|
+
print(children.mechanism) # must be job_object or cgroup_v2 by default
|
|
306
|
+
result = children.run(sys.executable, ["-c", "print('hello')"], timeout=20)
|
|
307
|
+
print(result.stdout)
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
`run()`/`await arun()` acquire admission before process execution and release it
|
|
311
|
+
when the command's output operation completes. They return a
|
|
312
|
+
`processkit.ProcessResult`; nonzero exit codes are data and should be checked.
|
|
313
|
+
Captured output is bounded to 1 MiB by default (`output_limit=` changes this).
|
|
314
|
+
Arguments are passed as a sequence, never implicitly through a shell.
|
|
315
|
+
|
|
316
|
+
`max_memory` is an **optional, explicit deployment budget** for the whole group,
|
|
317
|
+
not automatically chosen from system RAM. Omit it for lifecycle containment
|
|
318
|
+
without a memory cap. `max_processes` is also optional. Fixed limits are independent
|
|
319
|
+
of pressure signals; no automatic `memory.high` tuning is performed.
|
|
320
|
+
|
|
321
|
+
The adapter rejects process-group fallback unless
|
|
322
|
+
`allow_process_group_fallback=True` is explicit. Such fallback cannot enforce the
|
|
323
|
+
requested whole-tree memory/process limits. It is not a security sandbox, and
|
|
324
|
+
session-changing descendants can escape POSIX process groups.
|
|
325
|
+
|
|
326
|
+
**Only children started through this runner are covered.** Merely constructing a
|
|
327
|
+
group does not contain your existing process, browser, or Dask cluster.
|
|
328
|
+
To contain Dask's hierarchy, launch the supervisor inside a group **before** it
|
|
329
|
+
creates workers. `examples/contained_dask_launcher.py` demonstrates that outer
|
|
330
|
+
boundary; its child runs `examples/dask_local.py` with per-task admission.
|
|
331
|
+
Already-running workers are not adopted automatically. Finish all concurrent
|
|
332
|
+
`run`/`arun` calls before leaving the group context.
|
|
333
|
+
|
|
334
|
+
A root command can exit while descendants remain; the shared group's final
|
|
335
|
+
context exit handles its owned tree. An admission lease does not count every
|
|
336
|
+
remaining descendant. Teardown and abrupt-parent-death guarantees are those of
|
|
337
|
+
the reported processkit mechanism; do not assume Linux/macOS match Windows.
|
|
338
|
+
|
|
339
|
+
## Diagnostics and events
|
|
340
|
+
|
|
341
|
+
```bash
|
|
342
|
+
uv run resource-pressure doctor
|
|
343
|
+
uv run resource-pressure watch
|
|
344
|
+
uv run resource-pressure doctor --psi-path /path/to/delegated/memory.pressure
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
`doctor` registers the actual native monitor, reports its backend and initial
|
|
348
|
+
state, and closes it. It does not claim to test containment or induce pressure.
|
|
349
|
+
Missing/failed native support exits with code 2, not a fake NORMAL status.
|
|
350
|
+
|
|
351
|
+
```python
|
|
352
|
+
unsubscribe = governor.subscribe(lambda event: print(event.level.name, event.backend, event.reason))
|
|
353
|
+
# Later: unsubscribe()
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Subscribers run on the monitor thread and must be short/nonblocking. Exceptions
|
|
357
|
+
are logged and isolated. Use `loop.call_soon_threadsafe` to notify an asyncio
|
|
358
|
+
loop; do not perform expensive memory reclamation inside the callback.
|
|
359
|
+
Subscribers receive level transitions, not a durable audit log. Call
|
|
360
|
+
`check_health()` for explicit status; admission methods automatically check it.
|
|
361
|
+
|
|
362
|
+
## Development and validation
|
|
363
|
+
|
|
364
|
+
```bash
|
|
365
|
+
uv sync --extra dev
|
|
366
|
+
uv run pytest -q -ra
|
|
367
|
+
uv build
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
Or with an existing Python environment:
|
|
371
|
+
|
|
372
|
+
```bash
|
|
373
|
+
python -m pip install -e ".[dev]"
|
|
374
|
+
python -m pytest -q -ra
|
|
375
|
+
python -m build
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
The included CI workflow defines Linux, Windows, and macOS jobs. It has **not**
|
|
379
|
+
been run on hosted CI as part of this delivery. Real sensor tests require
|
|
380
|
+
`RESOURCE_PRESSURE_NATIVE=1`; dependency tests run only when their extras are
|
|
381
|
+
installed. No automated test deliberately exhausts host memory.
|
|
382
|
+
|
|
383
|
+
Read `docs/ARCHITECTURE.md`, `docs/TEST_REPORT.md`, and `docs/SOURCES.md` before
|
|
384
|
+
production deployment. This is a pressure-responsive admission library, **not an
|
|
385
|
+
OOM-proof memory manager or a security boundary**.
|