tigrbl_engine_memqueue 0.1.10.dev1__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.
- tigrbl_engine_memqueue-0.1.10.dev1/.gitignore +22 -0
- tigrbl_engine_memqueue-0.1.10.dev1/PKG-INFO +5 -0
- tigrbl_engine_memqueue-0.1.10.dev1/README.md +23 -0
- tigrbl_engine_memqueue-0.1.10.dev1/pyproject.toml +15 -0
- tigrbl_engine_memqueue-0.1.10.dev1/src/tigrbl_engine_memqueue/__init__.py +2 -0
- tigrbl_engine_memqueue-0.1.10.dev1/src/tigrbl_engine_memqueue/plugin.py +40 -0
- tigrbl_engine_memqueue-0.1.10.dev1/src/tigrbl_engine_memqueue/queuehub.py +135 -0
- tigrbl_engine_memqueue-0.1.10.dev1/src/tigrbl_engine_memqueue/session.py +43 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
.pytest_cache/
|
|
3
|
+
.ruff_cache/
|
|
4
|
+
.mypy_cache/
|
|
5
|
+
.tox/
|
|
6
|
+
.nox/
|
|
7
|
+
.venv/
|
|
8
|
+
.coverage
|
|
9
|
+
htmlcov/
|
|
10
|
+
build/
|
|
11
|
+
dist/
|
|
12
|
+
site/
|
|
13
|
+
target/
|
|
14
|
+
*.egg-info/
|
|
15
|
+
*.pyc
|
|
16
|
+
*.zip
|
|
17
|
+
/.vendor
|
|
18
|
+
/.tmp
|
|
19
|
+
*.body
|
|
20
|
+
.pip-cache/
|
|
21
|
+
*.pyd
|
|
22
|
+
.tmp-release-plan-check.json
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# tigrbl_engine_memqueue
|
|
2
|
+
|
|
3
|
+
This file is a package-local distribution entry point.
|
|
4
|
+
It is not the authoritative location for repository governance, current target status, current state reporting, certification claims, or release evidence.
|
|
5
|
+
|
|
6
|
+
## Canonical repository docs
|
|
7
|
+
|
|
8
|
+
- `README.md`
|
|
9
|
+
- `docs/README.md`
|
|
10
|
+
- `docs/conformance/CURRENT_TARGET.md`
|
|
11
|
+
- `docs/conformance/CURRENT_STATE.md`
|
|
12
|
+
- `docs/conformance/NEXT_STEPS.md`
|
|
13
|
+
- `docs/governance/DOC_POINTERS.md`
|
|
14
|
+
- `docs/developer/PACKAGE_CATALOG.md`
|
|
15
|
+
- `docs/developer/PACKAGE_LAYOUT.md`
|
|
16
|
+
|
|
17
|
+
## Package identity
|
|
18
|
+
|
|
19
|
+
- workspace path: `pkgs/engines/tigrbl_engine_memqueue`
|
|
20
|
+
- workspace class: engine package
|
|
21
|
+
- implementation layout: `src/tigrbl_engine_memqueue/`
|
|
22
|
+
|
|
23
|
+
Long-form repository documentation is governed from `docs/`.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.25.0"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tigrbl_engine_memqueue"
|
|
7
|
+
version = "0.1.10.dev1"
|
|
8
|
+
requires-python = ">=3.10,<3.14"
|
|
9
|
+
dependencies = ["tigrbl"]
|
|
10
|
+
|
|
11
|
+
[project.entry-points."tigrbl.engine"]
|
|
12
|
+
memqueue = "tigrbl_engine_memqueue.plugin:register"
|
|
13
|
+
|
|
14
|
+
[tool.uv.sources]
|
|
15
|
+
tigrbl = { workspace = true }
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from tigrbl.engine.registry import register_engine
|
|
4
|
+
|
|
5
|
+
from .queuehub import QueueHub
|
|
6
|
+
from .session import QueueSession, AsyncQueueSession
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def register() -> None:
|
|
10
|
+
register_engine(kind="memqueue", build=build_memqueue, capabilities=capabilities)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def capabilities() -> dict:
|
|
14
|
+
return {
|
|
15
|
+
"engine": "memqueue",
|
|
16
|
+
"transactional": False,
|
|
17
|
+
"async_native": True,
|
|
18
|
+
"persistence": "process",
|
|
19
|
+
"features": {"fifo", "lifo", "priority_optional", "named_queues"},
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def build_memqueue(*, mapping=None, spec=None, dsn=None, **_) -> tuple[object, object]:
|
|
24
|
+
mapping = dict(mapping or {})
|
|
25
|
+
async_ = bool(getattr(spec, "async_", False))
|
|
26
|
+
|
|
27
|
+
mode = str(mapping.get("mode", "fifo")).lower()
|
|
28
|
+
max_items = int(mapping.get("max_items", 100_000))
|
|
29
|
+
namespace = str(mapping.get("namespace", "default"))
|
|
30
|
+
|
|
31
|
+
engine = QueueHub(mode=mode, max_items=max_items, namespace=namespace)
|
|
32
|
+
|
|
33
|
+
if async_:
|
|
34
|
+
def sessionmaker():
|
|
35
|
+
return AsyncQueueSession(engine)
|
|
36
|
+
else:
|
|
37
|
+
def sessionmaker():
|
|
38
|
+
return QueueSession(engine)
|
|
39
|
+
|
|
40
|
+
return engine, sessionmaker
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import heapq
|
|
4
|
+
from collections import deque
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from threading import Condition, RLock
|
|
7
|
+
from time import monotonic
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class _Q:
|
|
13
|
+
mode: str
|
|
14
|
+
max_items: int
|
|
15
|
+
lock: RLock
|
|
16
|
+
cv: Condition
|
|
17
|
+
# one of these is used depending on mode
|
|
18
|
+
dq: deque[Any] | None = None
|
|
19
|
+
heap: list[tuple[int, float, Any]] | None = None
|
|
20
|
+
seq: int = 0
|
|
21
|
+
closed: bool = False
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class QueueHub:
|
|
25
|
+
"""Named in-process queues with blocking get/put.
|
|
26
|
+
|
|
27
|
+
Modes:
|
|
28
|
+
- fifo: deque append/pop left
|
|
29
|
+
- lifo: deque append/pop
|
|
30
|
+
- priority: heapq of (priority, t, item) (lower priority value first)
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, *, mode: str = "fifo", max_items: int = 100_000, namespace: str = "default") -> None:
|
|
34
|
+
mode = (mode or "fifo").lower()
|
|
35
|
+
if mode not in {"fifo", "lifo", "priority"}:
|
|
36
|
+
raise ValueError("mode must be fifo|lifo|priority")
|
|
37
|
+
if max_items <= 0:
|
|
38
|
+
raise ValueError("max_items must be > 0")
|
|
39
|
+
self.mode = mode
|
|
40
|
+
self.max_items = int(max_items)
|
|
41
|
+
self.namespace = namespace
|
|
42
|
+
self._lock = RLock()
|
|
43
|
+
self._qs: dict[str, _Q] = {}
|
|
44
|
+
|
|
45
|
+
def _q(self, name: str) -> _Q:
|
|
46
|
+
k = name or "default"
|
|
47
|
+
with self._lock:
|
|
48
|
+
q = self._qs.get(k)
|
|
49
|
+
if q is None:
|
|
50
|
+
lock = RLock()
|
|
51
|
+
cv = Condition(lock)
|
|
52
|
+
if self.mode == "priority":
|
|
53
|
+
q = _Q(mode=self.mode, max_items=self.max_items, lock=lock, cv=cv, heap=[])
|
|
54
|
+
else:
|
|
55
|
+
q = _Q(mode=self.mode, max_items=self.max_items, lock=lock, cv=cv, dq=deque())
|
|
56
|
+
self._qs[k] = q
|
|
57
|
+
return q
|
|
58
|
+
|
|
59
|
+
def close(self, name: str = "default") -> None:
|
|
60
|
+
q = self._q(name)
|
|
61
|
+
with q.lock:
|
|
62
|
+
q.closed = True
|
|
63
|
+
q.cv.notify_all()
|
|
64
|
+
|
|
65
|
+
def put(self, name: str, item: Any, *, priority: int = 0, timeout: float | None = None) -> bool:
|
|
66
|
+
q = self._q(name)
|
|
67
|
+
deadline = None if timeout is None else (monotonic() + max(0.0, float(timeout)))
|
|
68
|
+
with q.lock:
|
|
69
|
+
while not q.closed and self.size(name) >= q.max_items:
|
|
70
|
+
if deadline is None:
|
|
71
|
+
q.cv.wait()
|
|
72
|
+
else:
|
|
73
|
+
remain = deadline - monotonic()
|
|
74
|
+
if remain <= 0:
|
|
75
|
+
return False
|
|
76
|
+
q.cv.wait(remain)
|
|
77
|
+
if q.closed:
|
|
78
|
+
return False
|
|
79
|
+
if q.mode == "priority":
|
|
80
|
+
assert q.heap is not None
|
|
81
|
+
q.seq += 1
|
|
82
|
+
heapq.heappush(q.heap, (int(priority), monotonic(), item))
|
|
83
|
+
else:
|
|
84
|
+
assert q.dq is not None
|
|
85
|
+
q.dq.append(item)
|
|
86
|
+
q.cv.notify()
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
def get(self, name: str, *, timeout: float | None = None) -> Any | None:
|
|
90
|
+
q = self._q(name)
|
|
91
|
+
deadline = None if timeout is None else (monotonic() + max(0.0, float(timeout)))
|
|
92
|
+
with q.lock:
|
|
93
|
+
while not q.closed and self.size(name) == 0:
|
|
94
|
+
if deadline is None:
|
|
95
|
+
q.cv.wait()
|
|
96
|
+
else:
|
|
97
|
+
remain = deadline - monotonic()
|
|
98
|
+
if remain <= 0:
|
|
99
|
+
return None
|
|
100
|
+
q.cv.wait(remain)
|
|
101
|
+
if self.size(name) == 0:
|
|
102
|
+
return None
|
|
103
|
+
if q.mode == "priority":
|
|
104
|
+
assert q.heap is not None
|
|
105
|
+
_pri, _t, item = heapq.heappop(q.heap)
|
|
106
|
+
elif q.mode == "lifo":
|
|
107
|
+
assert q.dq is not None
|
|
108
|
+
item = q.dq.pop()
|
|
109
|
+
else: # fifo
|
|
110
|
+
assert q.dq is not None
|
|
111
|
+
item = q.dq.popleft()
|
|
112
|
+
q.cv.notify()
|
|
113
|
+
return item
|
|
114
|
+
|
|
115
|
+
def drain(self, name: str, *, max_items: int | None = None) -> list[Any]:
|
|
116
|
+
q = self._q(name)
|
|
117
|
+
out: list[Any] = []
|
|
118
|
+
with q.lock:
|
|
119
|
+
n = self.size(name)
|
|
120
|
+
if max_items is not None:
|
|
121
|
+
n = min(n, int(max_items))
|
|
122
|
+
for _ in range(n):
|
|
123
|
+
item = self.get(name, timeout=0.0)
|
|
124
|
+
if item is None:
|
|
125
|
+
break
|
|
126
|
+
out.append(item)
|
|
127
|
+
return out
|
|
128
|
+
|
|
129
|
+
def size(self, name: str) -> int:
|
|
130
|
+
q = self._q(name)
|
|
131
|
+
if q.mode == "priority":
|
|
132
|
+
assert q.heap is not None
|
|
133
|
+
return len(q.heap)
|
|
134
|
+
assert q.dq is not None
|
|
135
|
+
return len(q.dq)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .queuehub import QueueHub
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class QueueSession:
|
|
9
|
+
def __init__(self, engine: QueueHub) -> None:
|
|
10
|
+
self._engine = engine
|
|
11
|
+
self._closed = False
|
|
12
|
+
|
|
13
|
+
def close(self) -> None:
|
|
14
|
+
self._closed = True
|
|
15
|
+
|
|
16
|
+
def put(self, item: Any, *, name: str = "default", priority: int = 0, timeout: float | None = None) -> bool:
|
|
17
|
+
self._require_open()
|
|
18
|
+
return self._engine.put(name, item, priority=priority, timeout=timeout)
|
|
19
|
+
|
|
20
|
+
def get(self, *, name: str = "default", timeout: float | None = None) -> Any | None:
|
|
21
|
+
self._require_open()
|
|
22
|
+
return self._engine.get(name, timeout=timeout)
|
|
23
|
+
|
|
24
|
+
def drain(self, *, name: str = "default", max_items: int | None = None) -> list[Any]:
|
|
25
|
+
self._require_open()
|
|
26
|
+
return self._engine.drain(name, max_items=max_items)
|
|
27
|
+
|
|
28
|
+
def qsize(self, *, name: str = "default") -> int:
|
|
29
|
+
self._require_open()
|
|
30
|
+
return self._engine.size(name)
|
|
31
|
+
|
|
32
|
+
def close_queue(self, *, name: str = "default") -> None:
|
|
33
|
+
self._require_open()
|
|
34
|
+
self._engine.close(name)
|
|
35
|
+
|
|
36
|
+
def _require_open(self) -> None:
|
|
37
|
+
if self._closed:
|
|
38
|
+
raise RuntimeError("session is closed")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class AsyncQueueSession(QueueSession):
|
|
42
|
+
async def close(self) -> None: # type: ignore[override]
|
|
43
|
+
super().close()
|