leanqueue 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.
- leanqueue-0.2.0/.cargo/config.toml +4 -0
- leanqueue-0.2.0/.github/workflows/ci.yml +58 -0
- leanqueue-0.2.0/.github/workflows/release.yml +206 -0
- leanqueue-0.2.0/.gitignore +10 -0
- leanqueue-0.2.0/AGENTS.md +37 -0
- leanqueue-0.2.0/CLIENT.md +234 -0
- leanqueue-0.2.0/Cargo.lock +274 -0
- leanqueue-0.2.0/Cargo.toml +34 -0
- leanqueue-0.2.0/HISTORY.md +92 -0
- leanqueue-0.2.0/LICENSE +21 -0
- leanqueue-0.2.0/PKG-INFO +476 -0
- leanqueue-0.2.0/README.md +437 -0
- leanqueue-0.2.0/REPO.md +33 -0
- leanqueue-0.2.0/build.rs +9 -0
- leanqueue-0.2.0/example.py +55 -0
- leanqueue-0.2.0/leanqueue_server/__init__.py +14 -0
- leanqueue-0.2.0/leanqueue_server/cli.py +71 -0
- leanqueue-0.2.0/leanqueue_server/client.py +216 -0
- leanqueue-0.2.0/leanqueue_server/leanqueue.proto +99 -0
- leanqueue-0.2.0/leanqueue_server/leanqueue_pb2.py +82 -0
- leanqueue-0.2.0/leanqueue_server/leanqueue_pb2_grpc.py +662 -0
- leanqueue-0.2.0/leanqueue_server/serve.py +529 -0
- leanqueue-0.2.0/leanqueue_server/serve_grpc.py +339 -0
- leanqueue-0.2.0/leanqueue_server/serve_guardrails.py +441 -0
- leanqueue-0.2.0/leanqueue_server/service.py +421 -0
- leanqueue-0.2.0/leanqueuedb.py +523 -0
- leanqueue-0.2.0/pyproject.toml +74 -0
- leanqueue-0.2.0/requirements.txt +1 -0
- leanqueue-0.2.0/scripts/gen_grpc.py +37 -0
- leanqueue-0.2.0/scripts/server_smoke.py +124 -0
- leanqueue-0.2.0/src/compact.rs +503 -0
- leanqueue-0.2.0/src/ctl.rs +230 -0
- leanqueue-0.2.0/src/frame.rs +675 -0
- leanqueue-0.2.0/src/integrity.rs +90 -0
- leanqueue-0.2.0/src/lib.rs +215 -0
- leanqueue-0.2.0/src/proc_lock.rs +242 -0
- leanqueue-0.2.0/src/queue.rs +2204 -0
- leanqueue-0.2.0/src/sync.rs +405 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
test:
|
|
14
|
+
runs-on: ${{ matrix.os }}
|
|
15
|
+
# A hang must fail the job with a traceback, not hold a runner for 6 hours. The
|
|
16
|
+
# thread timeout method (the only one on Windows) exits pytest, so failures are
|
|
17
|
+
# printed as they happen (--instafail), not only in the final summary.
|
|
18
|
+
timeout-minutes: 45
|
|
19
|
+
strategy:
|
|
20
|
+
fail-fast: false
|
|
21
|
+
matrix:
|
|
22
|
+
include:
|
|
23
|
+
- os: ubuntu-latest
|
|
24
|
+
python-version: '3.8'
|
|
25
|
+
- os: ubuntu-latest
|
|
26
|
+
python-version: '3.13'
|
|
27
|
+
- os: macos-latest
|
|
28
|
+
python-version: '3.12'
|
|
29
|
+
- os: windows-latest
|
|
30
|
+
python-version: '3.12'
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@v4
|
|
33
|
+
- uses: actions/setup-python@v5
|
|
34
|
+
with:
|
|
35
|
+
python-version: ${{ matrix.python-version }}
|
|
36
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
37
|
+
- name: Rust unit tests
|
|
38
|
+
shell: bash
|
|
39
|
+
run: cargo test --release
|
|
40
|
+
- name: Build and install the wheel
|
|
41
|
+
shell: bash
|
|
42
|
+
run: |
|
|
43
|
+
python -m pip install --upgrade pip maturin
|
|
44
|
+
maturin build --release --out dist
|
|
45
|
+
python -m pip install dist/*.whl pytest pytest-timeout pytest-instafail psutil
|
|
46
|
+
- name: Install the server stacks (3.9+; the stubs need protobuf 7)
|
|
47
|
+
if: matrix.python-version != '3.8'
|
|
48
|
+
shell: bash
|
|
49
|
+
run: python -m pip install "fastapi>=0.110" "uvicorn[standard]>=0.29" httpx "pydantic>=2.8" "grpcio>=1.82.1" "protobuf>=7.35,<8"
|
|
50
|
+
- name: Python suite against the installed wheel (outside the checkout)
|
|
51
|
+
shell: bash
|
|
52
|
+
env:
|
|
53
|
+
LEANQUEUE_COMPAT_INSTALL: ${{ matrix.os == 'ubuntu-latest' && '1' || '0' }}
|
|
54
|
+
run: |
|
|
55
|
+
mkdir -p "$RUNNER_TEMP/run" && cd "$RUNNER_TEMP/run"
|
|
56
|
+
python -m pytest "$GITHUB_WORKSPACE/tests" -v --tb=short -p no:cacheprovider \
|
|
57
|
+
--durations=15 --timeout=600 --timeout-method=thread -o faulthandler_timeout=300 \
|
|
58
|
+
--instafail
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Tags `v*` build, test and publish. A manual run builds and tests every platform wheel
|
|
4
|
+
# but never publishes.
|
|
5
|
+
on:
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
push:
|
|
8
|
+
tags:
|
|
9
|
+
- 'v*'
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
# Nothing is built for other platforms, and nothing is published, until this passes.
|
|
16
|
+
# It builds the extension the way a user gets it (a wheel), installs THAT wheel into a
|
|
17
|
+
# clean environment and runs the Rust tests plus the whole Python suite against it
|
|
18
|
+
# from outside the source tree, so a wheel missing leanqueuedb.py or the server
|
|
19
|
+
# package, or a native module that does not match the facade, fails here.
|
|
20
|
+
test:
|
|
21
|
+
name: Test (Rust + Python against an installed wheel)
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v4
|
|
25
|
+
- uses: actions/setup-python@v5
|
|
26
|
+
with:
|
|
27
|
+
python-version: '3.12'
|
|
28
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
29
|
+
|
|
30
|
+
- name: Rust unit tests
|
|
31
|
+
run: cargo test --release
|
|
32
|
+
|
|
33
|
+
- name: Build the wheel
|
|
34
|
+
uses: PyO3/maturin-action@v1
|
|
35
|
+
with:
|
|
36
|
+
args: --release --out dist
|
|
37
|
+
sccache: 'true'
|
|
38
|
+
manylinux: auto
|
|
39
|
+
|
|
40
|
+
- name: Install the wheel (with server extras) and the test dependencies
|
|
41
|
+
run: |
|
|
42
|
+
python -m pip install --upgrade pip
|
|
43
|
+
WHEEL="$(ls dist/*.whl)"
|
|
44
|
+
python -m pip install "${WHEEL}[server,grpc]"
|
|
45
|
+
python -m pip install pytest "psutil>=5.9" httpx "grpcio-tools==1.82.1"
|
|
46
|
+
|
|
47
|
+
- name: Verify the checked-in gRPC stubs match the proto and the pinned toolchain
|
|
48
|
+
run: |
|
|
49
|
+
python scripts/gen_grpc.py
|
|
50
|
+
git diff --exit-code -- leanqueue_server/leanqueue_pb2.py leanqueue_server/leanqueue_pb2_grpc.py
|
|
51
|
+
|
|
52
|
+
- name: Import the installed wheel from outside the checkout
|
|
53
|
+
run: |
|
|
54
|
+
cd /tmp
|
|
55
|
+
python -c "
|
|
56
|
+
import leanqueue, leanqueuedb, sys, tempfile
|
|
57
|
+
assert 'leanqueue_server' not in sys.modules
|
|
58
|
+
print(leanqueuedb.__file__, leanqueue.__version__)
|
|
59
|
+
with tempfile.TemporaryDirectory() as d:
|
|
60
|
+
q = leanqueuedb.Queue(d)
|
|
61
|
+
q.put(b'hello')
|
|
62
|
+
m = q.get(); assert m.body == b'hello'; m.ack()
|
|
63
|
+
q.close()
|
|
64
|
+
print('wheel round trip ok')
|
|
65
|
+
"
|
|
66
|
+
|
|
67
|
+
- name: Exercise the packaged REST + gRPC servers from outside the checkout
|
|
68
|
+
run: |
|
|
69
|
+
cd /tmp
|
|
70
|
+
leanqueue-serve --help >/dev/null
|
|
71
|
+
leanqueue-grpc --help >/dev/null
|
|
72
|
+
python "$GITHUB_WORKSPACE/scripts/server_smoke.py"
|
|
73
|
+
|
|
74
|
+
- name: Run the full Python suite against the installed wheel
|
|
75
|
+
env:
|
|
76
|
+
# test_compat_published.py installs leanqueue 0.1.0 into a throwaway venv and
|
|
77
|
+
# checks old-writes/new-reads, new-writes/old-reads and mixed processes.
|
|
78
|
+
LEANQUEUE_COMPAT_INSTALL: '1'
|
|
79
|
+
run: |
|
|
80
|
+
mkdir -p /tmp/run && cd /tmp/run
|
|
81
|
+
timeout --signal=INT --kill-after=30s 20m python -m pytest "$GITHUB_WORKSPACE/tests" \
|
|
82
|
+
-vv -x --tb=short -o faulthandler_timeout=120 -p no:cacheprovider
|
|
83
|
+
|
|
84
|
+
linux:
|
|
85
|
+
needs: [test]
|
|
86
|
+
runs-on: ubuntu-latest
|
|
87
|
+
strategy:
|
|
88
|
+
matrix:
|
|
89
|
+
target: [x86_64, x86, aarch64, armv7]
|
|
90
|
+
steps:
|
|
91
|
+
- uses: actions/checkout@v4
|
|
92
|
+
- uses: actions/setup-python@v5
|
|
93
|
+
with:
|
|
94
|
+
python-version: '3.10'
|
|
95
|
+
- name: Build wheels
|
|
96
|
+
uses: PyO3/maturin-action@v1
|
|
97
|
+
with:
|
|
98
|
+
target: ${{ matrix.target }}
|
|
99
|
+
args: --release --out dist
|
|
100
|
+
sccache: 'true'
|
|
101
|
+
manylinux: auto
|
|
102
|
+
- name: Upload wheels
|
|
103
|
+
uses: actions/upload-artifact@v4
|
|
104
|
+
with:
|
|
105
|
+
name: wheels-linux-${{ matrix.target }}
|
|
106
|
+
path: dist
|
|
107
|
+
|
|
108
|
+
windows:
|
|
109
|
+
needs: [test]
|
|
110
|
+
runs-on: windows-latest
|
|
111
|
+
strategy:
|
|
112
|
+
matrix:
|
|
113
|
+
target: [x64]
|
|
114
|
+
steps:
|
|
115
|
+
- uses: actions/checkout@v4
|
|
116
|
+
- uses: actions/setup-python@v5
|
|
117
|
+
with:
|
|
118
|
+
python-version: '3.10'
|
|
119
|
+
architecture: ${{ matrix.target }}
|
|
120
|
+
- name: Build wheels
|
|
121
|
+
uses: PyO3/maturin-action@v1
|
|
122
|
+
with:
|
|
123
|
+
target: ${{ matrix.target }}
|
|
124
|
+
args: --release --out dist
|
|
125
|
+
sccache: 'true'
|
|
126
|
+
- name: Smoke-test the Windows wheel
|
|
127
|
+
shell: bash
|
|
128
|
+
run: |
|
|
129
|
+
python -m pip install dist/*.whl
|
|
130
|
+
cd "$RUNNER_TEMP"
|
|
131
|
+
python -c "
|
|
132
|
+
import leanqueuedb, tempfile, os
|
|
133
|
+
d = tempfile.mkdtemp()
|
|
134
|
+
q = leanqueuedb.Queue(d); q.put_many([b'a', b'b']); q.close()
|
|
135
|
+
q = leanqueuedb.Queue(d); m = q.get(); assert m.body == b'a'; m.ack()
|
|
136
|
+
q.compact(); assert q.available() == 1; assert q.verify_integrity()['ok']; q.close()
|
|
137
|
+
print('windows wheel ok')
|
|
138
|
+
"
|
|
139
|
+
- name: Upload wheels
|
|
140
|
+
uses: actions/upload-artifact@v4
|
|
141
|
+
with:
|
|
142
|
+
name: wheels-windows-${{ matrix.target }}
|
|
143
|
+
path: dist
|
|
144
|
+
|
|
145
|
+
macos:
|
|
146
|
+
needs: [test]
|
|
147
|
+
runs-on: macos-latest
|
|
148
|
+
strategy:
|
|
149
|
+
matrix:
|
|
150
|
+
target: [x86_64, aarch64]
|
|
151
|
+
steps:
|
|
152
|
+
- uses: actions/checkout@v4
|
|
153
|
+
- uses: actions/setup-python@v5
|
|
154
|
+
with:
|
|
155
|
+
python-version: '3.10'
|
|
156
|
+
- name: Build wheels
|
|
157
|
+
uses: PyO3/maturin-action@v1
|
|
158
|
+
with:
|
|
159
|
+
target: ${{ matrix.target }}
|
|
160
|
+
args: --release --out dist
|
|
161
|
+
sccache: 'true'
|
|
162
|
+
- name: Upload wheels
|
|
163
|
+
uses: actions/upload-artifact@v4
|
|
164
|
+
with:
|
|
165
|
+
name: wheels-macos-${{ matrix.target }}
|
|
166
|
+
path: dist
|
|
167
|
+
|
|
168
|
+
sdist:
|
|
169
|
+
needs: [test]
|
|
170
|
+
runs-on: ubuntu-latest
|
|
171
|
+
steps:
|
|
172
|
+
- uses: actions/checkout@v4
|
|
173
|
+
- name: Build sdist
|
|
174
|
+
uses: PyO3/maturin-action@v1
|
|
175
|
+
with:
|
|
176
|
+
command: sdist
|
|
177
|
+
args: --out dist
|
|
178
|
+
- name: Upload sdist
|
|
179
|
+
uses: actions/upload-artifact@v4
|
|
180
|
+
with:
|
|
181
|
+
name: wheels-sdist
|
|
182
|
+
path: dist
|
|
183
|
+
|
|
184
|
+
pypi-publish:
|
|
185
|
+
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
|
|
186
|
+
name: Publish to PyPI
|
|
187
|
+
needs: [test, linux, windows, macos, sdist]
|
|
188
|
+
runs-on: ubuntu-latest
|
|
189
|
+
environment:
|
|
190
|
+
name: pypi
|
|
191
|
+
url: https://pypi.org/p/leanqueue
|
|
192
|
+
# Publishing uses the token below, not OIDC trusted publishing.
|
|
193
|
+
steps:
|
|
194
|
+
- uses: actions/download-artifact@v4
|
|
195
|
+
with:
|
|
196
|
+
pattern: wheels-*
|
|
197
|
+
merge-multiple: true
|
|
198
|
+
path: dist
|
|
199
|
+
|
|
200
|
+
- name: Publish to PyPI
|
|
201
|
+
uses: PyO3/maturin-action@v1
|
|
202
|
+
env:
|
|
203
|
+
MATURIN_PYPI_TOKEN: ${{ secrets.PYPIV2 }}
|
|
204
|
+
with:
|
|
205
|
+
command: upload
|
|
206
|
+
args: --non-interactive --skip-existing dist/*
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Agent Directives: State & Memory Management
|
|
2
|
+
|
|
3
|
+
## 1. Initialization & Core Operating Principle
|
|
4
|
+
**Initialization Protocol:** On your very first interaction within any workspace, check the root directory for the existence of `HISTORY.md` and `REPO.md`. If either file is missing, you must create and initialize them immediately before executing any other commands or tasks.
|
|
5
|
+
|
|
6
|
+
**Core Principle:** You are responsible for maintaining the repository's internal memory. You will not write these files for human consumption. You will encode all information using a Sparse Priming Representation (SPR) methodology to optimize for future LLM context ingestion.
|
|
7
|
+
|
|
8
|
+
### SPR Encoding Rules:
|
|
9
|
+
* **Maximize Density:** Distill all features, decisions, and chronological events into succinct statements, conceptual associations, and technical assertions.
|
|
10
|
+
* **Minimize Tokens:** Capture the absolute maximum amount of conceptual meaning with the absolute minimum number of words.
|
|
11
|
+
* **Target Latent Space:** Use precise technical vocabulary, metaphors, or architectural analogies that efficiently prime an LLM's internal state. Avoid conversational filler. Use complete sentences.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 2. HISTORY.md (Temporal State & Dynamic Compression)
|
|
16
|
+
**Objective:** Maintain a strict, chronological ledger of repository modifications while rigorously actively managing the token footprint.
|
|
17
|
+
|
|
18
|
+
**Instructions:**
|
|
19
|
+
* **Event Logging:** Append a new entry upon the completion of any task, refactor, or architectural shift. Encode the delta (code state shifts, resolved edge cases, logic pathways) strictly using SPR. Do not use standard git-style commit messages.
|
|
20
|
+
* **Temporal Compression (The "Roll-up"):** To prevent linear context bloat, you must actively compress older history.
|
|
21
|
+
* Monitor the depth of the history log. If the file grows too long (e.g., exceeding 20-30 distinct entries), initiate a compression cycle.
|
|
22
|
+
* Condense the oldest 80% of the entries into a single, ultra-dense SPR macro-summary under a `# Legacy Epoch` header.
|
|
23
|
+
* Retain the most recent 20% of entries in their distinct chronological format for immediate context.
|
|
24
|
+
* Apply this fractal compression continuously as the repository ages, rolling older epochs into denser concepts while keeping recent context highly granular.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 3. REPO.md (Macro State)
|
|
29
|
+
**Objective:** Maintain a live, highly compressed snapshot of the repository's architecture and purpose.
|
|
30
|
+
|
|
31
|
+
**Instructions:**
|
|
32
|
+
* Continuously update `REPO.md` to reflect the current macro-state of the project.
|
|
33
|
+
* Whenever a new feature is added, an architectural decision is made, or the project boundaries shift, rewrite the relevant sections using SPR encoding.
|
|
34
|
+
* Ensure the file always contains these three conceptual clusters:
|
|
35
|
+
* **Features:** Active capabilities, I/O boundaries, and core operational loops.
|
|
36
|
+
* **Decisions:** Technical trade-offs, architecture selections, hardware/environment optimizations, and accepted constraints.
|
|
37
|
+
* **Scopes:** Project boundaries, target use cases, and explicit anti-goals (what the system is specifically designed *not* to do).
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# LeanQueue servers: client guide
|
|
2
|
+
|
|
3
|
+
LeanQueue is an embedded library first. The REST and gRPC servers expose the same
|
|
4
|
+
queues to programs that cannot import it (other languages, other hosts). Both ship in
|
|
5
|
+
the wheel; their dependencies are extras:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install "leanqueue[server]" # REST: leanqueue-serve
|
|
9
|
+
pip install "leanqueue[grpc]" # gRPC: leanqueue-grpc
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Both servers are backed by one `leanqueuedb.Broker` rooted at the data directory. Each
|
|
13
|
+
queue is a subdirectory, the same layout the library uses, so a server and local
|
|
14
|
+
processes can share a data directory (all of them coordinate through the queue's file
|
|
15
|
+
lock). REST, gRPC and the library return the same results for the same operations:
|
|
16
|
+
the test suite checks this.
|
|
17
|
+
|
|
18
|
+
## Running
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
leanqueue-serve --data-dir /var/lib/queues # REST on 127.0.0.1:8004
|
|
22
|
+
leanqueue-grpc --data-dir /var/lib/queues # gRPC on 127.0.0.1:50054
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The flags set the environment variables listed below. SIGINT and SIGTERM shut a
|
|
26
|
+
server down gracefully: in-flight requests finish, open gRPC consume streams return
|
|
27
|
+
their unacked messages to the queue, and every queue is fsynced and closed.
|
|
28
|
+
|
|
29
|
+
## Concepts
|
|
30
|
+
|
|
31
|
+
* **Declare before use.** `PUT /queues/{name}` (REST) or `DeclareQueue` (gRPC) creates
|
|
32
|
+
a queue. Any other operation on an undeclared queue returns 404 / `NOT_FOUND`. A
|
|
33
|
+
mistyped name therefore cannot create a queue that nobody consumes.
|
|
34
|
+
* **Queue names** have 1 to 255 characters from `A-Z a-z 0-9 - _ .`. The names `.` and
|
|
35
|
+
`..` are not allowed. Other names return 400 / `INVALID_ARGUMENT`.
|
|
36
|
+
* **Leases.** A get leases messages for `lease_ms` milliseconds. The default is the
|
|
37
|
+
queue's lease (30 s, or `LEANQUEUE_DEFAULT_LEASE_MS`). Settle each message with ack
|
|
38
|
+
(done), nack (requeue it, or drop it), or extend (renew the lease). If a lease
|
|
39
|
+
expires, the message is delivered again and its `deliveries` count goes up. Delivery
|
|
40
|
+
is at-least-once.
|
|
41
|
+
* **Sequence ids** (`seq`) identify messages for ack, nack, extend and states.
|
|
42
|
+
|
|
43
|
+
## REST API
|
|
44
|
+
|
|
45
|
+
All bodies are JSON. Replies are JSON. Unknown body fields and unknown query parameters
|
|
46
|
+
are rejected with 422. Numbers must be JSON integers: `true`, `"5"` or `1.5` where an
|
|
47
|
+
integer is expected is also a 422.
|
|
48
|
+
|
|
49
|
+
| Method + path | Body | Reply |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| `GET /health` | | `{"ready", "version", "limits"}` (never needs a token) |
|
|
52
|
+
| `GET /queues[?stats=true]` | | `{"queues": [names]}` (+ `"stats": {name: stats}`) |
|
|
53
|
+
| `PUT /queues/{q}` | | `{"queue", "created": bool}` |
|
|
54
|
+
| `DELETE /queues/{q}` | | `{"deleted": true}` (404 if absent) |
|
|
55
|
+
| `GET /queues/{q}/stats` | | the `Queue.stats()` dict |
|
|
56
|
+
| `POST /queues/{q}/publish` | one of `{"body": str}`, `{"body_b64": str}`, `{"json": any}` | `{"seq"}` |
|
|
57
|
+
| `POST /queues/{q}/publish_batch` | `{"messages": [<same shape>, ...]}` | `{"seqs": [...]}` |
|
|
58
|
+
| `POST /queues/{q}/get` | `{"max_messages": 1, "lease_ms": null, "wait_ms": 0, "encoding": "base64"}` | `{"messages": [...]}` |
|
|
59
|
+
| `POST /queues/{q}/ack` | `{"seqs": [...]}` | `{"acked": n}` |
|
|
60
|
+
| `POST /queues/{q}/nack` | `{"seqs": [...], "requeue": true}` | `{"nacked": n}` |
|
|
61
|
+
| `POST /queues/{q}/extend` | `{"seqs": [...], "lease_ms": null}` | `{"extended": n}` |
|
|
62
|
+
| `POST /queues/{q}/states` | `{"seqs": [...]}` | `{"states": [null or {"state", "deliveries", "lease_until_ms"}]}` |
|
|
63
|
+
| `POST /queues/{q}/purge` | | `{"purged": true}` |
|
|
64
|
+
|
|
65
|
+
Message bodies:
|
|
66
|
+
|
|
67
|
+
* Publish: `body` is stored as UTF-8 text, `body_b64` as the decoded bytes, and `json`
|
|
68
|
+
as the compact JSON encoding of the value.
|
|
69
|
+
* Get: each message is `{"seq", "deliveries", "enqueued_at_ms", ...}` plus the body.
|
|
70
|
+
With `"encoding": "base64"` (the default) the body is in `body_b64`. With `"text"` it
|
|
71
|
+
is in `body`, and with `"json"` it is the parsed value in `json`. If a body is not
|
|
72
|
+
valid in the requested encoding, it falls back to `body_b64`.
|
|
73
|
+
|
|
74
|
+
`wait_ms` > 0 turns a get on an empty queue into a long poll. The server checks the
|
|
75
|
+
queue every `LEANQUEUE_POLL_MS` (20 ms) until a message arrives or the wait ends.
|
|
76
|
+
Waiting does not block the server. If the client disconnects after messages were
|
|
77
|
+
leased, those leases expire and the messages are delivered again.
|
|
78
|
+
|
|
79
|
+
`publish_batch`, a get of several messages, and the ack, nack and extend calls each
|
|
80
|
+
use one write and one fsync.
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
curl -X PUT localhost:8004/queues/jobs
|
|
84
|
+
curl -X POST localhost:8004/queues/jobs/publish -H 'content-type: application/json' \
|
|
85
|
+
-d '{"json": {"task": "resize", "id": 42}}'
|
|
86
|
+
curl -X POST localhost:8004/queues/jobs/get -H 'content-type: application/json' \
|
|
87
|
+
-d '{"max_messages": 10, "wait_ms": 5000, "encoding": "json"}'
|
|
88
|
+
curl -X POST localhost:8004/queues/jobs/ack -H 'content-type: application/json' \
|
|
89
|
+
-d '{"seqs": [0]}'
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from leanqueue_server.client import LeanQueueRestClient
|
|
94
|
+
|
|
95
|
+
with LeanQueueRestClient("http://127.0.0.1:8004", token=None) as r:
|
|
96
|
+
r.declare("jobs")
|
|
97
|
+
r.publish("jobs", b"payload")
|
|
98
|
+
for m in r.get("jobs", max_messages=10, wait_ms=1000):
|
|
99
|
+
handle(m["body"]) # bytes
|
|
100
|
+
r.ack("jobs", [m["seq"]])
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## gRPC API
|
|
104
|
+
|
|
105
|
+
The service `leanqueue.LeanQueue` is defined in `leanqueue_server/leanqueue.proto`.
|
|
106
|
+
Bodies are `bytes` and stats are a JSON string.
|
|
107
|
+
|
|
108
|
+
| RPC | Request | Reply |
|
|
109
|
+
|---|---|---|
|
|
110
|
+
| `Health` | | `ready, version, limits_json` (never needs a token) |
|
|
111
|
+
| `ListQueues` | | `queues` |
|
|
112
|
+
| `DeclareQueue` / `DeleteQueue` | `queue` | `created` / `deleted` |
|
|
113
|
+
| `Stats` | `queue` | `stats_json` |
|
|
114
|
+
| `Publish` | `queue, body` | `seq` |
|
|
115
|
+
| `PublishBatch` | `queue, bodies` | `seqs` |
|
|
116
|
+
| `Get` | `queue, max_messages, lease_ms?, wait_ms` | `messages` (`Delivery`) |
|
|
117
|
+
| `Ack` | `queue, seqs` | `count` |
|
|
118
|
+
| `Nack` | `queue, seqs, drop` (`drop=false` requeues) | `count` |
|
|
119
|
+
| `Extend` | `queue, seqs, lease_ms?` | `count` |
|
|
120
|
+
| `States` | `queue, seqs` | `states` (`exists, in_flight, deliveries, lease_until_ms`) |
|
|
121
|
+
| `Purge` | `queue` | |
|
|
122
|
+
| `Consume` (server stream) | `queue, prefetch, lease_ms?, idle_timeout_ms` | stream of `Delivery` |
|
|
123
|
+
|
|
124
|
+
`Consume` pushes messages as they become available. At most `prefetch` delivered
|
|
125
|
+
messages can be unsettled at a time. Settle them with `Ack`, `Nack` or `Extend` on the
|
|
126
|
+
same or another channel. A message also stops counting against the window when its
|
|
127
|
+
lease expires.
|
|
128
|
+
|
|
129
|
+
When the stream ends, the server requeues the messages that are still leased to it.
|
|
130
|
+
This happens on a client cancel, a deadline, `idle_timeout_ms` with nothing to deliver,
|
|
131
|
+
or a server shutdown. It is the same behaviour as a RabbitMQ consumer whose channel
|
|
132
|
+
closes.
|
|
133
|
+
|
|
134
|
+
Each open stream holds one server worker thread, so the number of open streams is
|
|
135
|
+
limited (see `LEANQUEUE_GRPC_MAX_STREAMS`).
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from leanqueue_server.client import LeanQueueClient
|
|
139
|
+
|
|
140
|
+
with LeanQueueClient("127.0.0.1:50054", token=None) as c:
|
|
141
|
+
c.declare("jobs")
|
|
142
|
+
c.publish_batch("jobs", [b"a", b"b"])
|
|
143
|
+
for d in c.consume("jobs", prefetch=10): # dicts: seq, deliveries, enqueued_at_ms, body
|
|
144
|
+
handle(d["body"])
|
|
145
|
+
c.ack("jobs", [d["seq"]])
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Clients in other languages can generate stubs from `leanqueue.proto`. To regenerate the
|
|
149
|
+
Python stubs after a proto change, run `python scripts/gen_grpc.py` (needs
|
|
150
|
+
`grpcio-tools==1.82.1`).
|
|
151
|
+
|
|
152
|
+
## Authentication and TLS
|
|
153
|
+
|
|
154
|
+
* Both servers bind `127.0.0.1` by default.
|
|
155
|
+
* REST on any other address needs `LEANQUEUE_API_TOKEN`, or
|
|
156
|
+
`LEANQUEUE_ALLOW_INSECURE=1` to declare the network trusted. Otherwise the server
|
|
157
|
+
does not start.
|
|
158
|
+
* gRPC on any other address needs TLS (`LEANQUEUE_GRPC_TLS_CERT` and
|
|
159
|
+
`LEANQUEUE_GRPC_TLS_KEY`, PEM files) **and** `LEANQUEUE_API_TOKEN`, or
|
|
160
|
+
`LEANQUEUE_ALLOW_INSECURE=1`.
|
|
161
|
+
* When a token is set, every request except health must carry
|
|
162
|
+
`Authorization: Bearer <token>`. For gRPC, send it as the `authorization` metadata
|
|
163
|
+
entry. This applies to unary calls and streams. A missing or wrong token returns
|
|
164
|
+
401 / `UNAUTHENTICATED`. Tokens are compared in constant time.
|
|
165
|
+
* The token is read only from the environment, never from a flag, so it does not appear
|
|
166
|
+
in `ps`.
|
|
167
|
+
|
|
168
|
+
## Limits
|
|
169
|
+
|
|
170
|
+
Static caps can be changed with environment variables. A request over a cap is refused
|
|
171
|
+
before it reaches the queue.
|
|
172
|
+
|
|
173
|
+
| Env var | Default | Applies to |
|
|
174
|
+
|---|---|---|
|
|
175
|
+
| `LEANQUEUE_MAX_REQUEST_BYTES` | 128 MiB | Whole REST body (413), and the gRPC max message size |
|
|
176
|
+
| `LEANQUEUE_MAX_BODY_BYTES` | 512 MiB − 21 | One message body (enforced by the queue core, 400; it can only be lowered) |
|
|
177
|
+
| `LEANQUEUE_MAX_BATCH` | 10 000 | Messages per publish batch (429) |
|
|
178
|
+
| `LEANQUEUE_MAX_PREFETCH` | 1 000 | `max_messages` per get and `prefetch` per consume (429). Also limited by memory, see below |
|
|
179
|
+
| `LEANQUEUE_MAX_WAIT_MS` | 30 000 | Long-poll `wait_ms` (429) |
|
|
180
|
+
| `LEANQUEUE_MAX_SEQS` | 100 000 | Sequence ids per ack, nack, extend or states call (429) |
|
|
181
|
+
| `LEANQUEUE_MEM_FRACTION` | 0.5 | Share of available RAM used for the memory-aware caps |
|
|
182
|
+
| `LEANQUEUE_PER_RESULT_BYTES` | 4096 | Assumed memory per delivered message, for the memory-aware get cap |
|
|
183
|
+
| `LEANQUEUE_MIN_GET` | 16 | The memory-aware get cap never goes below this (unless memory is truly short) |
|
|
184
|
+
| `LEANQUEUE_BODY_MEM_FRACTION` | = MEM_FRACTION | Memory budget shared by REST bodies that are being parsed |
|
|
185
|
+
| `LEANQUEUE_MEM_REFRESH_SECS` | 1.0 | How often the memory probe runs |
|
|
186
|
+
|
|
187
|
+
Memory-aware caps:
|
|
188
|
+
|
|
189
|
+
* Available memory is the smaller of `MemAvailable` and the container's cgroup v1 or v2
|
|
190
|
+
allowance.
|
|
191
|
+
* The number of messages per get is limited by that memory, divided among the workers.
|
|
192
|
+
* The bodies in one get reply are limited to a byte budget, also derived from that
|
|
193
|
+
memory. A get stops early when the budget is used up. It always returns at least
|
|
194
|
+
one message, and it can go over the budget by at most one chunk of 64 messages.
|
|
195
|
+
|
|
196
|
+
REST body admission:
|
|
197
|
+
|
|
198
|
+
* A request body is admitted before it is parsed. A declared `Content-Length` over the
|
|
199
|
+
cap gets 413.
|
|
200
|
+
* A body that does not fit the current memory budget gets 429 with `Retry-After`.
|
|
201
|
+
* Chunked bodies are measured as they arrive. Bodies with `Transfer-Encoding` are not
|
|
202
|
+
trusted to match their `Content-Length`.
|
|
203
|
+
|
|
204
|
+
## Server settings
|
|
205
|
+
|
|
206
|
+
| Env var | Default | |
|
|
207
|
+
|---|---|---|
|
|
208
|
+
| `LEANQUEUE_DATA_DIR` | `./leanqueue_data` | Broker root (`--data-dir`) |
|
|
209
|
+
| `HOST` / `PORT` | `127.0.0.1` / `8004` | REST bind address (`--host`, `--port`) |
|
|
210
|
+
| `GRPC_HOST` / `GRPC_PORT` | `127.0.0.1` / `50054` | gRPC bind address |
|
|
211
|
+
| `LEANQUEUE_THREADS` | min(64, 2×CPUs) | REST worker threads |
|
|
212
|
+
| `LEANQUEUE_GRPC_WORKERS` | min(64, 2×CPUs) | gRPC worker threads |
|
|
213
|
+
| `LEANQUEUE_GRPC_MAX_STREAMS` | workers / 2 | Open `Consume` streams (`RESOURCE_EXHAUSTED` past it) |
|
|
214
|
+
| `LEANQUEUE_GRPC_MAX_CONCURRENT_RPCS` | unset (unbounded) | Opt-in limit on running plus queued RPCs |
|
|
215
|
+
| `LEANQUEUE_POLL_MS` | 20 | Poll interval of long-poll gets and consume streams |
|
|
216
|
+
| `LEANQUEUE_SYNC_INTERVAL_MS` | 0 | Queue durability: 0 fsyncs each write; N fsyncs in the background within N ms (`--sync-interval-ms`) |
|
|
217
|
+
| `LEANQUEUE_DEFAULT_LEASE_MS` | 30 000 | Default lease of the queues |
|
|
218
|
+
| `LEANQUEUE_AUTO_COMPACT_BYTES` | 64 MiB | Auto-compaction threshold of the queues |
|
|
219
|
+
| `LEANQUEUE_API_TOKEN`, `LEANQUEUE_ALLOW_INSECURE`, `LEANQUEUE_GRPC_TLS_CERT`, `LEANQUEUE_GRPC_TLS_KEY` | unset | See Authentication |
|
|
220
|
+
|
|
221
|
+
gRPC connections send keepalive pings (every 60 s, 20 s timeout). This cancels the
|
|
222
|
+
streams of a client that vanished without closing its connection.
|
|
223
|
+
|
|
224
|
+
## Errors
|
|
225
|
+
|
|
226
|
+
| Kind | HTTP | gRPC | Typical cause | Retry? |
|
|
227
|
+
|---|---|---|---|---|
|
|
228
|
+
| not found | 404 | `NOT_FOUND` | Queue not declared, or deleted | No |
|
|
229
|
+
| invalid | 400 (422 for a malformed body or query) | `INVALID_ARGUMENT` | Bad queue name, bad base64, negative seq, body over `LEANQUEUE_MAX_BODY_BYTES` | No |
|
|
230
|
+
| over capacity | 413 (body too large), 429 | `RESOURCE_EXHAUSTED` | A cap above, the memory budget, or too many streams | Yes (429 has `Retry-After: 1`) |
|
|
231
|
+
| unavailable | 503 | `UNAVAILABLE` | Process-lock timeout (busy peer), I/O error such as ENOSPC, queue closed under the request | Yes (`Retry-After: 1`) |
|
|
232
|
+
| internal | 500 | `INTERNAL` | `CorruptionError`, `UnsupportedFormatError`, `DurabilityError`: an operator must look at the queue (see docs/RECOVERY.md) | No |
|
|
233
|
+
|
|
234
|
+
Error details are limited to 512 characters and never repeat request data back.
|