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.
Files changed (38) hide show
  1. leanqueue-0.2.0/.cargo/config.toml +4 -0
  2. leanqueue-0.2.0/.github/workflows/ci.yml +58 -0
  3. leanqueue-0.2.0/.github/workflows/release.yml +206 -0
  4. leanqueue-0.2.0/.gitignore +10 -0
  5. leanqueue-0.2.0/AGENTS.md +37 -0
  6. leanqueue-0.2.0/CLIENT.md +234 -0
  7. leanqueue-0.2.0/Cargo.lock +274 -0
  8. leanqueue-0.2.0/Cargo.toml +34 -0
  9. leanqueue-0.2.0/HISTORY.md +92 -0
  10. leanqueue-0.2.0/LICENSE +21 -0
  11. leanqueue-0.2.0/PKG-INFO +476 -0
  12. leanqueue-0.2.0/README.md +437 -0
  13. leanqueue-0.2.0/REPO.md +33 -0
  14. leanqueue-0.2.0/build.rs +9 -0
  15. leanqueue-0.2.0/example.py +55 -0
  16. leanqueue-0.2.0/leanqueue_server/__init__.py +14 -0
  17. leanqueue-0.2.0/leanqueue_server/cli.py +71 -0
  18. leanqueue-0.2.0/leanqueue_server/client.py +216 -0
  19. leanqueue-0.2.0/leanqueue_server/leanqueue.proto +99 -0
  20. leanqueue-0.2.0/leanqueue_server/leanqueue_pb2.py +82 -0
  21. leanqueue-0.2.0/leanqueue_server/leanqueue_pb2_grpc.py +662 -0
  22. leanqueue-0.2.0/leanqueue_server/serve.py +529 -0
  23. leanqueue-0.2.0/leanqueue_server/serve_grpc.py +339 -0
  24. leanqueue-0.2.0/leanqueue_server/serve_guardrails.py +441 -0
  25. leanqueue-0.2.0/leanqueue_server/service.py +421 -0
  26. leanqueue-0.2.0/leanqueuedb.py +523 -0
  27. leanqueue-0.2.0/pyproject.toml +74 -0
  28. leanqueue-0.2.0/requirements.txt +1 -0
  29. leanqueue-0.2.0/scripts/gen_grpc.py +37 -0
  30. leanqueue-0.2.0/scripts/server_smoke.py +124 -0
  31. leanqueue-0.2.0/src/compact.rs +503 -0
  32. leanqueue-0.2.0/src/ctl.rs +230 -0
  33. leanqueue-0.2.0/src/frame.rs +675 -0
  34. leanqueue-0.2.0/src/integrity.rs +90 -0
  35. leanqueue-0.2.0/src/lib.rs +215 -0
  36. leanqueue-0.2.0/src/proc_lock.rs +242 -0
  37. leanqueue-0.2.0/src/queue.rs +2204 -0
  38. leanqueue-0.2.0/src/sync.rs +405 -0
@@ -0,0 +1,4 @@
1
+ # Align loop starts to 32 bytes on x86_64 so a short hot loop (CRC, frame scan) never
2
+ # straddles a 64-byte line by luck of the build (same setting as LeanText/LeanVec).
3
+ [target.'cfg(target_arch = "x86_64")']
4
+ rustflags = ["-C", "llvm-args=-align-loops=32"]
@@ -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,10 @@
1
+ *__pycache__*
2
+ target/
3
+ dist/
4
+ *.tar.gz
5
+ *.log
6
+ *_q/
7
+ *_queue/
8
+ .pytest_cache/
9
+ *.so
10
+ *.pyd
@@ -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.