priomesh-python 0.1.0__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: priomesh-python
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: A pluggable priority and fair-queue scheduling library
5
5
  License: MIT License
6
6
 
@@ -61,14 +61,13 @@ cd priomesh-python
61
61
  python -m pip install -e .
62
62
  ```
63
63
 
64
- Once the project is published to PyPI, end users will be able to install it
65
- with:
64
+ Install the latest published release from PyPI:
66
65
 
67
66
  ```bash
68
67
  python -m pip install priomesh-python
69
68
  ```
70
69
 
71
- Until then, users can install directly from the GitHub repository:
70
+ To install directly from the GitHub repository instead:
72
71
 
73
72
  ```bash
74
73
  python -m pip install "git+https://github.com/PrioMesh/priomesh-python.git"
@@ -108,11 +107,53 @@ sub-queue within that level. Jobs within each priority/sub-queue pair are
108
107
  returned in FIFO order. Calling `get()` on an empty queue waits until a job is
109
108
  put; `empty()` checks whether the queue currently has any jobs.
110
109
 
110
+ By default, the queue is unbounded. Pass a positive `maxsize` to limit the
111
+ number of jobs waiting in the queue across all priorities and sub-queues.
112
+ When it is full, `put()` waits until a `get()` removes a job and frees a slot.
113
+ `maxsize=0` keeps the queue unbounded; negative values raise `ValueError`.
114
+
111
115
  Call `shutdown()` to stop accepting jobs. It performs a graceful shutdown:
112
116
  `put()` raises `QueueShutDown`, while `get()` continues returning jobs that
113
117
  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.
118
+ calls raise `QueueShutDown`, allowing consumers to exit. A `put()` waiting
119
+ because the queue is full is also woken by shutdown and raises
120
+ `QueueShutDown`. The example above shows a consumer draining the queue and
121
+ stopping cleanly.
122
+
123
+ `put()` and `get()` accept an optional `timeout` in seconds. The default
124
+ `None` waits without a deadline; otherwise the timeout must be a finite
125
+ positive number. Zero, negative, `NaN`, and infinite timeout values raise
126
+ `ValueError`. The timeout is implemented with
127
+ `asyncio.wait_for()`, so an expired timeout raises `asyncio.TimeoutError`
128
+ (`TimeoutError` on Python 3.11 and newer). Cancelling the task awaiting either
129
+ operation also cancels the operation. If the operation is cancelled while
130
+ waiting for a job or for queue capacity, it does not remove or add a job.
131
+
132
+ ```python
133
+ import asyncio
134
+ from priomesh import Job, Priority, PriorityFairQueue
135
+
136
+ async def main() -> None:
137
+ queue = PriorityFairQueue()
138
+ try:
139
+ await queue.get(timeout=1.0)
140
+ except asyncio.TimeoutError:
141
+ print("No job arrived before the timeout")
142
+
143
+ bounded_queue = PriorityFairQueue(maxsize=1)
144
+ await bounded_queue.put(Job("queued", Priority.NORMAL))
145
+ try:
146
+ await bounded_queue.put(Job("waiting", Priority.NORMAL), timeout=1.0)
147
+ except asyncio.TimeoutError:
148
+ print("No capacity became available before the timeout")
149
+
150
+
151
+ asyncio.run(main())
152
+ ```
153
+
154
+ For manual cancellation, create a task for `put()` or `get()` and call its
155
+ `cancel()` method. Timeout and cancellation handling use `asyncio`; they do
156
+ not use a separate queue-specific cancellation API.
116
157
 
117
158
  ## Scheduling policies
118
159
 
@@ -20,14 +20,13 @@ cd priomesh-python
20
20
  python -m pip install -e .
21
21
  ```
22
22
 
23
- Once the project is published to PyPI, end users will be able to install it
24
- with:
23
+ Install the latest published release from PyPI:
25
24
 
26
25
  ```bash
27
26
  python -m pip install priomesh-python
28
27
  ```
29
28
 
30
- Until then, users can install directly from the GitHub repository:
29
+ To install directly from the GitHub repository instead:
31
30
 
32
31
  ```bash
33
32
  python -m pip install "git+https://github.com/PrioMesh/priomesh-python.git"
@@ -67,11 +66,53 @@ sub-queue within that level. Jobs within each priority/sub-queue pair are
67
66
  returned in FIFO order. Calling `get()` on an empty queue waits until a job is
68
67
  put; `empty()` checks whether the queue currently has any jobs.
69
68
 
69
+ By default, the queue is unbounded. Pass a positive `maxsize` to limit the
70
+ number of jobs waiting in the queue across all priorities and sub-queues.
71
+ When it is full, `put()` waits until a `get()` removes a job and frees a slot.
72
+ `maxsize=0` keeps the queue unbounded; negative values raise `ValueError`.
73
+
70
74
  Call `shutdown()` to stop accepting jobs. It performs a graceful shutdown:
71
75
  `put()` raises `QueueShutDown`, while `get()` continues returning jobs that
72
76
  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.
77
+ calls raise `QueueShutDown`, allowing consumers to exit. A `put()` waiting
78
+ because the queue is full is also woken by shutdown and raises
79
+ `QueueShutDown`. The example above shows a consumer draining the queue and
80
+ stopping cleanly.
81
+
82
+ `put()` and `get()` accept an optional `timeout` in seconds. The default
83
+ `None` waits without a deadline; otherwise the timeout must be a finite
84
+ positive number. Zero, negative, `NaN`, and infinite timeout values raise
85
+ `ValueError`. The timeout is implemented with
86
+ `asyncio.wait_for()`, so an expired timeout raises `asyncio.TimeoutError`
87
+ (`TimeoutError` on Python 3.11 and newer). Cancelling the task awaiting either
88
+ operation also cancels the operation. If the operation is cancelled while
89
+ waiting for a job or for queue capacity, it does not remove or add a job.
90
+
91
+ ```python
92
+ import asyncio
93
+ from priomesh import Job, Priority, PriorityFairQueue
94
+
95
+ async def main() -> None:
96
+ queue = PriorityFairQueue()
97
+ try:
98
+ await queue.get(timeout=1.0)
99
+ except asyncio.TimeoutError:
100
+ print("No job arrived before the timeout")
101
+
102
+ bounded_queue = PriorityFairQueue(maxsize=1)
103
+ await bounded_queue.put(Job("queued", Priority.NORMAL))
104
+ try:
105
+ await bounded_queue.put(Job("waiting", Priority.NORMAL), timeout=1.0)
106
+ except asyncio.TimeoutError:
107
+ print("No capacity became available before the timeout")
108
+
109
+
110
+ asyncio.run(main())
111
+ ```
112
+
113
+ For manual cancellation, create a task for `put()` or `get()` and call its
114
+ `cancel()` method. Timeout and cancellation handling use `asyncio`; they do
115
+ not use a separate queue-specific cancellation API.
75
116
 
76
117
  ## Scheduling policies
77
118
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "priomesh-python"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "A pluggable priority and fair-queue scheduling library"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import math
2
3
  from collections import deque
3
4
  from .models import Priority, SubQueue, Job
4
5
 
@@ -14,6 +15,11 @@ class QueueShutDown(Exception):
14
15
  """Raised when an operation is attempted on a shut-down queue."""
15
16
 
16
17
 
18
+ def _validate_timeout(timeout: float | None) -> None:
19
+ if timeout is not None and (not math.isfinite(timeout) or timeout <= 0):
20
+ raise ValueError("timeout must be a finite positive number or None")
21
+
22
+
17
23
  class PriorityFairQueue:
18
24
  """Schedule jobs by priority and distribute service fairly among subqueues.
19
25
 
@@ -26,6 +32,7 @@ class PriorityFairQueue:
26
32
  self,
27
33
  priority_policy: PriorityPolicy | None = None,
28
34
  subqueue_policy: SubQueuePolicy | None = None,
35
+ maxsize: int = 0,
29
36
  ) -> None:
30
37
  self._priority_policy = priority_policy or RandomPriorityPolicy()
31
38
  self._subqueue_policy = subqueue_policy or AgingWeightedRandomSubQueuePolicy()
@@ -34,11 +41,28 @@ class PriorityFairQueue:
34
41
  self._condition = asyncio.Condition()
35
42
  self._shutdown = False
36
43
 
44
+ if maxsize < 0:
45
+ raise ValueError("maxsize must be non-negative")
46
+ self._maxsize = maxsize
47
+ self._size = 0
48
+
37
49
  def empty(self) -> bool:
38
50
  return not self._rotation
39
51
 
40
- async def put(self, job: Job) -> None:
52
+ async def put(self, job: Job, timeout: float | None = None) -> None:
53
+ _validate_timeout(timeout)
54
+ if timeout is None:
55
+ await self._put(job)
56
+ else:
57
+ await asyncio.wait_for(self._put(job), timeout=timeout)
58
+
59
+ async def _put(self, job: Job) -> None:
41
60
  async with self._condition:
61
+ while self._maxsize > 0 and self._size >= self._maxsize:
62
+ if self._shutdown:
63
+ raise QueueShutDown("cannot put into a shut-down queue")
64
+ await self._condition.wait()
65
+
42
66
  if self._shutdown:
43
67
  raise QueueShutDown("cannot put into a shut-down queue")
44
68
  level_queues = self._queues.setdefault(job.priority, {})
@@ -53,9 +77,16 @@ class PriorityFairQueue:
53
77
  rotation.append(SubQueue(name=job.subqueue, waiting_rounds=0))
54
78
  else:
55
79
  subqueue.pending_jobs += 1
80
+ self._size += 1
56
81
  self._condition.notify()
57
82
 
58
- async def get(self) -> Job:
83
+ async def get(self, timeout: float | None = None) -> Job:
84
+ _validate_timeout(timeout)
85
+ if timeout is None:
86
+ return await self._get()
87
+ return await asyncio.wait_for(self._get(), timeout=timeout)
88
+
89
+ async def _get(self) -> Job:
59
90
  async with self._condition:
60
91
  while True:
61
92
  active_priorities = [p for p, r in self._rotation.items() if r]
@@ -67,6 +98,8 @@ class PriorityFairQueue:
67
98
  r.remove(subqueue)
68
99
  jobs = self._queues[priority][subqueue.name]
69
100
  job = jobs.popleft()
101
+ self._size -= 1
102
+ self._condition.notify_all()
70
103
  subqueue.pending_jobs -= 1
71
104
  if jobs:
72
105
  r.append(subqueue)
@@ -85,4 +118,10 @@ class PriorityFairQueue:
85
118
  async def shutdown(self) -> None:
86
119
  async with self._condition:
87
120
  self._shutdown = True
88
- self._condition.notify_all() # 唤醒所有等待的协程,促使其检查关闭态并退出
121
+ self._condition.notify_all() # 唤醒所有等待的协程,促使其检查关闭态并退出
122
+
123
+ def qsize(self) -> int:
124
+ return self._size
125
+
126
+ def full(self) -> bool:
127
+ return self._maxsize > 0 and self._size >= self._maxsize
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: priomesh-python
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: A pluggable priority and fair-queue scheduling library
5
5
  License: MIT License
6
6
 
@@ -61,14 +61,13 @@ cd priomesh-python
61
61
  python -m pip install -e .
62
62
  ```
63
63
 
64
- Once the project is published to PyPI, end users will be able to install it
65
- with:
64
+ Install the latest published release from PyPI:
66
65
 
67
66
  ```bash
68
67
  python -m pip install priomesh-python
69
68
  ```
70
69
 
71
- Until then, users can install directly from the GitHub repository:
70
+ To install directly from the GitHub repository instead:
72
71
 
73
72
  ```bash
74
73
  python -m pip install "git+https://github.com/PrioMesh/priomesh-python.git"
@@ -108,11 +107,53 @@ sub-queue within that level. Jobs within each priority/sub-queue pair are
108
107
  returned in FIFO order. Calling `get()` on an empty queue waits until a job is
109
108
  put; `empty()` checks whether the queue currently has any jobs.
110
109
 
110
+ By default, the queue is unbounded. Pass a positive `maxsize` to limit the
111
+ number of jobs waiting in the queue across all priorities and sub-queues.
112
+ When it is full, `put()` waits until a `get()` removes a job and frees a slot.
113
+ `maxsize=0` keeps the queue unbounded; negative values raise `ValueError`.
114
+
111
115
  Call `shutdown()` to stop accepting jobs. It performs a graceful shutdown:
112
116
  `put()` raises `QueueShutDown`, while `get()` continues returning jobs that
113
117
  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.
118
+ calls raise `QueueShutDown`, allowing consumers to exit. A `put()` waiting
119
+ because the queue is full is also woken by shutdown and raises
120
+ `QueueShutDown`. The example above shows a consumer draining the queue and
121
+ stopping cleanly.
122
+
123
+ `put()` and `get()` accept an optional `timeout` in seconds. The default
124
+ `None` waits without a deadline; otherwise the timeout must be a finite
125
+ positive number. Zero, negative, `NaN`, and infinite timeout values raise
126
+ `ValueError`. The timeout is implemented with
127
+ `asyncio.wait_for()`, so an expired timeout raises `asyncio.TimeoutError`
128
+ (`TimeoutError` on Python 3.11 and newer). Cancelling the task awaiting either
129
+ operation also cancels the operation. If the operation is cancelled while
130
+ waiting for a job or for queue capacity, it does not remove or add a job.
131
+
132
+ ```python
133
+ import asyncio
134
+ from priomesh import Job, Priority, PriorityFairQueue
135
+
136
+ async def main() -> None:
137
+ queue = PriorityFairQueue()
138
+ try:
139
+ await queue.get(timeout=1.0)
140
+ except asyncio.TimeoutError:
141
+ print("No job arrived before the timeout")
142
+
143
+ bounded_queue = PriorityFairQueue(maxsize=1)
144
+ await bounded_queue.put(Job("queued", Priority.NORMAL))
145
+ try:
146
+ await bounded_queue.put(Job("waiting", Priority.NORMAL), timeout=1.0)
147
+ except asyncio.TimeoutError:
148
+ print("No capacity became available before the timeout")
149
+
150
+
151
+ asyncio.run(main())
152
+ ```
153
+
154
+ For manual cancellation, create a task for `put()` or `get()` and call its
155
+ `cancel()` method. Timeout and cancellation handling use `asyncio`; they do
156
+ not use a separate queue-specific cancellation API.
116
157
 
117
158
  ## Scheduling policies
118
159
 
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import math
2
3
  import unittest
3
4
 
4
5
  from priomesh import Job, Priority, PriorityFairQueue, QueueShutDown
@@ -207,6 +208,168 @@ class PriorityFairQueueTests(unittest.IsolatedAsyncioTestCase):
207
208
  with self.assertRaises(QueueShutDown):
208
209
  await asyncio.wait_for(t, timeout=1)
209
210
 
211
+ async def test_maxsize_neg(self) -> None:
212
+ with self.assertRaises(ValueError):
213
+ PriorityFairQueue(maxsize=-1)
214
+
215
+ async def test_maxsize_zero(self) -> None:
216
+ queue = PriorityFairQueue(maxsize=0)
217
+ for i in range(10):
218
+ await queue.put(Job(f"job-{i}", Priority.NORMAL))
219
+ self.assertEqual(queue.qsize(), 10)
220
+ self.assertFalse(queue.full())
221
+ while not queue.empty():
222
+ await queue.get()
223
+ self.assertEqual(queue.qsize(), 0)
224
+
225
+ async def test_qsize_and_full_track_jobs(self) -> None:
226
+ queue = PriorityFairQueue(maxsize=2)
227
+
228
+ self.assertEqual(queue.qsize(), 0)
229
+ self.assertFalse(queue.full())
230
+
231
+ await queue.put(Job("job-1", Priority.NORMAL, "tenant-a"))
232
+ self.assertEqual(queue.qsize(), 1)
233
+ self.assertFalse(queue.full())
234
+
235
+ await queue.put(Job("job-2", Priority.HIGH, "tenant-b"))
236
+ self.assertEqual(queue.qsize(), 2)
237
+ self.assertTrue(queue.full())
238
+
239
+ await queue.get()
240
+ self.assertEqual(queue.qsize(), 1)
241
+ self.assertFalse(queue.full())
242
+
243
+ await queue.get()
244
+ self.assertEqual(queue.qsize(), 0)
245
+ self.assertFalse(queue.full())
246
+
247
+ async def test_full_is_false_for_unbounded_queue(self) -> None:
248
+ queue = PriorityFairQueue()
249
+ await queue.put(Job("job-1", Priority.NORMAL))
250
+
251
+ self.assertEqual(queue.qsize(), 1)
252
+ self.assertFalse(queue.full())
253
+
254
+ async def test_maxsize_1(self) -> None:
255
+ queue = PriorityFairQueue(maxsize=1)
256
+ await queue.put(Job("job-1", Priority.NORMAL))
257
+
258
+ t = asyncio.create_task(
259
+ queue.put(Job("job-2", Priority.NORMAL))
260
+ )
261
+ await asyncio.sleep(0)
262
+ self.assertFalse(t.done())
263
+ await queue.get()
264
+
265
+ await asyncio.wait_for(t, timeout=1)
266
+ await queue.get()
267
+ self.assertTrue(t.done())
268
+ self.assertTrue(queue.empty())
269
+
270
+ async def test_shutdown_wakes_put_waiting_on_full_queue(self) -> None:
271
+ queue = PriorityFairQueue(maxsize=1)
272
+ await queue.put(Job("job-1", Priority.NORMAL))
273
+
274
+ put_task = asyncio.create_task(
275
+ queue.put(Job("job-2", Priority.NORMAL))
276
+ )
277
+ await asyncio.sleep(0)
278
+ self.assertFalse(put_task.done())
279
+
280
+ await queue.shutdown()
281
+
282
+ with self.assertRaises(QueueShutDown):
283
+ await asyncio.wait_for(put_task, timeout=1)
284
+
285
+ self.assertEqual((await queue.get()).name, "job-1")
286
+ with self.assertRaises(QueueShutDown):
287
+ await queue.get()
288
+
289
+ async def test_get_times_out_when_queue_is_empty(self) -> None:
290
+ queue = PriorityFairQueue()
291
+
292
+ with self.assertRaises(asyncio.TimeoutError):
293
+ await queue.get(timeout=0.01)
294
+
295
+ self.assertTrue(queue.empty())
296
+ self.assertEqual(queue.qsize(), 0)
297
+
298
+ job = Job("job-1", Priority.NORMAL)
299
+ await queue.put(job)
300
+ self.assertEqual(await queue.get(), job)
301
+
302
+ async def test_put_times_out_when_queue_is_full(self) -> None:
303
+ queue = PriorityFairQueue(maxsize=1)
304
+ first_job = Job("job-1", Priority.NORMAL)
305
+ await queue.put(first_job)
306
+
307
+ with self.assertRaises(asyncio.TimeoutError):
308
+ await queue.put(Job("job-2", Priority.NORMAL), timeout=0.01)
309
+
310
+ self.assertEqual(queue.qsize(), 1)
311
+ self.assertTrue(queue.full())
312
+ self.assertEqual(await queue.get(), first_job)
313
+
314
+ async def test_cancelled_get_does_not_remove_jobs(self) -> None:
315
+ queue = PriorityFairQueue()
316
+ get_task = asyncio.create_task(queue.get())
317
+ await asyncio.sleep(0)
318
+ self.assertFalse(get_task.done())
319
+
320
+ get_task.cancel()
321
+ with self.assertRaises(asyncio.CancelledError):
322
+ await get_task
323
+
324
+ job = Job("job-1", Priority.NORMAL)
325
+ await queue.put(job)
326
+ self.assertEqual(queue.qsize(), 1)
327
+ self.assertEqual(await queue.get(), job)
328
+
329
+ async def test_cancelled_put_does_not_enqueue_job(self) -> None:
330
+ queue = PriorityFairQueue(maxsize=1)
331
+ first_job = Job("job-1", Priority.NORMAL)
332
+ await queue.put(first_job)
333
+
334
+ put_task = asyncio.create_task(
335
+ queue.put(Job("job-2", Priority.NORMAL))
336
+ )
337
+ await asyncio.sleep(0)
338
+ self.assertFalse(put_task.done())
339
+
340
+ put_task.cancel()
341
+ with self.assertRaises(asyncio.CancelledError):
342
+ await put_task
343
+
344
+ self.assertEqual(queue.qsize(), 1)
345
+ self.assertTrue(queue.full())
346
+ self.assertEqual(await queue.get(), first_job)
347
+
348
+ async def test_put_rejects_invalid_timeouts(self) -> None:
349
+ queue = PriorityFairQueue()
350
+ job = Job("job-1", Priority.NORMAL)
351
+
352
+ for timeout in (0, -1, math.inf, -math.inf, math.nan):
353
+ with self.subTest(timeout=timeout):
354
+ with self.assertRaises(ValueError):
355
+ await queue.put(job, timeout=timeout)
356
+
357
+ self.assertTrue(queue.empty())
358
+ self.assertEqual(queue.qsize(), 0)
359
+
360
+ async def test_get_rejects_invalid_timeouts_without_consuming_jobs(self) -> None:
361
+ queue = PriorityFairQueue()
362
+ job = Job("job-1", Priority.NORMAL)
363
+ await queue.put(job)
364
+
365
+ for timeout in (0, -1, math.inf, -math.inf, math.nan):
366
+ with self.subTest(timeout=timeout):
367
+ with self.assertRaises(ValueError):
368
+ await queue.get(timeout=timeout)
369
+
370
+ self.assertEqual(queue.qsize(), 1)
371
+ self.assertEqual(await queue.get(), job)
372
+
210
373
  if __name__ == "__main__":
211
374
  """
212
375
  python -m pip install .
File without changes