uring-api 0.1.0rc0__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.
- uring_api-0.1.0rc0/MANIFEST.in +3 -0
- uring_api-0.1.0rc0/PKG-INFO +470 -0
- uring_api-0.1.0rc0/README.md +445 -0
- uring_api-0.1.0rc0/pyproject.toml +61 -0
- uring_api-0.1.0rc0/setup.cfg +4 -0
- uring_api-0.1.0rc0/setup.py +94 -0
- uring_api-0.1.0rc0/src/_uring_api.c +369 -0
- uring_api-0.1.0rc0/src/_uring_api.pyi +110 -0
- uring_api-0.1.0rc0/src/_uring_api_capi.c +416 -0
- uring_api-0.1.0rc0/src/_uring_api_core.c +724 -0
- uring_api-0.1.0rc0/src/_uring_api_dispatch.c +407 -0
- uring_api-0.1.0rc0/src/_uring_api_probe.c +607 -0
- uring_api-0.1.0rc0/src/_uring_api_properties.c +92 -0
- uring_api-0.1.0rc0/src/_uring_api_ring.c +101 -0
- uring_api-0.1.0rc0/src/_uring_api_submit.c +780 -0
- uring_api-0.1.0rc0/src/uring_api/__init__.py +285 -0
- uring_api-0.1.0rc0/src/uring_api/include/uring_api_capi.h +92 -0
- uring_api-0.1.0rc0/src/uring_api/py.typed +0 -0
- uring_api-0.1.0rc0/src/uring_api.egg-info/PKG-INFO +470 -0
- uring_api-0.1.0rc0/src/uring_api.egg-info/SOURCES.txt +22 -0
- uring_api-0.1.0rc0/src/uring_api.egg-info/dependency_links.txt +1 -0
- uring_api-0.1.0rc0/src/uring_api.egg-info/top_level.txt +2 -0
- uring_api-0.1.0rc0/tests/capi_client/uring_api_capi_client.c +530 -0
- uring_api-0.1.0rc0/tests/test_uring_api.py +1470 -0
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: uring-api
|
|
3
|
+
Version: 0.1.0rc0
|
|
4
|
+
Summary: Small Python wrapper around Linux io_uring
|
|
5
|
+
Author-email: Kristjan Valur Jonsson <sweskman@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/kristjanvalur/pytealet/tree/main/packages/uring_api
|
|
8
|
+
Project-URL: Repository, https://github.com/kristjanvalur/pytealet
|
|
9
|
+
Project-URL: Source, https://github.com/kristjanvalur/pytealet/tree/main/packages/uring_api
|
|
10
|
+
Project-URL: Issues, https://github.com/kristjanvalur/pytealet/issues
|
|
11
|
+
Keywords: io_uring,linux,async,proactor
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: Programming Language :: C
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# uring-api
|
|
27
|
+
|
|
28
|
+
`uring-api` is a small Python wrapper around Linux `io_uring`.
|
|
29
|
+
|
|
30
|
+
The goal is deliberately modest: expose enough of the native ring lifecycle,
|
|
31
|
+
socket send/recv submission, completion waiting, and callback delivery to build
|
|
32
|
+
higher-level completion abstractions in Python. It does not implement an event
|
|
33
|
+
loop, scheduler, or asyncio compatibility layer.
|
|
34
|
+
|
|
35
|
+
Future work is tracked in [ROADMAP.md](ROADMAP.md), including queue resizing,
|
|
36
|
+
optional zero-copy receive models, and specialised kernel tuning.
|
|
37
|
+
|
|
38
|
+
## Quick Check
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
import uring_api
|
|
42
|
+
|
|
43
|
+
print(uring_api.probe())
|
|
44
|
+
|
|
45
|
+
with uring_api.Ring() as ring:
|
|
46
|
+
print(ring.fd)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Socket I/O
|
|
50
|
+
|
|
51
|
+
`Ring` currently exposes `submit_recv()`, `submit_recv_multishot()`,
|
|
52
|
+
`submit_send()`, `submit_send_zc()`, `submit_recvmsg()`, `submit_sendto()`,
|
|
53
|
+
`submit_sendmsg()`, `submit_sendmsg_zc()`, `submit_accept()`,
|
|
54
|
+
`submit_accept_multishot()`, `submit_connect()`, `submit_shutdown()`,
|
|
55
|
+
`submit_close()`, `submit_socket()`, and `wait()`. This is the complete baseline
|
|
56
|
+
for Python-oriented socket I/O in `uring-api`: normal sends and receives,
|
|
57
|
+
message-oriented operations, listener accept paths, connection setup, orderly
|
|
58
|
+
shutdown, fd creation/close, cancellation, and the practical multishot server
|
|
59
|
+
cases all have direct wrappers. Each submitted operation carries a Python
|
|
60
|
+
`user_data` object which comes back with its completion.
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
import socket
|
|
64
|
+
import uring_api
|
|
65
|
+
|
|
66
|
+
reader, writer = socket.socketpair()
|
|
67
|
+
try:
|
|
68
|
+
reader.setblocking(False)
|
|
69
|
+
writer.setblocking(False)
|
|
70
|
+
|
|
71
|
+
with uring_api.Ring() as ring:
|
|
72
|
+
token = {"operation": "greeting"}
|
|
73
|
+
buf = bytearray(5)
|
|
74
|
+
ring.submit_recv(reader.fileno(), buf, token)
|
|
75
|
+
writer.send(b"hello")
|
|
76
|
+
|
|
77
|
+
completion = ring.wait(1.0)
|
|
78
|
+
|
|
79
|
+
assert completion is not None
|
|
80
|
+
assert completion.user_data is token
|
|
81
|
+
assert bytes(buf) == b"hello"
|
|
82
|
+
print(completion.res, completion.result)
|
|
83
|
+
finally:
|
|
84
|
+
reader.close()
|
|
85
|
+
writer.close()
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
For sends, `uring-api` keeps the exported buffer alive until the kernel reports
|
|
89
|
+
the completion. That avoids copying the outgoing payload into an internal bytes
|
|
90
|
+
object just to keep memory valid. `submit_send_zc()` uses
|
|
91
|
+
`IORING_OP_SEND_ZC`, while `submit_sendmsg_zc()` uses `IORING_OP_SENDMSG_ZC` for
|
|
92
|
+
the `sendmsg` shape. Their ordinary operation CQE is delivered as the submitted
|
|
93
|
+
`Completion`; the later `IORING_CQE_F_NOTIF` buffer-lifetime CQE is consumed
|
|
94
|
+
internally and releases the retained buffer.
|
|
95
|
+
|
|
96
|
+
`submit_shutdown()` is a socket operation and mirrors `shutdown(fd, how)`.
|
|
97
|
+
`submit_accept()` and `submit_accept_multishot()` accept optional accept flags;
|
|
98
|
+
pass `socket.SOCK_NONBLOCK | socket.SOCK_CLOEXEC` when accepted sockets should
|
|
99
|
+
be ready for proactor ownership without a follow-up `fcntl()` call.
|
|
100
|
+
`submit_close()` is lower-level: pass only a raw fd whose ownership has already
|
|
101
|
+
been transferred away from Python objects such as `socket.socket`, for example
|
|
102
|
+
with `detach()`. Otherwise, Python and the kernel may both believe they own the
|
|
103
|
+
same descriptor.
|
|
104
|
+
|
|
105
|
+
`submit_recv_multishot()` owns an internal provided-buffer ring for the pending
|
|
106
|
+
operation. Each receive CQE is copied into a new Python `bytes` object, the
|
|
107
|
+
selected kernel buffer is recycled right away, and the delivered completion gets
|
|
108
|
+
a `sequence` number so callback users can reconstruct receive order even when
|
|
109
|
+
worker threads dispatch completions out of order. Multishot completions are
|
|
110
|
+
numbered from `0`; normal one-shot completions also report `sequence == 0`.
|
|
111
|
+
|
|
112
|
+
The local liburing headers expose more socket-adjacent operations than this
|
|
113
|
+
wrapper publishes, but those are intentionally outside the core Python-oriented
|
|
114
|
+
surface. Readiness polling is optional for a completion proactor, fixed-buffer
|
|
115
|
+
send variants and public provided-buffer ownership are a poor fit for normal
|
|
116
|
+
Python buffer lifetimes, and socket command or NAPI controls are specialised
|
|
117
|
+
tuning hooks. The one receive-side extension still worth exploring is a
|
|
118
|
+
zero-copy multishot receive model with explicit leased-buffer ownership. Those
|
|
119
|
+
items are tracked in [ROADMAP.md](ROADMAP.md) rather than implied by `probe()`,
|
|
120
|
+
which remains a compact runtime availability check.
|
|
121
|
+
|
|
122
|
+
If the submission queue cannot provide another entry after flushing already
|
|
123
|
+
prepared work to the kernel, submit methods raise `SubmissionQueueFull`. Treat
|
|
124
|
+
that as backpressure rather than as a permanent ring failure: wait for
|
|
125
|
+
completions, then retry or let a higher-level proactor defer the submission.
|
|
126
|
+
|
|
127
|
+
## Checking Availability
|
|
128
|
+
|
|
129
|
+
`io_uring` availability depends on more than the Python package importing
|
|
130
|
+
successfully. The kernel, container sandbox, seccomp profile, and process limits
|
|
131
|
+
can all affect whether a ring can actually be created.
|
|
132
|
+
|
|
133
|
+
Use `probe()` when you want a compact availability and capability dictionary:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
import uring_api
|
|
137
|
+
|
|
138
|
+
probe = uring_api.probe()
|
|
139
|
+
|
|
140
|
+
if probe:
|
|
141
|
+
print("io_uring is available")
|
|
142
|
+
print("capabilities:", probe)
|
|
143
|
+
else:
|
|
144
|
+
print("io_uring is not available")
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Use `is_available()` when you only need a boolean:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
import uring_api
|
|
151
|
+
|
|
152
|
+
if not uring_api.is_available():
|
|
153
|
+
raise RuntimeError("io_uring is not available in this environment")
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`probe()` creates a tiny temporary ring and closes it right away. If that fails,
|
|
157
|
+
it returns an empty dictionary. If it succeeds, the dictionary contains
|
|
158
|
+
`"available": True` plus named optional capabilities such as
|
|
159
|
+
`"IORING_ACCEPT_MULTISHOT"`, `"IORING_RECV_MULTISHOT"`, and
|
|
160
|
+
`"IORING_OP_SEND_ZC"` and `"IORING_OP_SENDMSG_ZC"`. Production code should
|
|
161
|
+
still handle `OSError` when it creates the real ring because limits or sandbox
|
|
162
|
+
policy may differ for larger settings.
|
|
163
|
+
|
|
164
|
+
Pass setup flags to `probe(flags=...)` to check whether this build and kernel
|
|
165
|
+
combination accepts a ring mode before using it for the real ring:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
import uring_api
|
|
169
|
+
|
|
170
|
+
flags = uring_api.IORING_SETUP_SINGLE_ISSUER
|
|
171
|
+
probe = uring_api.probe(flags=flags)
|
|
172
|
+
|
|
173
|
+
if probe:
|
|
174
|
+
print("setup flags accepted")
|
|
175
|
+
else:
|
|
176
|
+
print("setup flags rejected")
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Some flags also impose application-level contracts. For example,
|
|
180
|
+
`IORING_SETUP_SINGLE_ISSUER` means callers must submit SQEs from a single owning
|
|
181
|
+
thread even on kernels that accept the flag.
|
|
182
|
+
|
|
183
|
+
The compiled liburing version fields report the header version used to build the
|
|
184
|
+
binary extension. This is useful in CI because Linux distribution images can
|
|
185
|
+
compile the same Python package against different liburing development packages
|
|
186
|
+
while still running on the hosted runner's kernel.
|
|
187
|
+
|
|
188
|
+
`submit_send_zc()` is best gated with `probe()["IORING_OP_SEND_ZC"]`. Unsupported
|
|
189
|
+
systems may accept the submission and then report `ENOTSUP` or `EOPNOTSUPP` in
|
|
190
|
+
the operation CQE, so checking a kernel version is less useful than submitting a
|
|
191
|
+
small runtime probe. `probe()` reports both `"IORING_OP_SEND_ZC"` and
|
|
192
|
+
`"IORING_OP_SENDMSG_ZC"` for caller convenience, and derives both from the
|
|
193
|
+
simpler `sendmsg_zc` UDP loopback probe with a bound local receiver. If your CI
|
|
194
|
+
image is expected to support these operations, make that expectation explicit:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
uv run --active python - <<'PY'
|
|
198
|
+
import uring_api
|
|
199
|
+
|
|
200
|
+
probe = uring_api.probe()
|
|
201
|
+
print(probe)
|
|
202
|
+
raise SystemExit(0 if probe.get("IORING_OP_SEND_ZC") and probe.get("IORING_OP_SENDMSG_ZC") else 1)
|
|
203
|
+
PY
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
If the native extension cannot be imported after installation, importing
|
|
207
|
+
`uring_api` still succeeds and `probe()` returns `{}`. Source builds with
|
|
208
|
+
unsupported native dependencies warn and install the pure Python wrapper without
|
|
209
|
+
`_uring_api`.
|
|
210
|
+
|
|
211
|
+
The `IORING_ACCEPT_MULTISHOT` capability uses a runtime operation probe rather
|
|
212
|
+
than a kernel version check. It creates a private temporary ring and loopback
|
|
213
|
+
listener, submits one multishot accept request, connects a local client, and
|
|
214
|
+
checks whether the first accept completion keeps the request armed. If the build
|
|
215
|
+
headers do not expose the helper flag, the capability simply reports `False`.
|
|
216
|
+
|
|
217
|
+
The `IORING_RECV_MULTISHOT` capability is also checked with a runtime operation
|
|
218
|
+
probe because it requires newer kernel support than multishot accept. It creates
|
|
219
|
+
a private socket pair and provided-buffer ring, submits one multishot receive,
|
|
220
|
+
sends one byte, and reports `True` only if the first completion selects a buffer
|
|
221
|
+
and keeps the request armed with `IORING_CQE_F_MORE`.
|
|
222
|
+
|
|
223
|
+
## Initialising a Ring
|
|
224
|
+
|
|
225
|
+
The current wrapper exposes the native ring lifecycle. A ring is a file
|
|
226
|
+
descriptor plus shared submission/completion queues owned by the process.
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
import uring_api
|
|
230
|
+
|
|
231
|
+
with uring_api.Ring(entries=8) as ring:
|
|
232
|
+
print("fd:", ring.fd)
|
|
233
|
+
print("kernel features:", ring.features)
|
|
234
|
+
print("submission entries:", ring.sq_entries)
|
|
235
|
+
print("completion entries:", ring.cq_entries)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
`entries` is the requested submission queue depth. The kernel may round or size
|
|
239
|
+
the actual submission and completion queues, so inspect `sq_entries` and
|
|
240
|
+
`cq_entries` after initialisation if the exact capacity matters.
|
|
241
|
+
|
|
242
|
+
Pass `flags=` to request setup modes that were accepted by `probe(flags=...)`:
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
import uring_api
|
|
246
|
+
|
|
247
|
+
flags = uring_api.IORING_SETUP_SINGLE_ISSUER
|
|
248
|
+
|
|
249
|
+
if uring_api.probe(flags=flags):
|
|
250
|
+
with uring_api.Ring(entries=8, flags=flags) as ring:
|
|
251
|
+
...
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
The constructor passes these flags to `io_uring_queue_init_params()` for the
|
|
255
|
+
real ring. The application is still responsible for the contracts implied by
|
|
256
|
+
each flag; for example, `IORING_SETUP_SINGLE_ISSUER` requires all submissions to
|
|
257
|
+
come from the owning thread.
|
|
258
|
+
|
|
259
|
+
If initialisation fails, the constructor raises `OSError`:
|
|
260
|
+
|
|
261
|
+
```python
|
|
262
|
+
import errno
|
|
263
|
+
import uring_api
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
ring = uring_api.Ring(entries=256)
|
|
267
|
+
except OSError as exc:
|
|
268
|
+
if exc.errno == errno.EPERM:
|
|
269
|
+
raise RuntimeError("io_uring is blocked by seccomp or policy") from exc
|
|
270
|
+
if exc.errno == errno.ENOMEM:
|
|
271
|
+
raise RuntimeError("io_uring could not allocate or pin the requested resources") from exc
|
|
272
|
+
raise
|
|
273
|
+
else:
|
|
274
|
+
try:
|
|
275
|
+
print(ring.fd)
|
|
276
|
+
finally:
|
|
277
|
+
ring.close()
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Threading Model
|
|
281
|
+
|
|
282
|
+
`Ring` deliberately stays close to liburing's shared-ring model, but the Python
|
|
283
|
+
object adds native locking around the parts that matter for normal use.
|
|
284
|
+
|
|
285
|
+
The intended baseline is simple:
|
|
286
|
+
|
|
287
|
+
- one thread may reap completions with `wait()`;
|
|
288
|
+
- other threads may call submit-side methods such as `submit_recv()`,
|
|
289
|
+
`submit_recv_multishot()`, `submit_send()`, `submit_send_zc()`,
|
|
290
|
+
`submit_recvmsg()`, `submit_sendto()`, `submit_sendmsg_zc()`,
|
|
291
|
+
`submit_accept()`, `submit_accept_multishot()`, `submit_connect()`, and
|
|
292
|
+
`break_wait()`;
|
|
293
|
+
- `break_wait()` is safe to call while another thread is blocked in `wait()`;
|
|
294
|
+
- multiple concurrent `wait()` calls are serialised by the `Ring` object;
|
|
295
|
+
- alternatively, callers may start their own Python threads and have each one
|
|
296
|
+
call `serve_completions()` to wait for completions and call the callback
|
|
297
|
+
directly.
|
|
298
|
+
|
|
299
|
+
`break_wait()` prepares and submits an internal NOP. When the reaper consumes that
|
|
300
|
+
completion, `wait()` returns `None` rather than a user completion.
|
|
301
|
+
|
|
302
|
+
Serving workers use the same receive side as `wait()`, so public `wait()` calls
|
|
303
|
+
raise `RuntimeError` while they are running. Each worker calls
|
|
304
|
+
`serve_completions()`, then loops until `stop_serving()` asks the service to
|
|
305
|
+
exit. Workers compete for an internal wait lock, so only one worker is inside
|
|
306
|
+
`io_uring_wait_cqe()` at a time, while another worker can dispatch a completion
|
|
307
|
+
callback.
|
|
308
|
+
|
|
309
|
+
`stop_serving()` asks workers to exit and wakes the active waiter with
|
|
310
|
+
`break_wait()`. The caller owns the threads, so the caller must join them before
|
|
311
|
+
closing the ring; `close()` and `__exit__()` raise while completion service is
|
|
312
|
+
still active. `reset_serving()` clears the stop flag so a fresh set of workers
|
|
313
|
+
can enter `serve_completions()` again. If a callback raises, the exception is
|
|
314
|
+
reported as unraisable and the worker group exits.
|
|
315
|
+
|
|
316
|
+
Native C clients can register a worker-thread callback through the C API. When a
|
|
317
|
+
C callback is present, the serving worker calls it instead of `Ring.callback`;
|
|
318
|
+
otherwise it falls back to the Python callback property.
|
|
319
|
+
|
|
320
|
+
```python
|
|
321
|
+
import uring_api
|
|
322
|
+
import threading
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def delivered(completion):
|
|
326
|
+
print(completion.user_data, completion.res, completion.result)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
with uring_api.Ring() as ring:
|
|
330
|
+
ring.callback = delivered
|
|
331
|
+
threads = [threading.Thread(target=ring.serve_completions) for _ in range(2)]
|
|
332
|
+
for thread in threads:
|
|
333
|
+
thread.start()
|
|
334
|
+
try:
|
|
335
|
+
ring.submit_recv(fd, bytearray(4096), 200)
|
|
336
|
+
finally:
|
|
337
|
+
ring.stop_serving()
|
|
338
|
+
for thread in threads:
|
|
339
|
+
thread.join()
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
`close()` is still an owner-coordinated shutdown operation for submissions. Do
|
|
343
|
+
not close a ring while another thread may submit new user operations.
|
|
344
|
+
|
|
345
|
+
## C API
|
|
346
|
+
|
|
347
|
+
Native clients can include `uring_api_capi.h` and import `_uring_api._C_API` with
|
|
348
|
+
`PyCapsule_Import()`. Use `uring_api.get_include()` to find the installed header
|
|
349
|
+
directory when compiling an extension module.
|
|
350
|
+
|
|
351
|
+
The capsule currently exposes:
|
|
352
|
+
|
|
353
|
+
- `abi_version`, `struct_size`, and `feature_flags` for compatibility checks;
|
|
354
|
+
- `compiled_liburing_major` and `compiled_liburing_minor` for build-time header
|
|
355
|
+
visibility;
|
|
356
|
+
- `probe(entries, flags)`, which returns a new reference to the same flat
|
|
357
|
+
availability and capability dictionary as `_uring_api.probe()`;
|
|
358
|
+
- `ring_new()`, lifecycle helpers, metadata helpers, `ring_submit_recv()`,
|
|
359
|
+
`ring_submit_recv_multishot()`, `ring_submit_send()`,
|
|
360
|
+
`ring_submit_send_zc()`, `ring_submit_recvmsg()`, `ring_submit_sendto()`,
|
|
361
|
+
`ring_submit_sendmsg()`, `ring_submit_sendmsg_zc()`, `ring_submit_accept()`,
|
|
362
|
+
`ring_submit_accept_multishot()`, `ring_submit_connect()`,
|
|
363
|
+
`ring_submit_shutdown()`, `ring_submit_close()`, `ring_submit_socket()`,
|
|
364
|
+
`ring_break_wait()`, and `ring_wait()`;
|
|
365
|
+
- `ring_set_callback()`, `ring_set_c_callback()`, `ring_serve_completions()`,
|
|
366
|
+
`ring_stop_serving()`, and `ring_reset_serving()` for completion-service
|
|
367
|
+
control;
|
|
368
|
+
- `completion_check()`, `completion_user_data()`, `completion_res()`,
|
|
369
|
+
`completion_flags()`, `completion_sequence()`, and `completion_result()`
|
|
370
|
+
for native completion inspection.
|
|
371
|
+
|
|
372
|
+
Check `URING_API_CAPI_FEATURE_CORE` before calling the function table. The flag
|
|
373
|
+
describes the capsule API surface, not runtime kernel support for individual
|
|
374
|
+
operations. Use `probe()` to check whether this process can create a ring and to
|
|
375
|
+
read runtime support for optional operation helpers from the returned flat
|
|
376
|
+
dictionary. A C completion callback receives the ring object, the completion
|
|
377
|
+
object, and the supplied `user_data`. Return `0` for success; return a negative
|
|
378
|
+
value with a Python exception set to report an unraisable error and stop the
|
|
379
|
+
serving worker group.
|
|
380
|
+
|
|
381
|
+
## Choosing Ring Sizes
|
|
382
|
+
|
|
383
|
+
Ring sizing is about queue depth, not payload buffer size. A modest application
|
|
384
|
+
can start with a small number of in-flight operations; a server usually wants
|
|
385
|
+
enough entries to cover its expected concurrent I/O without constantly draining
|
|
386
|
+
and refilling the ring.
|
|
387
|
+
|
|
388
|
+
Typical starting points:
|
|
389
|
+
|
|
390
|
+
| Use case | Suggested entries | Notes |
|
|
391
|
+
| --- | ---: | --- |
|
|
392
|
+
| Availability probe | 2 | Enough to prove the kernel will create a ring. |
|
|
393
|
+
| Modest local I/O | 8-32 | Good for simple tools and initial experiments. |
|
|
394
|
+
| Concurrent client work | 64-256 | Enough room for batches without large memory pressure. |
|
|
395
|
+
| Server-style I/O | 512-4096 | Needs deliberate resource-limit checks and backpressure. |
|
|
396
|
+
|
|
397
|
+
For now, `uring-api` does not register fixed buffers. When those are added, ring
|
|
398
|
+
entries and registered buffers should be configured separately:
|
|
399
|
+
|
|
400
|
+
- ring entries control how many operations can be submitted or completed at
|
|
401
|
+
once;
|
|
402
|
+
- registered buffers control how much memory the kernel pins for direct I/O or
|
|
403
|
+
zero-copy style operation;
|
|
404
|
+
- large registered buffer pools can exceed `RLIMIT_MEMLOCK` even when ring
|
|
405
|
+
creation itself succeeds.
|
|
406
|
+
|
|
407
|
+
That distinction matters. During probing, a 64 MiB fixed-buffer pool exceeded a
|
|
408
|
+
default 64 MiB memlock limit because the limit must cover the pinned payload
|
|
409
|
+
memory plus kernel/accounting overhead.
|
|
410
|
+
|
|
411
|
+
You can inspect the process limit before choosing future buffer-pool sizes:
|
|
412
|
+
|
|
413
|
+
```python
|
|
414
|
+
import resource
|
|
415
|
+
|
|
416
|
+
soft, hard = resource.getrlimit(resource.RLIMIT_MEMLOCK)
|
|
417
|
+
|
|
418
|
+
print("memlock soft limit:", soft)
|
|
419
|
+
print("memlock hard limit:", hard)
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
For a future registered-buffer API, size the pool explicitly rather than
|
|
423
|
+
assuming the largest useful value is safe:
|
|
424
|
+
|
|
425
|
+
```python
|
|
426
|
+
buffer_size = 16 * 1024
|
|
427
|
+
buffer_count = 256
|
|
428
|
+
pool_bytes = buffer_size * buffer_count
|
|
429
|
+
|
|
430
|
+
print("planned pinned buffer pool:", pool_bytes)
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
Good default profiles for that future layer would look something like:
|
|
434
|
+
|
|
435
|
+
| Profile | Ring entries | Buffer size | Buffer count | Pinned bytes |
|
|
436
|
+
| --- | ---: | ---: | ---: | ---: |
|
|
437
|
+
| modest | 32 | 16 KiB | 64 | 1 MiB |
|
|
438
|
+
| interactive | 128 | 16 KiB | 256 | 4 MiB |
|
|
439
|
+
| server | 1024 | 64 KiB | 1024 | 64 MiB |
|
|
440
|
+
|
|
441
|
+
The server profile is intentionally near the common default memlock limit on
|
|
442
|
+
some systems. In practice, leave headroom or raise the limit before registering
|
|
443
|
+
that much memory.
|
|
444
|
+
|
|
445
|
+
## Containers and Limits
|
|
446
|
+
|
|
447
|
+
Containers may block `io_uring_setup()` even when the host kernel supports it.
|
|
448
|
+
For example, Docker's default seccomp profile commonly rejects ring creation
|
|
449
|
+
with `EPERM`. A less restricted profile may be required for development.
|
|
450
|
+
|
|
451
|
+
Large future registered-buffer pools may also require raising `RLIMIT_MEMLOCK`.
|
|
452
|
+
Prefer smaller buffers while developing the operation model, then make server
|
|
453
|
+
profiles opt-in and explicit.
|
|
454
|
+
|
|
455
|
+
## Build Requirements
|
|
456
|
+
|
|
457
|
+
`uring-api` links against system `liburing`:
|
|
458
|
+
|
|
459
|
+
```bash
|
|
460
|
+
sudo apt install liburing-dev
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
The native extension requires `liburing >= 2.4`. Older headers do not expose the
|
|
464
|
+
version macros we use for build-time validation, and they also predate the data
|
|
465
|
+
and ring entry helpers used by the extension. On Ubuntu, that means
|
|
466
|
+
`ubuntu-23.10` or newer from distro packages; `ubuntu-22.04` needs a newer
|
|
467
|
+
liburing installed from another source to build `_uring_api`.
|
|
468
|
+
|
|
469
|
+
The extension uses multi-phase module initialisation and declares itself safe to
|
|
470
|
+
import without enabling the GIL on free-threaded CPython builds.
|