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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,4 @@
1
+ from .priority import PriorityPolicy, RandomPriorityPolicy, AbsolutePriorityPolicy
2
+ from .subqueue import SubQueuePolicy, FifoSubQueuePolicy, AgingWeightedRandomSubQueuePolicy, RandomSubQueuePolicy
3
+
4
+ # 定义可导出
@@ -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,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()