priomesh-python 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.
- priomesh_python-0.1.0/LICENSE +21 -0
- priomesh_python-0.1.0/PKG-INFO +181 -0
- priomesh_python-0.1.0/README.md +140 -0
- priomesh_python-0.1.0/pyproject.toml +30 -0
- priomesh_python-0.1.0/setup.cfg +4 -0
- priomesh_python-0.1.0/src/priomesh/__init__.py +26 -0
- priomesh_python-0.1.0/src/priomesh/models.py +24 -0
- priomesh_python-0.1.0/src/priomesh/policies/__init__.py +4 -0
- priomesh_python-0.1.0/src/priomesh/policies/priority.py +30 -0
- priomesh_python-0.1.0/src/priomesh/policies/subqueue.py +52 -0
- priomesh_python-0.1.0/src/priomesh/queue.py +88 -0
- priomesh_python-0.1.0/src/priomesh_python.egg-info/PKG-INFO +181 -0
- priomesh_python-0.1.0/src/priomesh_python.egg-info/SOURCES.txt +15 -0
- priomesh_python-0.1.0/src/priomesh_python.egg-info/dependency_links.txt +1 -0
- priomesh_python-0.1.0/src/priomesh_python.egg-info/top_level.txt +1 -0
- priomesh_python-0.1.0/tests/test_policy.py +161 -0
- priomesh_python-0.1.0/tests/test_queue.py +218 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 PrioMesh 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,181 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: priomesh-python
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A pluggable priority and fair-queue scheduling library
|
|
5
|
+
License: MIT License
|
|
6
|
+
|
|
7
|
+
Copyright (c) 2026 PrioMesh contributors
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
|
26
|
+
|
|
27
|
+
Classifier: Development Status :: 3 - Alpha
|
|
28
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
29
|
+
Classifier: Operating System :: OS Independent
|
|
30
|
+
Classifier: Programming Language :: Python :: 3
|
|
31
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
32
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
33
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
34
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
36
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
37
|
+
Requires-Python: >=3.10
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
License-File: LICENSE
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
|
|
42
|
+
# PrioMesh
|
|
43
|
+
|
|
44
|
+
PrioMesh is an early-stage Python library for priority-based task scheduling
|
|
45
|
+
across named sub-queues. It provides pluggable priority and sub-queue policies.
|
|
46
|
+
|
|
47
|
+
The current implementation is an in-memory, single-process prototype. Durable
|
|
48
|
+
storage, workers, and distributed execution are not implemented yet.
|
|
49
|
+
|
|
50
|
+
## Requirements
|
|
51
|
+
|
|
52
|
+
- Python 3.10 or newer
|
|
53
|
+
|
|
54
|
+
## Install
|
|
55
|
+
|
|
56
|
+
For development from a local checkout:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
git clone https://github.com/PrioMesh/priomesh-python.git
|
|
60
|
+
cd priomesh-python
|
|
61
|
+
python -m pip install -e .
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Once the project is published to PyPI, end users will be able to install it
|
|
65
|
+
with:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
python -m pip install priomesh-python
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Until then, users can install directly from the GitHub repository:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
python -m pip install "git+https://github.com/PrioMesh/priomesh-python.git"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Quick start
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
import asyncio
|
|
81
|
+
|
|
82
|
+
from priomesh import Job, Priority, PriorityFairQueue, QueueShutDown
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
async def main() -> None:
|
|
86
|
+
queue = PriorityFairQueue()
|
|
87
|
+
|
|
88
|
+
async def consume() -> None:
|
|
89
|
+
try:
|
|
90
|
+
while True:
|
|
91
|
+
job = await queue.get()
|
|
92
|
+
print(job)
|
|
93
|
+
except QueueShutDown:
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
consumer = asyncio.create_task(consume())
|
|
97
|
+
await queue.put(Job("job-1", Priority.HIGH, "tenant-a"))
|
|
98
|
+
await queue.put(Job("job-2", Priority.HIGH, "tenant-b"))
|
|
99
|
+
await queue.shutdown()
|
|
100
|
+
await consumer
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
asyncio.run(main())
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`get()` first selects an active priority level, then selects an active
|
|
107
|
+
sub-queue within that level. Jobs within each priority/sub-queue pair are
|
|
108
|
+
returned in FIFO order. Calling `get()` on an empty queue waits until a job is
|
|
109
|
+
put; `empty()` checks whether the queue currently has any jobs.
|
|
110
|
+
|
|
111
|
+
Call `shutdown()` to stop accepting jobs. It performs a graceful shutdown:
|
|
112
|
+
`put()` raises `QueueShutDown`, while `get()` continues returning jobs that
|
|
113
|
+
were already queued. Once the queue is drained, waiting and future `get()`
|
|
114
|
+
calls raise `QueueShutDown`, allowing consumers to exit. The example above
|
|
115
|
+
shows a consumer draining the queue and stopping cleanly.
|
|
116
|
+
|
|
117
|
+
## Scheduling policies
|
|
118
|
+
|
|
119
|
+
Priority and sub-queue policies are independent and can be combined:
|
|
120
|
+
|
|
121
|
+
| Policy | Behavior | Use when |
|
|
122
|
+
| --- | --- | --- |
|
|
123
|
+
| `RandomPriorityPolicy` (default) | Randomly selects an active priority, weighting higher priorities more heavily. With all four levels active, the weights are 4:3:2:1 from `CRITICAL` to `LOW`. | Higher priorities should usually run sooner, while lower priorities should still be eligible. |
|
|
124
|
+
| `AbsolutePriorityPolicy` | Always selects the highest-priority active level. | Priority order must be strict. A continuously active higher priority can indefinitely delay lower priorities. |
|
|
125
|
+
| `AgingWeightedRandomSubQueuePolicy` (default) | Randomly selects a sub-queue with weight `sqrt(pending_jobs) * (waiting_rounds + 1)`. | Larger backlogs should receive more opportunities, while sub-queues skipped in prior selections gain weight. |
|
|
126
|
+
| `FifoSubQueuePolicy` | Selects the sub-queue at the front of the rotation; a sub-queue with remaining work goes to the back after serving one job. | Predictable round-robin service among active sub-queues at the selected priority is preferred. |
|
|
127
|
+
| `RandomSubQueuePolicy` | Selects uniformly at random among active sub-queues. | Sub-queues should have equal selection probability, regardless of backlog. |
|
|
128
|
+
|
|
129
|
+
The aging policy is probabilistic: its weights affect relative selection
|
|
130
|
+
chances, but it does not guarantee a fixed service ratio or a maximum wait.
|
|
131
|
+
`waiting_rounds` counts selections of other sub-queues at the same priority,
|
|
132
|
+
not elapsed time. When a sub-queue is selected, its waiting count resets.
|
|
133
|
+
|
|
134
|
+
Policies can be replaced when constructing the queue:
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from priomesh import (
|
|
138
|
+
AbsolutePriorityPolicy,
|
|
139
|
+
AgingWeightedRandomSubQueuePolicy,
|
|
140
|
+
FifoSubQueuePolicy,
|
|
141
|
+
PriorityFairQueue,
|
|
142
|
+
RandomPriorityPolicy,
|
|
143
|
+
RandomSubQueuePolicy,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Strict priority order, with deterministic round-robin among sub-queues.
|
|
147
|
+
queue = PriorityFairQueue(
|
|
148
|
+
priority_policy=AbsolutePriorityPolicy(),
|
|
149
|
+
subqueue_policy=FifoSubQueuePolicy(),
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Probabilistic priority and uniform random sub-queue selection.
|
|
153
|
+
queue = PriorityFairQueue(
|
|
154
|
+
priority_policy=RandomPriorityPolicy(),
|
|
155
|
+
subqueue_policy=RandomSubQueuePolicy(),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# These are also the defaults; this is equivalent to PriorityFairQueue().
|
|
159
|
+
queue = PriorityFairQueue(
|
|
160
|
+
priority_policy=RandomPriorityPolicy(),
|
|
161
|
+
subqueue_policy=AgingWeightedRandomSubQueuePolicy(),
|
|
162
|
+
)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Run the example from a checkout
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
python -m pip install -e .
|
|
169
|
+
python examples/basic.py
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Project status
|
|
173
|
+
|
|
174
|
+
PrioMesh is under active early development. The current queue is in-memory and
|
|
175
|
+
does not provide persistence, worker execution, retries, or cross-process
|
|
176
|
+
scheduling. A Go implementation is planned after the scheduling semantics
|
|
177
|
+
stabilize.
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
|
|
181
|
+
PrioMesh is licensed under the MIT License. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# PrioMesh
|
|
2
|
+
|
|
3
|
+
PrioMesh is an early-stage Python library for priority-based task scheduling
|
|
4
|
+
across named sub-queues. It provides pluggable priority and sub-queue policies.
|
|
5
|
+
|
|
6
|
+
The current implementation is an in-memory, single-process prototype. Durable
|
|
7
|
+
storage, workers, and distributed execution are not implemented yet.
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
- Python 3.10 or newer
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
For development from a local checkout:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
git clone https://github.com/PrioMesh/priomesh-python.git
|
|
19
|
+
cd priomesh-python
|
|
20
|
+
python -m pip install -e .
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Once the project is published to PyPI, end users will be able to install it
|
|
24
|
+
with:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
python -m pip install priomesh-python
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Until then, users can install directly from the GitHub repository:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
python -m pip install "git+https://github.com/PrioMesh/priomesh-python.git"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
import asyncio
|
|
40
|
+
|
|
41
|
+
from priomesh import Job, Priority, PriorityFairQueue, QueueShutDown
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async def main() -> None:
|
|
45
|
+
queue = PriorityFairQueue()
|
|
46
|
+
|
|
47
|
+
async def consume() -> None:
|
|
48
|
+
try:
|
|
49
|
+
while True:
|
|
50
|
+
job = await queue.get()
|
|
51
|
+
print(job)
|
|
52
|
+
except QueueShutDown:
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
consumer = asyncio.create_task(consume())
|
|
56
|
+
await queue.put(Job("job-1", Priority.HIGH, "tenant-a"))
|
|
57
|
+
await queue.put(Job("job-2", Priority.HIGH, "tenant-b"))
|
|
58
|
+
await queue.shutdown()
|
|
59
|
+
await consumer
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
asyncio.run(main())
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`get()` first selects an active priority level, then selects an active
|
|
66
|
+
sub-queue within that level. Jobs within each priority/sub-queue pair are
|
|
67
|
+
returned in FIFO order. Calling `get()` on an empty queue waits until a job is
|
|
68
|
+
put; `empty()` checks whether the queue currently has any jobs.
|
|
69
|
+
|
|
70
|
+
Call `shutdown()` to stop accepting jobs. It performs a graceful shutdown:
|
|
71
|
+
`put()` raises `QueueShutDown`, while `get()` continues returning jobs that
|
|
72
|
+
were already queued. Once the queue is drained, waiting and future `get()`
|
|
73
|
+
calls raise `QueueShutDown`, allowing consumers to exit. The example above
|
|
74
|
+
shows a consumer draining the queue and stopping cleanly.
|
|
75
|
+
|
|
76
|
+
## Scheduling policies
|
|
77
|
+
|
|
78
|
+
Priority and sub-queue policies are independent and can be combined:
|
|
79
|
+
|
|
80
|
+
| Policy | Behavior | Use when |
|
|
81
|
+
| --- | --- | --- |
|
|
82
|
+
| `RandomPriorityPolicy` (default) | Randomly selects an active priority, weighting higher priorities more heavily. With all four levels active, the weights are 4:3:2:1 from `CRITICAL` to `LOW`. | Higher priorities should usually run sooner, while lower priorities should still be eligible. |
|
|
83
|
+
| `AbsolutePriorityPolicy` | Always selects the highest-priority active level. | Priority order must be strict. A continuously active higher priority can indefinitely delay lower priorities. |
|
|
84
|
+
| `AgingWeightedRandomSubQueuePolicy` (default) | Randomly selects a sub-queue with weight `sqrt(pending_jobs) * (waiting_rounds + 1)`. | Larger backlogs should receive more opportunities, while sub-queues skipped in prior selections gain weight. |
|
|
85
|
+
| `FifoSubQueuePolicy` | Selects the sub-queue at the front of the rotation; a sub-queue with remaining work goes to the back after serving one job. | Predictable round-robin service among active sub-queues at the selected priority is preferred. |
|
|
86
|
+
| `RandomSubQueuePolicy` | Selects uniformly at random among active sub-queues. | Sub-queues should have equal selection probability, regardless of backlog. |
|
|
87
|
+
|
|
88
|
+
The aging policy is probabilistic: its weights affect relative selection
|
|
89
|
+
chances, but it does not guarantee a fixed service ratio or a maximum wait.
|
|
90
|
+
`waiting_rounds` counts selections of other sub-queues at the same priority,
|
|
91
|
+
not elapsed time. When a sub-queue is selected, its waiting count resets.
|
|
92
|
+
|
|
93
|
+
Policies can be replaced when constructing the queue:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from priomesh import (
|
|
97
|
+
AbsolutePriorityPolicy,
|
|
98
|
+
AgingWeightedRandomSubQueuePolicy,
|
|
99
|
+
FifoSubQueuePolicy,
|
|
100
|
+
PriorityFairQueue,
|
|
101
|
+
RandomPriorityPolicy,
|
|
102
|
+
RandomSubQueuePolicy,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Strict priority order, with deterministic round-robin among sub-queues.
|
|
106
|
+
queue = PriorityFairQueue(
|
|
107
|
+
priority_policy=AbsolutePriorityPolicy(),
|
|
108
|
+
subqueue_policy=FifoSubQueuePolicy(),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Probabilistic priority and uniform random sub-queue selection.
|
|
112
|
+
queue = PriorityFairQueue(
|
|
113
|
+
priority_policy=RandomPriorityPolicy(),
|
|
114
|
+
subqueue_policy=RandomSubQueuePolicy(),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# These are also the defaults; this is equivalent to PriorityFairQueue().
|
|
118
|
+
queue = PriorityFairQueue(
|
|
119
|
+
priority_policy=RandomPriorityPolicy(),
|
|
120
|
+
subqueue_policy=AgingWeightedRandomSubQueuePolicy(),
|
|
121
|
+
)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Run the example from a checkout
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
python -m pip install -e .
|
|
128
|
+
python examples/basic.py
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Project status
|
|
132
|
+
|
|
133
|
+
PrioMesh is under active early development. The current queue is in-memory and
|
|
134
|
+
does not provide persistence, worker execution, retries, or cross-process
|
|
135
|
+
scheduling. A Go implementation is planned after the scheduling semantics
|
|
136
|
+
stabilize.
|
|
137
|
+
|
|
138
|
+
## License
|
|
139
|
+
|
|
140
|
+
PrioMesh is licensed under the MIT License. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "priomesh-python"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A pluggable priority and fair-queue scheduling library"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { file = "LICENSE" }
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Operating System :: OS Independent",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Topic :: System :: Distributed Computing",
|
|
23
|
+
]
|
|
24
|
+
dependencies = []
|
|
25
|
+
|
|
26
|
+
[tool.setuptools]
|
|
27
|
+
package-dir = { "" = "src" }
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.packages.find]
|
|
30
|
+
where = ["src"]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from .models import Job, Priority, SubQueue
|
|
2
|
+
from .policies import (
|
|
3
|
+
AbsolutePriorityPolicy,
|
|
4
|
+
AgingWeightedRandomSubQueuePolicy,
|
|
5
|
+
FifoSubQueuePolicy,
|
|
6
|
+
PriorityPolicy,
|
|
7
|
+
RandomPriorityPolicy,
|
|
8
|
+
RandomSubQueuePolicy,
|
|
9
|
+
SubQueuePolicy,
|
|
10
|
+
)
|
|
11
|
+
from .queue import PriorityFairQueue, QueueShutDown
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"AbsolutePriorityPolicy",
|
|
15
|
+
"AgingWeightedRandomSubQueuePolicy",
|
|
16
|
+
"FifoSubQueuePolicy",
|
|
17
|
+
"Job",
|
|
18
|
+
"Priority",
|
|
19
|
+
"PriorityFairQueue",
|
|
20
|
+
"QueueShutDown",
|
|
21
|
+
"PriorityPolicy",
|
|
22
|
+
"RandomPriorityPolicy",
|
|
23
|
+
"RandomSubQueuePolicy",
|
|
24
|
+
"SubQueue",
|
|
25
|
+
"SubQueuePolicy",
|
|
26
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Job, Priority, SubQueue
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from enum import IntEnum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Priority(IntEnum):
|
|
7
|
+
CRITICAL = 1
|
|
8
|
+
HIGH = 2
|
|
9
|
+
NORMAL = 3
|
|
10
|
+
LOW = 4
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class Job:
|
|
15
|
+
name: str
|
|
16
|
+
priority: Priority
|
|
17
|
+
subqueue: str = "default"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=False)
|
|
21
|
+
class SubQueue:
|
|
22
|
+
name: str
|
|
23
|
+
waiting_rounds: int
|
|
24
|
+
pending_jobs: int = 1
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import random
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
from typing import Protocol
|
|
4
|
+
|
|
5
|
+
from ..models import Priority
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PriorityPolicy(Protocol):
|
|
9
|
+
def choose(self, active_priorities: Sequence[Priority]) -> Priority:
|
|
10
|
+
"""Choose one currently active priority."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AbsolutePriorityPolicy:
|
|
14
|
+
"""Always choose the highest-priority active level."""
|
|
15
|
+
|
|
16
|
+
def choose(self, active_priorities: Sequence[Priority]) -> Priority:
|
|
17
|
+
return min(active_priorities)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RandomPriorityPolicy:
|
|
21
|
+
"""Randomly choose an active priority, favoring higher priorities."""
|
|
22
|
+
|
|
23
|
+
def choose(self, active_priorities: Sequence[Priority]) -> Priority:
|
|
24
|
+
ordered_priorities = sorted(active_priorities)
|
|
25
|
+
priority_weights = {
|
|
26
|
+
priority: len(ordered_priorities) - index
|
|
27
|
+
for index, priority in enumerate(ordered_priorities)
|
|
28
|
+
}
|
|
29
|
+
weights = [priority_weights[priority] for priority in active_priorities]
|
|
30
|
+
return random.choices(active_priorities, weights=weights, k=1)[0]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import random
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from typing import Protocol
|
|
5
|
+
from ..models import SubQueue
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SubQueuePolicy(Protocol):
|
|
9
|
+
def choose(self, subqueues: Sequence[SubQueue]) -> SubQueue:
|
|
10
|
+
"""Choose one currently active subqueue."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AgingWeightedRandomSubQueuePolicy:
|
|
14
|
+
"""Favor larger backlogs while increasing chances for waiting subqueues.
|
|
15
|
+
|
|
16
|
+
Each active subqueue's weight is sqrt(pending jobs) * (waiting rounds + 1).
|
|
17
|
+
The square root gives deeper queues more service opportunities without
|
|
18
|
+
making a small queue's chance negligible. Aging raises the weight of a
|
|
19
|
+
subqueue each time it is not selected and resets it when it is selected.
|
|
20
|
+
Selection remains probabilistic; this policy does not impose a wait limit.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def choose(self, subqueues: Sequence[SubQueue]) -> SubQueue:
|
|
24
|
+
weights = [
|
|
25
|
+
math.sqrt(subqueue.pending_jobs) * (subqueue.waiting_rounds + 1)
|
|
26
|
+
for subqueue in subqueues
|
|
27
|
+
]
|
|
28
|
+
selected = random.choices(
|
|
29
|
+
subqueues,
|
|
30
|
+
weights=weights,
|
|
31
|
+
k=1,
|
|
32
|
+
)[0]
|
|
33
|
+
for subqueue in subqueues:
|
|
34
|
+
if subqueue is selected:
|
|
35
|
+
subqueue.waiting_rounds = 0
|
|
36
|
+
else:
|
|
37
|
+
subqueue.waiting_rounds += 1
|
|
38
|
+
return selected
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class RandomSubQueuePolicy:
|
|
42
|
+
"""Choose an active subqueue uniformly at random."""
|
|
43
|
+
|
|
44
|
+
def choose(self, subqueues: Sequence[SubQueue]) -> SubQueue:
|
|
45
|
+
return random.choice(subqueues)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class FifoSubQueuePolicy:
|
|
49
|
+
"""Choose the subqueue at the front of the rotation."""
|
|
50
|
+
|
|
51
|
+
def choose(self, subqueues: Sequence[SubQueue]) -> SubQueue:
|
|
52
|
+
return subqueues[0]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from collections import deque
|
|
3
|
+
from .models import Priority, SubQueue, Job
|
|
4
|
+
|
|
5
|
+
from .policies import (
|
|
6
|
+
PriorityPolicy,
|
|
7
|
+
SubQueuePolicy,
|
|
8
|
+
RandomPriorityPolicy,
|
|
9
|
+
AgingWeightedRandomSubQueuePolicy
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class QueueShutDown(Exception):
|
|
14
|
+
"""Raised when an operation is attempted on a shut-down queue."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PriorityFairQueue:
|
|
18
|
+
"""Schedule jobs by priority and distribute service fairly among subqueues.
|
|
19
|
+
|
|
20
|
+
The aging-weighted-random policy increases a subqueue's selection weight
|
|
21
|
+
with each round it is not selected. This provides probabilistic fairness:
|
|
22
|
+
longer-waiting active subqueues become increasingly likely to be served.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
priority_policy: PriorityPolicy | None = None,
|
|
28
|
+
subqueue_policy: SubQueuePolicy | None = None,
|
|
29
|
+
) -> None:
|
|
30
|
+
self._priority_policy = priority_policy or RandomPriorityPolicy()
|
|
31
|
+
self._subqueue_policy = subqueue_policy or AgingWeightedRandomSubQueuePolicy()
|
|
32
|
+
self._queues: dict[Priority, dict[str, deque[Job]]] = {}
|
|
33
|
+
self._rotation: dict[Priority, list[SubQueue]] = {}
|
|
34
|
+
self._condition = asyncio.Condition()
|
|
35
|
+
self._shutdown = False
|
|
36
|
+
|
|
37
|
+
def empty(self) -> bool:
|
|
38
|
+
return not self._rotation
|
|
39
|
+
|
|
40
|
+
async def put(self, job: Job) -> None:
|
|
41
|
+
async with self._condition:
|
|
42
|
+
if self._shutdown:
|
|
43
|
+
raise QueueShutDown("cannot put into a shut-down queue")
|
|
44
|
+
level_queues = self._queues.setdefault(job.priority, {})
|
|
45
|
+
level_queues.setdefault(job.subqueue, deque()).append(job)
|
|
46
|
+
|
|
47
|
+
rotation = self._rotation.setdefault(job.priority, [])
|
|
48
|
+
subqueue = next(
|
|
49
|
+
(entry for entry in rotation if entry.name == job.subqueue),
|
|
50
|
+
None,
|
|
51
|
+
)
|
|
52
|
+
if subqueue is None:
|
|
53
|
+
rotation.append(SubQueue(name=job.subqueue, waiting_rounds=0))
|
|
54
|
+
else:
|
|
55
|
+
subqueue.pending_jobs += 1
|
|
56
|
+
self._condition.notify()
|
|
57
|
+
|
|
58
|
+
async def get(self) -> Job:
|
|
59
|
+
async with self._condition:
|
|
60
|
+
while True:
|
|
61
|
+
active_priorities = [p for p, r in self._rotation.items() if r]
|
|
62
|
+
if active_priorities:
|
|
63
|
+
priority = self._priority_policy.choose(active_priorities)
|
|
64
|
+
r = self._rotation[priority]
|
|
65
|
+
|
|
66
|
+
subqueue = self._subqueue_policy.choose(r)
|
|
67
|
+
r.remove(subqueue)
|
|
68
|
+
jobs = self._queues[priority][subqueue.name]
|
|
69
|
+
job = jobs.popleft()
|
|
70
|
+
subqueue.pending_jobs -= 1
|
|
71
|
+
if jobs:
|
|
72
|
+
r.append(subqueue)
|
|
73
|
+
else:
|
|
74
|
+
del self._queues[priority][subqueue.name]
|
|
75
|
+
if not r:
|
|
76
|
+
del self._queues[priority]
|
|
77
|
+
del self._rotation[priority]
|
|
78
|
+
# print(f"Processing job: {job.name}, priority: {job.priority}, subqueue: {job.subqueue}")
|
|
79
|
+
return job
|
|
80
|
+
|
|
81
|
+
if self._shutdown:
|
|
82
|
+
raise QueueShutDown("queue is shut down and empty")
|
|
83
|
+
await self._condition.wait()
|
|
84
|
+
|
|
85
|
+
async def shutdown(self) -> None:
|
|
86
|
+
async with self._condition:
|
|
87
|
+
self._shutdown = True
|
|
88
|
+
self._condition.notify_all() # 唤醒所有等待的协程,促使其检查关闭态并退出
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: priomesh-python
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A pluggable priority and fair-queue scheduling library
|
|
5
|
+
License: MIT License
|
|
6
|
+
|
|
7
|
+
Copyright (c) 2026 PrioMesh contributors
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
|
26
|
+
|
|
27
|
+
Classifier: Development Status :: 3 - Alpha
|
|
28
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
29
|
+
Classifier: Operating System :: OS Independent
|
|
30
|
+
Classifier: Programming Language :: Python :: 3
|
|
31
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
32
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
33
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
34
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
36
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
37
|
+
Requires-Python: >=3.10
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
License-File: LICENSE
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
|
|
42
|
+
# PrioMesh
|
|
43
|
+
|
|
44
|
+
PrioMesh is an early-stage Python library for priority-based task scheduling
|
|
45
|
+
across named sub-queues. It provides pluggable priority and sub-queue policies.
|
|
46
|
+
|
|
47
|
+
The current implementation is an in-memory, single-process prototype. Durable
|
|
48
|
+
storage, workers, and distributed execution are not implemented yet.
|
|
49
|
+
|
|
50
|
+
## Requirements
|
|
51
|
+
|
|
52
|
+
- Python 3.10 or newer
|
|
53
|
+
|
|
54
|
+
## Install
|
|
55
|
+
|
|
56
|
+
For development from a local checkout:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
git clone https://github.com/PrioMesh/priomesh-python.git
|
|
60
|
+
cd priomesh-python
|
|
61
|
+
python -m pip install -e .
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Once the project is published to PyPI, end users will be able to install it
|
|
65
|
+
with:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
python -m pip install priomesh-python
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Until then, users can install directly from the GitHub repository:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
python -m pip install "git+https://github.com/PrioMesh/priomesh-python.git"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Quick start
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
import asyncio
|
|
81
|
+
|
|
82
|
+
from priomesh import Job, Priority, PriorityFairQueue, QueueShutDown
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
async def main() -> None:
|
|
86
|
+
queue = PriorityFairQueue()
|
|
87
|
+
|
|
88
|
+
async def consume() -> None:
|
|
89
|
+
try:
|
|
90
|
+
while True:
|
|
91
|
+
job = await queue.get()
|
|
92
|
+
print(job)
|
|
93
|
+
except QueueShutDown:
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
consumer = asyncio.create_task(consume())
|
|
97
|
+
await queue.put(Job("job-1", Priority.HIGH, "tenant-a"))
|
|
98
|
+
await queue.put(Job("job-2", Priority.HIGH, "tenant-b"))
|
|
99
|
+
await queue.shutdown()
|
|
100
|
+
await consumer
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
asyncio.run(main())
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`get()` first selects an active priority level, then selects an active
|
|
107
|
+
sub-queue within that level. Jobs within each priority/sub-queue pair are
|
|
108
|
+
returned in FIFO order. Calling `get()` on an empty queue waits until a job is
|
|
109
|
+
put; `empty()` checks whether the queue currently has any jobs.
|
|
110
|
+
|
|
111
|
+
Call `shutdown()` to stop accepting jobs. It performs a graceful shutdown:
|
|
112
|
+
`put()` raises `QueueShutDown`, while `get()` continues returning jobs that
|
|
113
|
+
were already queued. Once the queue is drained, waiting and future `get()`
|
|
114
|
+
calls raise `QueueShutDown`, allowing consumers to exit. The example above
|
|
115
|
+
shows a consumer draining the queue and stopping cleanly.
|
|
116
|
+
|
|
117
|
+
## Scheduling policies
|
|
118
|
+
|
|
119
|
+
Priority and sub-queue policies are independent and can be combined:
|
|
120
|
+
|
|
121
|
+
| Policy | Behavior | Use when |
|
|
122
|
+
| --- | --- | --- |
|
|
123
|
+
| `RandomPriorityPolicy` (default) | Randomly selects an active priority, weighting higher priorities more heavily. With all four levels active, the weights are 4:3:2:1 from `CRITICAL` to `LOW`. | Higher priorities should usually run sooner, while lower priorities should still be eligible. |
|
|
124
|
+
| `AbsolutePriorityPolicy` | Always selects the highest-priority active level. | Priority order must be strict. A continuously active higher priority can indefinitely delay lower priorities. |
|
|
125
|
+
| `AgingWeightedRandomSubQueuePolicy` (default) | Randomly selects a sub-queue with weight `sqrt(pending_jobs) * (waiting_rounds + 1)`. | Larger backlogs should receive more opportunities, while sub-queues skipped in prior selections gain weight. |
|
|
126
|
+
| `FifoSubQueuePolicy` | Selects the sub-queue at the front of the rotation; a sub-queue with remaining work goes to the back after serving one job. | Predictable round-robin service among active sub-queues at the selected priority is preferred. |
|
|
127
|
+
| `RandomSubQueuePolicy` | Selects uniformly at random among active sub-queues. | Sub-queues should have equal selection probability, regardless of backlog. |
|
|
128
|
+
|
|
129
|
+
The aging policy is probabilistic: its weights affect relative selection
|
|
130
|
+
chances, but it does not guarantee a fixed service ratio or a maximum wait.
|
|
131
|
+
`waiting_rounds` counts selections of other sub-queues at the same priority,
|
|
132
|
+
not elapsed time. When a sub-queue is selected, its waiting count resets.
|
|
133
|
+
|
|
134
|
+
Policies can be replaced when constructing the queue:
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from priomesh import (
|
|
138
|
+
AbsolutePriorityPolicy,
|
|
139
|
+
AgingWeightedRandomSubQueuePolicy,
|
|
140
|
+
FifoSubQueuePolicy,
|
|
141
|
+
PriorityFairQueue,
|
|
142
|
+
RandomPriorityPolicy,
|
|
143
|
+
RandomSubQueuePolicy,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Strict priority order, with deterministic round-robin among sub-queues.
|
|
147
|
+
queue = PriorityFairQueue(
|
|
148
|
+
priority_policy=AbsolutePriorityPolicy(),
|
|
149
|
+
subqueue_policy=FifoSubQueuePolicy(),
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Probabilistic priority and uniform random sub-queue selection.
|
|
153
|
+
queue = PriorityFairQueue(
|
|
154
|
+
priority_policy=RandomPriorityPolicy(),
|
|
155
|
+
subqueue_policy=RandomSubQueuePolicy(),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# These are also the defaults; this is equivalent to PriorityFairQueue().
|
|
159
|
+
queue = PriorityFairQueue(
|
|
160
|
+
priority_policy=RandomPriorityPolicy(),
|
|
161
|
+
subqueue_policy=AgingWeightedRandomSubQueuePolicy(),
|
|
162
|
+
)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Run the example from a checkout
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
python -m pip install -e .
|
|
169
|
+
python examples/basic.py
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Project status
|
|
173
|
+
|
|
174
|
+
PrioMesh is under active early development. The current queue is in-memory and
|
|
175
|
+
does not provide persistence, worker execution, retries, or cross-process
|
|
176
|
+
scheduling. A Go implementation is planned after the scheduling semantics
|
|
177
|
+
stabilize.
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
|
|
181
|
+
PrioMesh is licensed under the MIT License. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/priomesh/__init__.py
|
|
5
|
+
src/priomesh/models.py
|
|
6
|
+
src/priomesh/queue.py
|
|
7
|
+
src/priomesh/policies/__init__.py
|
|
8
|
+
src/priomesh/policies/priority.py
|
|
9
|
+
src/priomesh/policies/subqueue.py
|
|
10
|
+
src/priomesh_python.egg-info/PKG-INFO
|
|
11
|
+
src/priomesh_python.egg-info/SOURCES.txt
|
|
12
|
+
src/priomesh_python.egg-info/dependency_links.txt
|
|
13
|
+
src/priomesh_python.egg-info/top_level.txt
|
|
14
|
+
tests/test_policy.py
|
|
15
|
+
tests/test_queue.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
priomesh
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import unittest
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from unittest.mock import patch
|
|
5
|
+
|
|
6
|
+
from priomesh import Job, Priority, PriorityFairQueue, SubQueue
|
|
7
|
+
from priomesh.policies import FifoSubQueuePolicy, AgingWeightedRandomSubQueuePolicy, RandomSubQueuePolicy
|
|
8
|
+
from priomesh.policies import AbsolutePriorityPolicy, RandomPriorityPolicy
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PriorityFairQueueTests(unittest.IsolatedAsyncioTestCase):
|
|
12
|
+
async def test_absolute_priority_policy(self) -> None:
|
|
13
|
+
policy = AbsolutePriorityPolicy()
|
|
14
|
+
queue = PriorityFairQueue(priority_policy=policy)
|
|
15
|
+
|
|
16
|
+
await queue.put(Job("job-1", Priority.NORMAL, "tenant-a"))
|
|
17
|
+
await queue.put(Job("job-2", Priority.NORMAL, "tenant-a"))
|
|
18
|
+
await queue.put(Job("job-3", Priority.HIGH, "tenant-a"))
|
|
19
|
+
await queue.put(Job("job-4", Priority.LOW, "tenant-a"))
|
|
20
|
+
|
|
21
|
+
self.assertEqual((await queue.get()).name, "job-3")
|
|
22
|
+
self.assertEqual((await queue.get()).name, "job-1")
|
|
23
|
+
self.assertEqual((await queue.get()).name, "job-2")
|
|
24
|
+
self.assertEqual((await queue.get()).name, "job-4")
|
|
25
|
+
|
|
26
|
+
self.assertTrue(queue.empty())
|
|
27
|
+
|
|
28
|
+
async def test_random_priority_policy(self) -> None:
|
|
29
|
+
policy = RandomPriorityPolicy()
|
|
30
|
+
queue = PriorityFairQueue(priority_policy=policy)
|
|
31
|
+
normal_jobs = [
|
|
32
|
+
Job(f"job-normal-{i}", Priority.NORMAL)
|
|
33
|
+
for i in range(1000)
|
|
34
|
+
]
|
|
35
|
+
high_jobs = [
|
|
36
|
+
Job(f"job-high-{i}", Priority.HIGH)
|
|
37
|
+
for i in range(1000)
|
|
38
|
+
]
|
|
39
|
+
for job in [*normal_jobs, *high_jobs]:
|
|
40
|
+
await queue.put(job)
|
|
41
|
+
|
|
42
|
+
receive_normal_count = receive_high_count = 0
|
|
43
|
+
for _ in range(1000):
|
|
44
|
+
job = await queue.get()
|
|
45
|
+
if job.name.startswith("job-normal-"):
|
|
46
|
+
receive_normal_count += 1
|
|
47
|
+
else:
|
|
48
|
+
receive_high_count += 1
|
|
49
|
+
self.assertTrue(receive_high_count > 500)
|
|
50
|
+
self.assertTrue(receive_normal_count < 500)
|
|
51
|
+
|
|
52
|
+
async def test_subqueue_fifo_policy(self) -> None:
|
|
53
|
+
policy = FifoSubQueuePolicy()
|
|
54
|
+
queue = PriorityFairQueue(subqueue_policy=policy)
|
|
55
|
+
|
|
56
|
+
await queue.put(Job("job-1", Priority.NORMAL, "tenant-a"))
|
|
57
|
+
await queue.put(Job("job-2", Priority.NORMAL, "tenant-a"))
|
|
58
|
+
await queue.put(Job("job-3", Priority.NORMAL, "tenant-b"))
|
|
59
|
+
await queue.put(Job("job-4", Priority.NORMAL, "tenant-a"))
|
|
60
|
+
await queue.put(Job("job-5", Priority.NORMAL, "tenant-c"))
|
|
61
|
+
await queue.put(Job("job-6", Priority.NORMAL, "tenant-b"))
|
|
62
|
+
|
|
63
|
+
self.assertEqual((await queue.get()).name, "job-1")
|
|
64
|
+
self.assertEqual((await queue.get()).name, "job-3")
|
|
65
|
+
self.assertEqual((await queue.get()).name, "job-5")
|
|
66
|
+
self.assertEqual((await queue.get()).name, "job-2")
|
|
67
|
+
self.assertEqual((await queue.get()).name, "job-6")
|
|
68
|
+
self.assertEqual((await queue.get()).name, "job-4")
|
|
69
|
+
|
|
70
|
+
async def test_custom_for_aging_policy(self) -> None:
|
|
71
|
+
policy = AgingWeightedRandomSubQueuePolicy()
|
|
72
|
+
queue = PriorityFairQueue(
|
|
73
|
+
priority_policy=AbsolutePriorityPolicy(),
|
|
74
|
+
subqueue_policy=policy,
|
|
75
|
+
)
|
|
76
|
+
await queue.put(Job("job-1", Priority.NORMAL, "tenant-a"))
|
|
77
|
+
await queue.put(Job("job-2", Priority.NORMAL, "tenant-b"))
|
|
78
|
+
await queue.put(Job("job-3", Priority.NORMAL, "tenant-c"))
|
|
79
|
+
await queue.put(Job("job-4", Priority.NORMAL, "tenant-c"))
|
|
80
|
+
await queue.put(Job("job-5", Priority.NORMAL, "tenant-c"))
|
|
81
|
+
await queue.put(Job("job-6", Priority.NORMAL, "tenant-c"))
|
|
82
|
+
await queue.put(Job("job-7", Priority.NORMAL, "tenant-c"))
|
|
83
|
+
await queue.put(Job("job-8", Priority.NORMAL, "tenant-c"))
|
|
84
|
+
await queue.put(Job("job-9", Priority.NORMAL, "tenant-c"))
|
|
85
|
+
|
|
86
|
+
# c 的概率很大
|
|
87
|
+
# self.assertEqual((await queue.get()).name, "job-3")
|
|
88
|
+
while queue.empty():
|
|
89
|
+
await queue.get()
|
|
90
|
+
|
|
91
|
+
def test_subqueue_aging_policy_weights_backlog_and_wait(self) -> None:
|
|
92
|
+
policy = AgingWeightedRandomSubQueuePolicy()
|
|
93
|
+
bulk = SubQueue("bulk", waiting_rounds=0, pending_jobs=100)
|
|
94
|
+
small = SubQueue("small", waiting_rounds=0, pending_jobs=1)
|
|
95
|
+
|
|
96
|
+
with patch(
|
|
97
|
+
"priomesh.policies.subqueue.random.choices",
|
|
98
|
+
side_effect=[[small], [small]],
|
|
99
|
+
) as choices:
|
|
100
|
+
self.assertIs(policy.choose([bulk, small]), small)
|
|
101
|
+
self.assertEqual(bulk.waiting_rounds, 1)
|
|
102
|
+
self.assertEqual(small.waiting_rounds, 0)
|
|
103
|
+
|
|
104
|
+
self.assertIs(policy.choose([bulk, small]), small)
|
|
105
|
+
|
|
106
|
+
self.assertEqual(choices.call_args_list[0].kwargs["weights"], [10.0, 1.0])
|
|
107
|
+
self.assertEqual(choices.call_args_list[1].kwargs["weights"], [20.0, 1.0])
|
|
108
|
+
|
|
109
|
+
async def test_queue_updates_backlog_for_aging_policy(self) -> None:
|
|
110
|
+
policy = AgingWeightedRandomSubQueuePolicy()
|
|
111
|
+
queue = PriorityFairQueue(
|
|
112
|
+
priority_policy=AbsolutePriorityPolicy(),
|
|
113
|
+
subqueue_policy=policy,
|
|
114
|
+
)
|
|
115
|
+
jobs = [
|
|
116
|
+
*(Job(f"bulk-{i}", Priority.NORMAL, "bulk") for i in range(4)),
|
|
117
|
+
*(Job(f"small-{i}", Priority.NORMAL, "small") for i in range(2)),
|
|
118
|
+
]
|
|
119
|
+
for job in jobs:
|
|
120
|
+
await queue.put(job)
|
|
121
|
+
|
|
122
|
+
def choose_small(candidates: Sequence[SubQueue], **_: object) -> list[SubQueue]:
|
|
123
|
+
return [next(sq for sq in candidates if sq.name == "small")]
|
|
124
|
+
|
|
125
|
+
with patch(
|
|
126
|
+
"priomesh.policies.subqueue.random.choices",
|
|
127
|
+
side_effect=choose_small,
|
|
128
|
+
) as choices:
|
|
129
|
+
await queue.get()
|
|
130
|
+
await queue.get()
|
|
131
|
+
|
|
132
|
+
self.assertEqual(choices.call_args_list[0].kwargs["weights"], [2.0, 2**0.5])
|
|
133
|
+
self.assertEqual(choices.call_args_list[1].kwargs["weights"], [4.0, 1.0])
|
|
134
|
+
|
|
135
|
+
def test_subqueue_random_policy(self) -> None:
|
|
136
|
+
policy = RandomSubQueuePolicy()
|
|
137
|
+
subqueues = [
|
|
138
|
+
SubQueue("tenant-a", waiting_rounds=2),
|
|
139
|
+
SubQueue("tenant-b", waiting_rounds=5),
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
with patch(
|
|
143
|
+
"priomesh.policies.subqueue.random.choice",
|
|
144
|
+
return_value=subqueues[1],
|
|
145
|
+
) as choice:
|
|
146
|
+
result = policy.choose(subqueues)
|
|
147
|
+
|
|
148
|
+
self.assertIs(result, subqueues[1])
|
|
149
|
+
choice.assert_called_once_with(subqueues)
|
|
150
|
+
self.assertEqual([sq.waiting_rounds for sq in subqueues], [2, 5])
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
if __name__ == "__main__":
|
|
154
|
+
"""
|
|
155
|
+
python -m pip install .
|
|
156
|
+
python -m unittest discover -s tests -v
|
|
157
|
+
|
|
158
|
+
or
|
|
159
|
+
PYTHONPATH=src python -m unittest discover -s tests -v
|
|
160
|
+
"""
|
|
161
|
+
unittest.main()
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import unittest
|
|
3
|
+
|
|
4
|
+
from priomesh import Job, Priority, PriorityFairQueue, QueueShutDown
|
|
5
|
+
from priomesh.policies import FifoSubQueuePolicy
|
|
6
|
+
from priomesh.policies import AbsolutePriorityPolicy
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PriorityFairQueueTests(unittest.IsolatedAsyncioTestCase):
|
|
10
|
+
async def test_empty_tracks_pending_jobs(self) -> None:
|
|
11
|
+
queue = PriorityFairQueue()
|
|
12
|
+
|
|
13
|
+
self.assertTrue(queue.empty())
|
|
14
|
+
|
|
15
|
+
await queue.put(Job("job-1", Priority.NORMAL, "tenant-a"))
|
|
16
|
+
await queue.put(Job("job-2", Priority.NORMAL, "tenant-a"))
|
|
17
|
+
self.assertFalse(queue.empty())
|
|
18
|
+
|
|
19
|
+
await queue.get()
|
|
20
|
+
self.assertFalse(queue.empty())
|
|
21
|
+
|
|
22
|
+
await queue.get()
|
|
23
|
+
self.assertTrue(queue.empty())
|
|
24
|
+
|
|
25
|
+
async def test_subqueue_fifo(self) -> None:
|
|
26
|
+
queue = PriorityFairQueue()
|
|
27
|
+
|
|
28
|
+
await queue.put(Job("job-1", Priority.NORMAL, "tenant-a"))
|
|
29
|
+
await queue.put(Job("job-2", Priority.NORMAL, "tenant-a"))
|
|
30
|
+
await queue.put(Job("job-3", Priority.NORMAL, "tenant-b"))
|
|
31
|
+
await queue.put(Job("job-4", Priority.NORMAL, "tenant-c"))
|
|
32
|
+
await queue.put(Job("job-5", Priority.NORMAL, "tenant-a"))
|
|
33
|
+
await queue.put(Job("job-6", Priority.NORMAL, "tenant-c"))
|
|
34
|
+
await queue.put(Job("job-7", Priority.NORMAL, "tenant-a"))
|
|
35
|
+
|
|
36
|
+
jobs = []
|
|
37
|
+
for _ in range(7):
|
|
38
|
+
job = await queue.get()
|
|
39
|
+
if job.subqueue == "tenant-a":
|
|
40
|
+
jobs.append(job)
|
|
41
|
+
self.assertEqual(len(jobs), 4)
|
|
42
|
+
|
|
43
|
+
self.assertEqual(jobs[0].name, "job-1")
|
|
44
|
+
self.assertEqual(jobs[1].name, "job-2")
|
|
45
|
+
self.assertEqual(jobs[2].name, "job-5")
|
|
46
|
+
self.assertEqual(jobs[3].name, "job-7")
|
|
47
|
+
|
|
48
|
+
async def test_subqueue_rotation(self) -> None:
|
|
49
|
+
policy = FifoSubQueuePolicy()
|
|
50
|
+
queue = PriorityFairQueue(subqueue_policy=policy)
|
|
51
|
+
|
|
52
|
+
await queue.put(Job("job-1", Priority.NORMAL, "tenant-a"))
|
|
53
|
+
await queue.put(Job("job-2", Priority.NORMAL, "tenant-a"))
|
|
54
|
+
await queue.put(Job("job-3", Priority.NORMAL, "tenant-b"))
|
|
55
|
+
await queue.put(Job("job-4", Priority.NORMAL, "tenant-c"))
|
|
56
|
+
await queue.put(Job("job-5", Priority.NORMAL, "tenant-a"))
|
|
57
|
+
await queue.put(Job("job-6", Priority.NORMAL, "tenant-c"))
|
|
58
|
+
await queue.put(Job("job-7", Priority.NORMAL, "tenant-a"))
|
|
59
|
+
|
|
60
|
+
self.assertEqual((await queue.get()).name, "job-1")
|
|
61
|
+
self.assertEqual((await queue.get()).name, "job-3")
|
|
62
|
+
self.assertEqual((await queue.get()).name, "job-4")
|
|
63
|
+
self.assertEqual((await queue.get()).name, "job-2")
|
|
64
|
+
self.assertEqual((await queue.get()).name, "job-6")
|
|
65
|
+
self.assertEqual((await queue.get()).name, "job-5")
|
|
66
|
+
self.assertEqual((await queue.get()).name, "job-7")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
async def test_absolute_priority_policy(self) -> None:
|
|
70
|
+
policy = AbsolutePriorityPolicy()
|
|
71
|
+
queue = PriorityFairQueue(priority_policy=policy)
|
|
72
|
+
|
|
73
|
+
await queue.put(Job("job-1", Priority.CRITICAL, "A"))
|
|
74
|
+
await queue.put(Job("job-2", Priority.NORMAL, "A"))
|
|
75
|
+
await queue.put(Job("job-3", Priority.NORMAL, "A"))
|
|
76
|
+
await queue.put(Job("job-4", Priority.HIGH, "A"))
|
|
77
|
+
await queue.put(Job("job-5", Priority.LOW, "A"))
|
|
78
|
+
await queue.put(Job("job-6", Priority.HIGH, "A"))
|
|
79
|
+
|
|
80
|
+
self.assertEqual((await queue.get()).name, "job-1")
|
|
81
|
+
self.assertEqual((await queue.get()).name, "job-4")
|
|
82
|
+
self.assertEqual((await queue.get()).name, "job-6")
|
|
83
|
+
self.assertEqual((await queue.get()).name, "job-2")
|
|
84
|
+
self.assertEqual((await queue.get()).name, "job-3")
|
|
85
|
+
self.assertEqual((await queue.get()).name, "job-5")
|
|
86
|
+
|
|
87
|
+
async def test_empty_wait_and_notify(self) -> None:
|
|
88
|
+
queue = PriorityFairQueue()
|
|
89
|
+
job = Job("job-1", Priority.NORMAL)
|
|
90
|
+
|
|
91
|
+
# 创建一个正在等待取任务的协程。
|
|
92
|
+
task_get = asyncio.create_task(queue.get())
|
|
93
|
+
|
|
94
|
+
# 让出执行权,让 task_get 有机会运行并进入等待
|
|
95
|
+
await asyncio.sleep(0.1)
|
|
96
|
+
self.assertFalse(task_get.done())
|
|
97
|
+
|
|
98
|
+
# 放入任务后,之前等待的 task_get 应该被唤醒并拿到它
|
|
99
|
+
await queue.put(job)
|
|
100
|
+
result = await asyncio.wait_for(task_get, timeout=1)
|
|
101
|
+
self.assertEqual(result, job)
|
|
102
|
+
|
|
103
|
+
async def test_concurrent_put_get_preserves_all_jobs(self) -> None:
|
|
104
|
+
queue = PriorityFairQueue()
|
|
105
|
+
|
|
106
|
+
producer_count = 4
|
|
107
|
+
jobs_per_producer = 25
|
|
108
|
+
consumer_count = 5
|
|
109
|
+
jobs_per_consumer = 20
|
|
110
|
+
|
|
111
|
+
batches = [
|
|
112
|
+
[
|
|
113
|
+
Job(
|
|
114
|
+
name=f"producer-{producer}-job-{job_number}",
|
|
115
|
+
priority=Priority.NORMAL,
|
|
116
|
+
subqueue=f"tenant-{producer}"
|
|
117
|
+
)
|
|
118
|
+
for job_number in range(jobs_per_producer)
|
|
119
|
+
]
|
|
120
|
+
for producer in range(producer_count)
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
async def produce(jobs: list[Job]) -> None:
|
|
124
|
+
for job in jobs:
|
|
125
|
+
await queue.put(job)
|
|
126
|
+
|
|
127
|
+
async def consume(count: int) -> list[Job]:
|
|
128
|
+
# 未拿到足够的会卡住
|
|
129
|
+
return [
|
|
130
|
+
await queue.get()
|
|
131
|
+
for _ in range(count)
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
consumer_tasks = [
|
|
135
|
+
asyncio.create_task(consume(jobs_per_consumer))
|
|
136
|
+
for _ in range(consumer_count)
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
await asyncio.wait_for(
|
|
140
|
+
asyncio.gather(
|
|
141
|
+
*(produce(batch) for batch in batches),
|
|
142
|
+
*consumer_tasks
|
|
143
|
+
),
|
|
144
|
+
timeout=5
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
received_names = [
|
|
148
|
+
job.name
|
|
149
|
+
for task in consumer_tasks
|
|
150
|
+
for job in task.result()
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
expected_names = [
|
|
154
|
+
job.name
|
|
155
|
+
for batch in batches
|
|
156
|
+
for job in batch
|
|
157
|
+
]
|
|
158
|
+
self.assertEqual(len(received_names), producer_count * jobs_per_producer)
|
|
159
|
+
self.assertCountEqual(received_names, expected_names)
|
|
160
|
+
self.assertTrue(queue.empty())
|
|
161
|
+
|
|
162
|
+
async def test_get_from_shutdown_queue(self) -> None:
|
|
163
|
+
queue = PriorityFairQueue()
|
|
164
|
+
await queue.put(Job("job-1", Priority.NORMAL))
|
|
165
|
+
await queue.put(Job("job-2", Priority.NORMAL))
|
|
166
|
+
await queue.put(Job("job-3", Priority.NORMAL))
|
|
167
|
+
|
|
168
|
+
await queue.shutdown()
|
|
169
|
+
|
|
170
|
+
self.assertEqual((await queue.get()).name, "job-1")
|
|
171
|
+
self.assertEqual((await queue.get()).name, "job-2")
|
|
172
|
+
self.assertEqual((await queue.get()).name, "job-3")
|
|
173
|
+
with self.assertRaises(QueueShutDown):
|
|
174
|
+
await queue.get()
|
|
175
|
+
|
|
176
|
+
async def test_put_to_shutdown_queue(self) -> None:
|
|
177
|
+
queue = PriorityFairQueue()
|
|
178
|
+
await queue.shutdown()
|
|
179
|
+
|
|
180
|
+
with self.assertRaises(QueueShutDown):
|
|
181
|
+
await queue.put(Job("job-1", Priority.NORMAL))
|
|
182
|
+
|
|
183
|
+
async def test_wait_waiter(self) -> None:
|
|
184
|
+
queue = PriorityFairQueue()
|
|
185
|
+
|
|
186
|
+
task = asyncio.create_task(queue.get())
|
|
187
|
+
await asyncio.sleep(0.01)
|
|
188
|
+
self.assertFalse(task.done())
|
|
189
|
+
|
|
190
|
+
await queue.shutdown()
|
|
191
|
+
with self.assertRaises(QueueShutDown):
|
|
192
|
+
await asyncio.wait_for(task, timeout=1)
|
|
193
|
+
|
|
194
|
+
async def test_wait_mul_waiters(self) -> None:
|
|
195
|
+
queue = PriorityFairQueue()
|
|
196
|
+
|
|
197
|
+
task1 = asyncio.create_task(queue.get())
|
|
198
|
+
task2 = asyncio.create_task(queue.get())
|
|
199
|
+
task3 = asyncio.create_task(queue.get())
|
|
200
|
+
await asyncio.sleep(0.01)
|
|
201
|
+
self.assertFalse(task1.done())
|
|
202
|
+
self.assertFalse(task2.done())
|
|
203
|
+
self.assertFalse(task3.done())
|
|
204
|
+
|
|
205
|
+
await queue.shutdown()
|
|
206
|
+
for t in (task1, task2, task3):
|
|
207
|
+
with self.assertRaises(QueueShutDown):
|
|
208
|
+
await asyncio.wait_for(t, timeout=1)
|
|
209
|
+
|
|
210
|
+
if __name__ == "__main__":
|
|
211
|
+
"""
|
|
212
|
+
python -m pip install .
|
|
213
|
+
python -m unittest discover -s tests -v
|
|
214
|
+
|
|
215
|
+
or
|
|
216
|
+
PYTHONPATH=src python -m unittest discover -s tests -v
|
|
217
|
+
"""
|
|
218
|
+
unittest.main()
|