sync-cancel.dhover×112all
#!/usr/bin/env dub
/+ dub.sdl:
    name "io_uring_sync_cancel"
    dependency "during" version="~>0.5.0"
    platforms "linux"
    targetPath "build"
+/
/**
 * `io_uring` — synchronous cancellation from userspace
 * (`IORING_REGISTER_SYNC_CANCEL`, Linux 6.0).
 *
 * Before 6.0 the only way to cancel an in-flight request was to submit an
 * `IORING_OP_ASYNC_CANCEL` SQE and then reap *its* completion plus the
 * cancelled op's completion — an asynchronous, two-CQE dance. 6.0 added a
 * `register`-family opcode that cancels matching requests **synchronously**:
 * the `io_uring_register(REGISTER_SYNC_CANCEL, …)` call blocks until the
 * matching request(s) are torn down and returns the count, with no cancel SQE
 * and no extra CQE.
 *
 * This example:
 *   1. Opens a pipe and arms a `POLL_ADD` for `POLLIN` on the read end. Nothing
 *      is ever written, so the poll would block forever — a perfect stand-in
 *      for a genuinely in-flight request. The SQE carries `user_data = 1`.
 *   2. Fills an `io_uring_sync_cancel_reg` with `addr = 1` (match by the same
 *      `user_data`; `flags = 0` selects user_data matching) and calls
 *      `io.registerSyncCancel(reg)`.
 *   3. Reaps the poll's CQE and asserts it came back with `-ECANCELED`.
 *
 * Companion to the io_uring chronology:
 * see docs/research/async-io/io-uring/timeline.md
 * § "6.0 — Zero-copy send, single-issuer, sync cancel (October 2022)".
 *
 * Run with: `dub run --single sync-cancel.d`
 *
 * Portability: prints a `SKIP:` line and exits 0 when io_uring is unavailable
 * (old kernel / sandbox) or when `REGISTER_SYNC_CANCEL` is missing (kernel
 * < 6.0, reported as `-EINVAL` / `-ENOSYS`). It returns nonzero only if a call
 * that should have worked fails. This host runs kernel 6.18, where the feature
 * is present and is exercised for real.
 */
module 
(module) io_uring_sync_cancel

io_uring — synchronous cancellation from userspace (IORING_REGISTER_SYNC_CANCEL, Linux 6.0).

Before 6.0 the only way to cancel an in-flight request was to submit an IORING_OP_ASYNC_CANCEL SQE and then reap its completion plus the cancelled op's completion — an asynchronous, two-CQE dance. 6.0 added a register-family opcode that cancels matching requests synchronously: the io_uring_register(REGISTER_SYNC_CANCEL, …) call blocks until the matching request(s) are torn down and returns the count, with no cancel SQE and no extra CQE.

This example:

  1. Opens a pipe and arms a POLL_ADD for POLLIN on the read end. Nothing is ever written, so the poll would block forever — a perfect stand-in for a genuinely in-flight request. The SQE carries user_data = 1.

  2. Fills an io_uring_sync_cancel_reg with addr = 1 (match by the same user_data; flags = 0 selects user_data matching) and calls io.registerSyncCancel(reg).

  3. Reaps the poll's CQE and asserts it came back with -ECANCELED.

Companion to the io_uring chronology: see docs/research/async-io/io-uring/timeline.md § "6.0 — Zero-copy send, single-issuer, sync cancel (October 2022)".

Run with: dub run --single sync-cancel.d

Portability

prints a SKIP: line and exits 0 when io_uring is unavailable (old kernel / sandbox) or when REGISTER_SYNC_CANCEL is missing (kernel < 6.0, reported as -EINVAL / -ENOSYS). It returns nonzero only if a call that should have worked fails. This host runs kernel 6.18, where the feature is present and is exercised for real.

io_uring_sync_cancel
;
import
(module) during

Simple idiomatic dlang wrapper around linux io_uring (see: https://kernel.dk/io_uring.pdf) asynchronous API.

during
;
import
(package) core
core
.
(package) core.sys
sys
.
(package) core.sys.linux
linux
.
(module) core.sys.linux.errno

D header file for GNU/Linux

glibc stdlib/errno.h

errno
:
(alias constant) io_uring_sync_cancel.ECANCELED = int core.stdc.errno.ECANCELED = 125
ECANCELED
,
(alias constant) io_uring_sync_cancel.EINTR = int core.stdc.errno.EINTR = 4
EINTR
,
(alias constant) io_uring_sync_cancel.EINVAL = int core.stdc.errno.EINVAL = 22
EINVAL
,
(alias constant) io_uring_sync_cancel.ENOSYS = int core.stdc.errno.ENOSYS = 38
ENOSYS
;
import
(package) core
core
.
(package) core.sys
sys
.
(package) core.sys.posix
posix
.
(module) core.sys.posix.unistd

D header file for POSIX.

@copyrightCopyright Sean Kelly 2005 - 2009.@licenseBoost License 1.0.@authorsSean Kelly@standardsThe Open Group Base Specifications Issue 8, IEEE Std 1003.1, 2024 Edition
unistd
:
(alias) io_uring_sync_cancel.close = int core.sys.posix.unistd.close(int) nothrow @nogc @trusted
close
,
(alias) io_uring_sync_cancel.pipe = int core.sys.posix.unistd.pipe(ref int[2]) nothrow @nogc @trusted
pipe
;
import
(package) std
std
.
(module) std.stdio
Category Symbols
File handles _popen File isFileHandle openNetwork stderr stdin stdout
Reading chunks lines readf readfln readln
Writing toFile write writef writefln writeln
Misc KeepTerminator LockType StdioException

Standard I/O functions that extend core.stdc.stdio. core.stdc.stdio is publically imported when importing std.stdio.

There are three layers of I/O:

  1. The lowest layer is the operating system layer. The two main schemes are Windows and Posix.

  2. C's stdio.h which unifies the two operating system schemes.

  3. std.stdio, this module, unifies the various stdio.h implementations into a high level package for D programs.

Source

std/stdio.d

@copyrightCopyright The D Language Foundation 2007-.@licenseBoost License 1.0.@authorsWalter Bright, Andrei Alexandrescu, Alex Rønne Petersen
stdio
:
(alias template) io_uring_sync_cancel.writefln = std.stdio.writefln(alias fmt, A...)(A args) if (isSomeString!(typeof(fmt)))

Equivalent to writef(fmt, args, '\n').

writefln
, stderr;
int
int D main()
main
()
{ // The cookie we will both tag the poll with and match against on cancel. enum ulong
(constant) ulong io_uring_sync_cancel.main.cookie = 1LU
cookie
= 1;
(struct) during.Uring

Main entry point to work with io_uring.

It hides SubmissionQueue and CompletionQueue behind standard range interface. We put in SubmissionEntry entries and take out CompletionEntry entries.

Use predefined prepXX methods to fill required fields of SubmissionEntry before put or during putWith.

Note

prepXX functions doesn't touch previous entry state, just fills in operation properties. This is because for less error prone interface it is cleared automatically when prepared using putWith. So when using on own SubmissionEntry (outside submission queue), that would be added to the submission queue using put, be sure its cleared if it's reused for multiple operations.

Uring
(local variable) during.Uring io
io
;
const
(local variable) const(int) setupRet
setupRet
=
(local variable) during.Uring io
io
.
int during.setup(ref during.Uring uring, uint entries = 128u, during.io_uring.SetupFlags flags = SetupFlags.NONE) nothrow @nogc @safe

Setup new instance of io_uring into provided Uring structure.

@paramuring Uring structure to be initialized (must not be already initialized)@paramentries Number of entries to initialize uring with@paramflags SetupFlags to use to initialize uring.@returnsOn succes it returns 0, -errno otherwise.
setup
(8);
if (
(local variable) const(int) setupRet
setupRet
< 0)
{
void std.stdio.writefln!(char, const(int))(in char[] fmt, const(int) __param_1) @safe

Equivalent to writef(fmt, args, '\n').

writefln
("SKIP: io_uring_setup failed (errno %d) — io_uring unavailable on this host",
-
(local variable) const(int) setupRet
setupRet
);
return 0; } // A pipe with no writer: POLLIN on the read end can never become ready, so // the poll request stays genuinely in-flight until we cancel it. int[2]
(local variable) int[2] fds
fds
;
if (
int core.sys.posix.unistd.pipe(ref int[2]) nothrow @nogc @trusted
pipe
(
(local variable) int[2] fds
fds
) != 0)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("pipe() failed");
return 1; } scope (exit) {
int core.sys.posix.unistd.close(int) nothrow @nogc @trusted
close
(
(local variable) int[2] fds
fds
[0]);
int core.sys.posix.unistd.close(int) nothrow @nogc @trusted
close
(
(local variable) int[2] fds
fds
[1]); }
// Arm a single POLL_ADD for readability on the read end, tagged with `cookie`.
(local variable) during.Uring io
io
.putWith!((ref SubmissionEntry e, int fd) {
e.prepPollAdd(fd, PollEvents.IN); e.user_data = cookie; })(
during.Uring during.Uring.putWith!(function (ref during.io_uring.SubmissionEntry e, int fd) nothrow @nogc @safe { prepPollAdd(e, fd, PollEvents.IN, PollFlags.NONE); e.user_data = 1LU; } , int)(ref int __param_0) nothrow @nogc return ref @safe

Adds new entry to the SubmissionQueue.

Note that this just adds entry to the queue and doesn't advance the tail marker kernel sees. For that finishSq() is needed to be called next.

Also note that to actually enter new entries to kernel, it's needed to call submit().

@paramFN Function to fill next entry in queue by ref (should be faster). It is expected to be in a form of void function(ARGS)(ref SubmissionEntry, auto ref ARGS). Note that in this case queue entry is cleaned first before function is called.@paramentry Custom built SubmissionEntry to be posted as is. Note that in this case it is copied whole over one in the SubmissionQueue.@paramargs Optional arguments passed to the function@returnsreference to Uring structure so it's possible to chain multiple commands.
fds
[0]);
const
(local variable) const(int) submitted
submitted
=
(local variable) during.Uring io
io
.
int during.Uring.submit(uint want) nothrow @nogc @safe

Submits qued SubmissionEntry to be processed by kernel.

@paramwant number of CompletionEntries to wait for. If 0, this just submits queued entries and returns. If > 0, it blocks until at least wanted number of entries were completed.@paramsig See io_uring_enter(2) man page@returnsNumber of submitted entries on success, -errno on error
submit
(0); // submit without waiting — nothing will complete yet
if (
(local variable) const(int) submitted
submitted
< 0)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("submit failed: errno %d", -
(local variable) const(int) submitted
submitted
);
return 1; } // Synchronous cancel: match by user_data (flags = 0, the default selector). // timeout {0,0} means "don't wait for the cancel itself to settle"; a poll // is cancellable immediately so this returns the match count right away.
(struct) during.io_uring.io_uring_sync_cancel_reg

Argument to IORING_REGISTER_SYNC_CANCEL. Synchronously cancels matching in-flight requests; addr, fd, flags, and opcode act as match keys (combined the same way as IORING_OP_ASYNC_CANCEL). timeout bounds the cancel wait — {-1, -1} means "no timeout".

Note

Available from Linux 6.0

io_uring_sync_cancel_reg
(local variable) during.io_uring.io_uring_sync_cancel_reg reg
reg
;
(local variable) during.io_uring.io_uring_sync_cancel_reg reg
reg
.
(field) ulong during.io_uring.io_uring_sync_cancel_reg.addr
addr
=
(constant) ulong io_uring_sync_cancel.main.cookie = 1LU
cookie
; // match key: the poll's user_data
(local variable) during.io_uring.io_uring_sync_cancel_reg reg
reg
.
(field) int during.io_uring.io_uring_sync_cancel_reg.fd
fd
= -1; // unused when matching by user_data
(local variable) during.io_uring.io_uring_sync_cancel_reg reg
reg
.
(field) uint during.io_uring.io_uring_sync_cancel_reg.flags
flags
= 0; // 0 => match by user_data
(local variable) during.io_uring.io_uring_sync_cancel_reg reg
reg
.
(field) ubyte during.io_uring.io_uring_sync_cancel_reg.opcode
opcode
= 0;
(local variable) during.io_uring.io_uring_sync_cancel_reg reg
reg
.
(field) during.io_uring.KernelTimespec during.io_uring.io_uring_sync_cancel_reg.timeout
timeout
.
(field) long during.io_uring.KernelTimespec.tv_sec

seconds

tv_sec
= 0;
(local variable) during.io_uring.io_uring_sync_cancel_reg reg
reg
.
(field) during.io_uring.KernelTimespec during.io_uring.io_uring_sync_cancel_reg.timeout
timeout
.
(field) long during.io_uring.KernelTimespec.tv_nsec

nanoseconds

tv_nsec
= 0;
const
(local variable) const(int) cret
cret
=
(local variable) during.Uring io
io
.
int during.Uring.registerSyncCancel(ref scope during.io_uring.io_uring_sync_cancel_reg reg) nothrow @nogc @trusted

Synchronously cancel one or more in-flight requests matching the keys in reg. Returns the number of cancelled requests on success, -errno on failure. Use KernelTimespec(-1, -1) in ``reg.timeout to wait indefinitely.

Note

Available from Linux 6.0

registerSyncCancel
(
(local variable) during.io_uring.io_uring_sync_cancel_reg reg
reg
);
if (
(local variable) const(int) cret
cret
== -
(constant) int core.stdc.errno.EINVAL = 22
EINVAL
||
(local variable) const(int) cret
cret
== -
(constant) int core.stdc.errno.ENOSYS = 38
ENOSYS
)
{
void std.stdio.writefln!(char, const(int))(in char[] fmt, const(int) __param_1) @safe

Equivalent to writef(fmt, args, '\n').

writefln
("SKIP: IORING_REGISTER_SYNC_CANCEL unsupported (errno %d) — needs Linux 6.0+",
-
(local variable) const(int) cret
cret
);
return 0; } if (
(local variable) const(int) cret
cret
< 0)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("registerSyncCancel failed: errno %d", -
(local variable) const(int) cret
cret
);
return 1; } // A successful synchronous cancel guarantees the cancelled request's CQE is // already enqueued, so the completion is here now. Bound the wait anyway so a // misbehaving kernel can't hang us: `submitAndWaitMinTimeout` blocks at most // `ts` (one second) before giving up. (EXT_ARG-style waits need Linux 5.11+, // which is implied by the 6.0 feature we are already on.) const
(local variable) const(during.io_uring.KernelTimespec) ts
ts
=
(struct) during.io_uring.KernelTimespec

Time specification as defined in kernel headers (used by TIMEOUT operations)

KernelTimespec
(1, 0); // {1s, 0ns}
const
(local variable) const(int) waited
waited
=
(local variable) during.Uring io
io
.
int during.Uring.submitAndWaitMinTimeout(uint want, ref const(during.io_uring.KernelTimespec) ts, uint minWaitUsec, const(core.sys.posix.signal.sigset_t)* sigmask = null) nothrow @nogc @trusted

Submit pending SQEs and wait for at least want CQEs with an absolute timeout ts and a minimum wait minWaitUsec (the kernel will let through completions arriving sooner than ts once it has waited at least minWaitUsec microseconds).

Note

Available from Linux 6.13

submitAndWaitMinTimeout
(1,
(local variable) const(during.io_uring.KernelTimespec) ts
ts
, 0);
if (
(local variable) const(int) waited
waited
< 0 ||
(local variable) during.Uring io
io
.
bool during.Uring.empty() const pure nothrow @nogc @safe

Check if there is some CompletionEntry to process.

empty
)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("no completion after sync cancel (wait returned %d)",
(local variable) const(int) waited
waited
);
return 1; } const
(local variable) const(int) res
res
=
(local variable) during.Uring io
io
.
during.io_uring.CompletionEntry during.Uring.front() pure nothrow @nogc return ref @safe

Get first CompletionEntry from cq ring

front
.
(field) int during.io_uring.CompletionEntry.res

result code for this event

res
;
const
(local variable) const(ulong) echoed
echoed
=
(local variable) during.Uring io
io
.
during.io_uring.CompletionEntry during.Uring.front() pure nothrow @nogc return ref @safe

Get first CompletionEntry from cq ring

front
.
(field) ulong during.io_uring.CompletionEntry.user_data

sqe->data submission passed back

user_data
;
(local variable) during.Uring io
io
.
void during.Uring.popFront() pure nothrow @nogc @safe

Move to next CompletionEntry

popFront
();
if (
(local variable) const(ulong) echoed
echoed
!=
(constant) ulong io_uring_sync_cancel.main.cookie = 1LU
cookie
)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("user_data mismatch: expected %d, got %d",
(constant) ulong io_uring_sync_cancel.main.cookie = 1LU
cookie
,
(local variable) const(ulong) echoed
echoed
);
return 1; } // A cancelled request reports -ECANCELED (some kernels surface -EINTR for // interrupted ops); either confirms the synchronous cancel took effect. if (
(local variable) const(int) res
res
!= -
(constant) int core.stdc.errno.ECANCELED = 125
ECANCELED
&&
(local variable) const(int) res
res
!= -
(constant) int core.stdc.errno.EINTR = 4
EINTR
)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("expected -ECANCELED, got res=%d",
(local variable) const(int) res
res
);
return 1; } // `cret` is the kernel's reported match count (often 0 for a poll the kernel // tears down inline); the authoritative proof is the poll's -ECANCELED CQE.
void std.stdio.writefln!(char, string, const(int), const(int))(in char[] fmt, string __param_1, const(int) __param_2, const(int) __param_3) @safe

Equivalent to writef(fmt, args, '\n').

writefln
("ok: REGISTER_SYNC_CANCEL torn down the in-flight poll; CQE returned %s (res=%d, matches=%d)",
(local variable) const(int) res
res
== -
(constant) int core.stdc.errno.ECANCELED = 125
ECANCELED
? "-ECANCELED" : "-EINTR",
(local variable) const(int) res
res
,
(local variable) const(int) cret
cret
);
return 0; }