summaryrefslogtreecommitdiff
path: root/cmd
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-06 10:01:58 +0300
committerPaul Buetow <paul@buetow.org>2026-06-06 10:01:58 +0300
commitd807c1ad9eb8b176e36300c6ea41744431a05bf0 (patch)
tree96b8b6a38aea95905db6280fa10921d61b01c023 /cmd
parent17cb27871a6cb5a1c21ca604c2285e7f072478a0 (diff)
test(aio): add io_getevents and io_cancel enter coverage
Close the integration-test gaps for two classic-AIO syscalls that the existing scenario never exercised. The AIO workload only drove io_setup/io_submit/io_destroy, so io_getevents (nr 208) and io_cancel (nr 210) had no end-to-end coverage despite their tracer classification (FamilyAIO, KindNull enter, UNCLASSIFIED ret) being correct by inspection. cmd/ioworkload/scenario_aio.go: - Factor the submit scaffolding (temp dir, target file, AIO context) into withAioTarget so each scenario stays short. - ioSubmitWrite now returns the submitted iocb pointer (io_cancel needs it). - aio-getevents: submit, then reap the completion with a blocking io_getevents (min_nr=1, NULL timeout); asserts the return is a count. - aio-cancel: submit, then best-effort io_cancel (return ignored: it races the I/O completion and often yields -EINVAL/-EAGAIN, but enter still fires), then drain the ring non-blockingly (min_nr=0) so io_destroy has nothing in flight and we never hang when the cancel left no completion. integrationtests/aio_test.go: - TestAioGetevents asserts enter_io_getevents (MinCount 1), mirroring TestAioSubmit, with io_getevents added to the trace-arg set. - TestAioCancel asserts ONLY enter_io_cancel (MinCount 1) — never success — because io_cancel's return is non-deterministic. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'cmd')
-rw-r--r--cmd/ioworkload/scenario_aio.go147
-rw-r--r--cmd/ioworkload/scenarios.go2
2 files changed, 140 insertions, 9 deletions
diff --git a/cmd/ioworkload/scenario_aio.go b/cmd/ioworkload/scenario_aio.go
index 61bd1ff..f4cecb1 100644
--- a/cmd/ioworkload/scenario_aio.go
+++ b/cmd/ioworkload/scenario_aio.go
@@ -14,9 +14,11 @@ import (
// distinct from the io_uring_* family. We invoke them raw via Syscall because
// the Go standard library does not wrap them.
const (
- sysIoSetup = 206
- sysIoDestroy = 207
- sysIoSubmit = 209
+ sysIoSetup = 206
+ sysIoDestroy = 207
+ sysIoGetevents = 208
+ sysIoSubmit = 209
+ sysIoCancel = 210
// aioMaxEvents is the nr_events count requested from io_setup(2). It is a
// plain count (NOT an fd), so the tracer must classify the enter event as
@@ -29,6 +31,17 @@ const (
iocbCmdPwrite = 1
)
+// ioEvent mirrors struct io_event from <linux/aio_abi.h> on x86_64 (32 bytes).
+// io_getevents(2) fills an array of these from the completion ring; we only
+// need the layout to reap a completion, not to trace it (io_getevents args are
+// opaque to the tracer, KindNull).
+type ioEvent struct {
+ data uint64 // 0: aio_data echoed from the submitting iocb
+ obj uint64 // 8: pointer to the originating iocb
+ res int64 // 16: primary result (bytes transferred, or -errno)
+ res2 int64 // 24: secondary result
+}
+
// iocb mirrors struct iocb from <linux/aio_abi.h> on x86_64 (64 bytes). It is
// the control block io_submit(2) consumes via its iocbpp pointer-array argument.
// The tracer treats io_submit's args (ctx_id, nr, iocbpp) as opaque (KindNull),
@@ -87,7 +100,58 @@ func aioSetupEinval() error {
// AIO family. Note io_submit returns the COUNT of iocbs submitted (here 1), NOT
// a byte count, which is why the tracer must classify its return UNCLASSIFIED.
func aioSubmit() error {
- dir, cleanup, err := makeTempDir("aio-submit")
+ return withAioTarget("aio-submit", func(ctx uint64, fd int) error {
+ _, err := ioSubmitWrite(ctx, fd)
+ return err
+ })
+}
+
+// aioGetevents exercises io_getevents(2) end-to-end: it submits one iocb, then
+// reaps its completion with io_getevents(ctx, min_nr, nr, events, timeout)
+// (sys nr 208 on x86_64). This drives a real io_getevents tracepoint so the
+// integration harness can validate enter_io_getevents/exit_io_getevents for the
+// AIO family. io_getevents returns the COUNT of events reaped (not a byte
+// count), which is why the tracer classifies its return UNCLASSIFIED.
+func aioGetevents() error {
+ return withAioTarget("aio-getevents", func(ctx uint64, fd int) error {
+ if _, err := ioSubmitWrite(ctx, fd); err != nil {
+ return err
+ }
+ return ioGeteventsReap(ctx)
+ })
+}
+
+// aioCancel exercises io_cancel(2): it submits one iocb and then calls
+// io_cancel(ctx, iocbp, &result) (sys nr 210 on x86_64). io_cancel is
+// non-deterministic — the I/O frequently completes before the cancel runs, so
+// the syscall often returns -EINVAL/-EAGAIN — but the enter_io_cancel
+// tracepoint fires regardless of the return value, which is all the integration
+// harness asserts on. To keep the context valid we still reap any pending
+// completion with io_getevents afterwards before tearing down.
+func aioCancel() error {
+ return withAioTarget("aio-cancel", func(ctx uint64, fd int) error {
+ cbp, err := ioSubmitWrite(ctx, fd)
+ if err != nil {
+ return err
+ }
+ // Best-effort cancel: ignore the (non-deterministic) return value;
+ // only the enter tracepoint matters for coverage.
+ ioCancelRequest(ctx, cbp)
+ // Drain any pending completion non-blockingly so io_destroy has
+ // nothing left in flight. We must NOT block here: if the cancel
+ // succeeded the request produces no completion event, so a blocking
+ // (min_nr=1) reap would hang.
+ ioGeteventsDrain(ctx)
+ return nil
+ })
+}
+
+// withAioTarget sets up the common AIO scaffolding shared by the submit-based
+// scenarios: a temp dir, a writable target file, and an AIO context. It invokes
+// fn with the context id and the target fd, then tears the context and temp dir
+// down. Factoring this out keeps the individual scenarios short.
+func withAioTarget(label string, fn func(ctx uint64, fd int) error) error {
+ dir, cleanup, err := makeTempDir(label)
if err != nil {
return err
}
@@ -106,14 +170,15 @@ func aioSubmit() error {
}
defer ioDestroyContext(ctx)
- return ioSubmitWrite(ctx, int(f.Fd()))
+ return fn(ctx, int(f.Fd()))
}
// ioSubmitWrite submits one IOCB_CMD_PWRITE iocb against fd via io_submit(2).
// io_submit takes (ctx_id, nr, iocbpp): an aio_context_t handle (NOT an fd), a
// count, and a userspace array of iocb pointers. On success it returns the
-// number of iocbs accepted (1 here).
-func ioSubmitWrite(ctx uint64, fd int) error {
+// number of iocbs accepted (1 here) and the submitted iocb pointer, which
+// io_cancel(2) needs to identify the request to cancel.
+func ioSubmitWrite(ctx uint64, fd int) (*iocb, error) {
buf := []byte("ior-aio-submit\n")
cb := iocb{
aioLioOpcode: iocbCmdPwrite,
@@ -134,14 +199,78 @@ func ioSubmitWrite(ctx uint64, fd int) error {
runtime.KeepAlive(buf)
runtime.KeepAlive(cbp)
if errno != 0 {
- return fmt.Errorf("io_submit: %w", errno)
+ return nil, fmt.Errorf("io_submit: %w", errno)
}
if ret != uintptr(len(cbs)) {
- return fmt.Errorf("io_submit submitted %d iocbs, want %d", ret, len(cbs))
+ return nil, fmt.Errorf("io_submit submitted %d iocbs, want %d", ret, len(cbs))
+ }
+ return cbp, nil
+}
+
+// ioGeteventsReap reaps up to one completion from the AIO context with
+// io_getevents(2). It takes (ctx_id, min_nr, nr, events, timeout): we request
+// at least one event (min_nr=1) and a NULL timeout so the call blocks until the
+// submitted write completes. The aio_context_t handle is NOT an fd and the
+// events/timeout pointers are opaque to the tracer (KindNull enter); the return
+// is a COUNT of events reaped (UNCLASSIFIED), not a byte count.
+func ioGeteventsReap(ctx uint64) error {
+ var events [1]ioEvent
+ ret, _, errno := syscall.Syscall6(
+ sysIoGetevents,
+ uintptr(ctx),
+ 1, // min_nr: block until at least one completion is ready
+ uintptr(len(events)),
+ uintptr(unsafe.Pointer(&events[0])),
+ 0, // timeout: NULL -> wait indefinitely
+ 0,
+ )
+ runtime.KeepAlive(events)
+ if errno != 0 {
+ return fmt.Errorf("io_getevents: %w", errno)
+ }
+ if ret < 1 {
+ return fmt.Errorf("io_getevents reaped %d events, want >= 1", ret)
}
return nil
}
+// ioGeteventsDrain reaps any already-completed events non-blockingly
+// (min_nr=0, NULL timeout) and discards them. It is used by the cancel scenario
+// to clear the completion ring without risking a hang when the cancel succeeded
+// and left no completion behind. Errors are ignored: this is best-effort
+// cleanup, not a tracepoint-bearing assertion path.
+func ioGeteventsDrain(ctx uint64) {
+ var events [1]ioEvent
+ _, _, _ = syscall.Syscall6(
+ sysIoGetevents,
+ uintptr(ctx),
+ 0, // min_nr=0: return immediately even if nothing is ready
+ uintptr(len(events)),
+ uintptr(unsafe.Pointer(&events[0])),
+ 0, // timeout: NULL
+ 0,
+ )
+ runtime.KeepAlive(events)
+}
+
+// ioCancelRequest attempts to cancel the in-flight iocb via io_cancel(2),
+// which takes (ctx_id, iocb, result). The result io_event receives the
+// completion data on a successful cancel. The return value is intentionally
+// ignored by callers: io_cancel races the I/O completion and commonly fails
+// with -EINVAL/-EAGAIN, but the enter_io_cancel tracepoint fires regardless,
+// which is the only thing the integration harness asserts on.
+func ioCancelRequest(ctx uint64, cbp *iocb) {
+ var result ioEvent
+ _, _, _ = syscall.Syscall(
+ sysIoCancel,
+ uintptr(ctx),
+ uintptr(unsafe.Pointer(cbp)),
+ uintptr(unsafe.Pointer(&result)),
+ )
+ runtime.KeepAlive(cbp)
+ runtime.KeepAlive(result)
+}
+
// ioSetupContext calls io_setup(2) and returns the opaque aio_context_t id.
func ioSetupContext(nrEvents uint32) (uint64, error) {
var ctx uint64
diff --git a/cmd/ioworkload/scenarios.go b/cmd/ioworkload/scenarios.go
index 811127a..50f84e1 100644
--- a/cmd/ioworkload/scenarios.go
+++ b/cmd/ioworkload/scenarios.go
@@ -156,6 +156,8 @@ var scenarios = map[string]func() error{
"aio-setup": aioSetup,
"aio-setup-einval": aioSetupEinval,
"aio-submit": aioSubmit,
+ "aio-getevents": aioGetevents,
+ "aio-cancel": aioCancel,
"signals-basic": signalsBasic,
"misc-basic": miscBasic,
"sched-basic": schedBasic,