From 7dcd86994af3e0467c4c97c2afbc581bbec3ccb0 Mon Sep 17 00:00:00 2001 From: Diogo Martins Date: Mon, 22 Jun 2026 13:51:15 +0100 Subject: [PATCH 1/6] Run the ioxide.e2e suite in the build workflow Add postgres + redis service containers and run Tests/ioxide.e2e (already built as part of ioxide.slnx) after the build, before pack/publish, so a failing test blocks the release. The suite exits non-zero on failure and drives real reactors over real sockets; pg/redis/kTLS tests use the sidecars + tls module and skip cleanly if absent. Verified locally: 15 passed, 0 failed. --- .github/workflows/build.yml | 44 +++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f18bc30..a35e961 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,6 +16,31 @@ jobs: build: runs-on: ubuntu-latest + # pg + redis sidecars so the e2e suite's pg/redis tests run (they skip cleanly if absent). + services: + postgres: + image: postgres:18 + env: + POSTGRES_USER: bench + POSTGRES_DB: bench + POSTGRES_HOST_AUTH_METHOD: trust + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + redis: + image: redis:7-alpine + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: - name: Checkout code uses: actions/checkout@v4 @@ -31,6 +56,25 @@ jobs: - name: Build run: dotnet build ioxide.slnx --configuration Release --no-restore + # io_uring is needed by the reactor; the tls module by the kTLS test. Best-effort — io_uring is + # enabled by default on the runners, and the suite skips the kTLS test if the module is absent. + - name: Enable io_uring + load kTLS module + run: | + sudo sysctl -w kernel.io_uring_disabled=0 2>/dev/null || true + sudo modprobe tls 2>/dev/null || true + + # E2E suite (already compiled as part of ioxide.slnx). Drives real reactors over real sockets; + # pg/redis tests use the sidecars above. Non-zero exit fails the job before any pack/publish. + - name: Run E2E tests + env: + EXAMPLES_PG_HOST: localhost + EXAMPLES_PG_PORT: 5432 + EXAMPLES_PG_USER: bench + EXAMPLES_PG_DB: bench + EXAMPLES_REDIS_HOST: localhost + EXAMPLES_REDIS_PORT: 6379 + run: dotnet run --project Tests/ioxide.e2e.csproj --configuration Release --no-build + - name: Pack ioxide run: dotnet pack ioxide/ioxide.csproj --configuration Release --no-build --output ./artifacts From 09a4af874ead4b480c7f89f95189e1002def864a Mon Sep 17 00:00:00 2001 From: Diogo Martins Date: Mon, 22 Jun 2026 13:58:01 +0100 Subject: [PATCH 2/6] Fix e2e sidecar host to an IPv4 literal + diagnose io_uring ioxide.pg/redis need an IPv4 literal, not 'localhost'. Also print the io_uring_disabled sysctl state to see whether the hosted runner allows io_uring. --- .github/workflows/build.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a35e961..0882308 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -60,18 +60,21 @@ jobs: # enabled by default on the runners, and the suite skips the kTLS test if the module is absent. - name: Enable io_uring + load kTLS module run: | - sudo sysctl -w kernel.io_uring_disabled=0 2>/dev/null || true - sudo modprobe tls 2>/dev/null || true + echo "kernel: $(uname -r)" + echo "io_uring_disabled (before): $(cat /proc/sys/kernel/io_uring_disabled 2>/dev/null || echo n/a)" + sudo sysctl -w kernel.io_uring_disabled=0 || echo "WARN: could not set kernel.io_uring_disabled" + echo "io_uring_disabled (after): $(cat /proc/sys/kernel/io_uring_disabled 2>/dev/null || echo n/a)" + sudo modprobe tls 2>/dev/null && echo "tls module loaded" || echo "tls module unavailable (kTLS test will skip)" # E2E suite (already compiled as part of ioxide.slnx). Drives real reactors over real sockets; # pg/redis tests use the sidecars above. Non-zero exit fails the job before any pack/publish. - name: Run E2E tests env: - EXAMPLES_PG_HOST: localhost + EXAMPLES_PG_HOST: 127.0.0.1 EXAMPLES_PG_PORT: 5432 EXAMPLES_PG_USER: bench EXAMPLES_PG_DB: bench - EXAMPLES_REDIS_HOST: localhost + EXAMPLES_REDIS_HOST: 127.0.0.1 EXAMPLES_REDIS_PORT: 6379 run: dotnet run --project Tests/ioxide.e2e.csproj --configuration Release --no-build From 0c50a7f868d0eac2e1db63ef3d4940aee27d6769 Mon Sep 17 00:00:00 2001 From: Diogo Martins Date: Mon, 22 Jun 2026 14:05:39 +0100 Subject: [PATCH 3/6] Probe io_uring on the runner: host vs seccomp-unconfined / privileged container Determine the io_uring_setup errno and whether a container can bypass the hosted-runner block, so the e2e could run in one instead of needing self-hosted. --- .github/workflows/build.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0882308..6211570 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -66,6 +66,27 @@ jobs: echo "io_uring_disabled (after): $(cat /proc/sys/kernel/io_uring_disabled 2>/dev/null || echo n/a)" sudo modprobe tls 2>/dev/null && echo "tls module loaded" || echo "tls module unavailable (kTLS test will skip)" + # Diagnostic: io_uring_setup errno on the host, and whether a seccomp-unconfined / privileged + # container can bypass the block (so the e2e could run inside one instead of needing self-hosted). + - name: Probe io_uring (host vs container) + run: | + cat > /tmp/p.c <<'PROBE' + #include + #include + #include + #include + int main(void){ + unsigned char p[256]={0}; + long fd=syscall(425,8,p); + if(fd<0){printf("FAIL errno=%d (%s)\n",errno,strerror(errno));return 1;} + printf("OK fd=%ld\n",fd);return 0;} + PROBE + gcc -O2 -o /tmp/p /tmp/p.c + echo "=== host ==="; /tmp/p || true + echo "=== docker default seccomp ==="; docker run --rm -v /tmp/p:/p:ro ubuntu:24.04 /p || true + echo "=== docker seccomp=unconfined ==="; docker run --rm --security-opt seccomp=unconfined -v /tmp/p:/p:ro ubuntu:24.04 /p || true + echo "=== docker privileged ==="; docker run --rm --privileged -v /tmp/p:/p:ro ubuntu:24.04 /p || true + # E2E suite (already compiled as part of ioxide.slnx). Drives real reactors over real sockets; # pg/redis tests use the sidecars above. Non-zero exit fails the job before any pack/publish. - name: Run E2E tests From 671f1dcc379a624742f3a100eb7cb65e82cd7bcf Mon Sep 17 00:00:00 2001 From: Diogo Martins Date: Mon, 22 Jun 2026 14:13:02 +0100 Subject: [PATCH 4/6] Probe io_uring setup flags/entries on the runner Pinpoint which of SINGLE_ISSUER|DEFER_TASKRUN|NO_SQARRAY (or ring size) the hosted runner rejects, with the real errno per combination. --- .github/workflows/build.yml | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6211570..97f9f8a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -68,24 +68,34 @@ jobs: # Diagnostic: io_uring_setup errno on the host, and whether a seccomp-unconfined / privileged # container can bypass the block (so the e2e could run inside one instead of needing self-hosted). - - name: Probe io_uring (host vs container) + - name: Probe io_uring (flags + entries) run: | + echo "memlock (ulimit -l): $(ulimit -l) KB" cat > /tmp/p.c <<'PROBE' #include #include #include + #include #include - int main(void){ + static void t(const char* n, unsigned int flags, unsigned int entries){ unsigned char p[256]={0}; - long fd=syscall(425,8,p); - if(fd<0){printf("FAIL errno=%d (%s)\n",errno,strerror(errno));return 1;} - printf("OK fd=%ld\n",fd);return 0;} + *(unsigned int*)(p+8)=flags; + long fd=syscall(425,entries,p); + if(fd<0) printf("%-34s FAIL errno=%d (%s)\n",n,errno,strerror(errno)); + else { printf("%-34s OK fd=%ld\n",n,fd); close(fd); } + } + int main(void){ + unsigned SI=1u<<12, DTR=1u<<13, NSA=1u<<16; + t("flags=0 e=8",0,8); + t("SINGLE_ISSUER e=8",SI,8); + t("SI|DEFER_TASKRUN e=8",SI|DTR,8); + t("SI|DTR|NO_SQARRAY e=8",SI|DTR|NSA,8); + t("SI|DTR|NO_SQARRAY e=256",SI|DTR|NSA,256); + t("SI|DTR|NO_SQARRAY e=4096",SI|DTR|NSA,4096); + return 0;} PROBE gcc -O2 -o /tmp/p /tmp/p.c - echo "=== host ==="; /tmp/p || true - echo "=== docker default seccomp ==="; docker run --rm -v /tmp/p:/p:ro ubuntu:24.04 /p || true - echo "=== docker seccomp=unconfined ==="; docker run --rm --security-opt seccomp=unconfined -v /tmp/p:/p:ro ubuntu:24.04 /p || true - echo "=== docker privileged ==="; docker run --rm --privileged -v /tmp/p:/p:ro ubuntu:24.04 /p || true + /tmp/p # E2E suite (already compiled as part of ioxide.slnx). Drives real reactors over real sockets; # pg/redis tests use the sidecars above. Non-zero exit fails the job before any pack/publish. From 1909f2173f18a805132b5ccddd36715b35ee629a Mon Sep 17 00:00:00 2001 From: Diogo Martins Date: Mon, 22 Jun 2026 14:19:16 +0100 Subject: [PATCH 5/6] Test memlock as the io_uring_setup failure cause + raise it for the e2e Probe accumulates 8192-entry rings at 8MB memlock vs raised; the e2e step raises RLIMIT_MEMLOCK before running (ioxide rings pin memory, many reactors exhaust 8MB). --- .github/workflows/build.yml | 40 +++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 97f9f8a..9b319ab 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -68,34 +68,31 @@ jobs: # Diagnostic: io_uring_setup errno on the host, and whether a seccomp-unconfined / privileged # container can bypass the block (so the e2e could run inside one instead of needing self-hosted). - - name: Probe io_uring (flags + entries) + - name: Probe io_uring (memlock accumulation at 8192 entries) run: | - echo "memlock (ulimit -l): $(ulimit -l) KB" + echo "memlock soft/hard: $(ulimit -Sl) / $(ulimit -Hl) KB" cat > /tmp/p.c <<'PROBE' #include #include #include - #include + #include #include - static void t(const char* n, unsigned int flags, unsigned int entries){ - unsigned char p[256]={0}; - *(unsigned int*)(p+8)=flags; + int main(int argc, char** argv){ + unsigned flags=(1u<<12)|(1u<<13)|(1u<<16); + unsigned entries=argc>1?(unsigned)atoi(argv[1]):8192; + int n=0; + for(;;){ + unsigned char p[256]={0}; *(unsigned*)(p+8)=flags; long fd=syscall(425,entries,p); - if(fd<0) printf("%-34s FAIL errno=%d (%s)\n",n,errno,strerror(errno)); - else { printf("%-34s OK fd=%ld\n",n,fd); close(fd); } + if(fd<0){printf("entries=%u: FAILED at ring #%d errno=%d (%s)\n",entries,n,errno,strerror(errno));return 0;} + if(++n>=80){printf("entries=%u: created 80 rings OK\n",entries);return 0;} + } } - int main(void){ - unsigned SI=1u<<12, DTR=1u<<13, NSA=1u<<16; - t("flags=0 e=8",0,8); - t("SINGLE_ISSUER e=8",SI,8); - t("SI|DEFER_TASKRUN e=8",SI|DTR,8); - t("SI|DTR|NO_SQARRAY e=8",SI|DTR|NSA,8); - t("SI|DTR|NO_SQARRAY e=256",SI|DTR|NSA,256); - t("SI|DTR|NO_SQARRAY e=4096",SI|DTR|NSA,4096); - return 0;} PROBE gcc -O2 -o /tmp/p /tmp/p.c - /tmp/p + echo "--- accumulate 8192-entry rings @ default memlock ---"; /tmp/p 8192 + sudo prlimit --pid $$ --memlock=-1:-1 2>/dev/null || ulimit -l unlimited 2>/dev/null || true + echo "--- after raising memlock to $(ulimit -Sl) ---"; /tmp/p 8192 # E2E suite (already compiled as part of ioxide.slnx). Drives real reactors over real sockets; # pg/redis tests use the sidecars above. Non-zero exit fails the job before any pack/publish. @@ -107,7 +104,12 @@ jobs: EXAMPLES_PG_DB: bench EXAMPLES_REDIS_HOST: 127.0.0.1 EXAMPLES_REDIS_PORT: 6379 - run: dotnet run --project Tests/ioxide.e2e.csproj --configuration Release --no-build + run: | + # ioxide rings (RingEntries=8192) pin memory; the e2e starts many reactors, so the + # default 8 MB RLIMIT_MEMLOCK is exhausted. Raise it before the run. + sudo prlimit --pid $$ --memlock=-1:-1 2>/dev/null || ulimit -l unlimited 2>/dev/null || true + echo "memlock for e2e: $(ulimit -Sl) KB" + dotnet run --project Tests/ioxide.e2e.csproj --configuration Release --no-build - name: Pack ioxide run: dotnet pack ioxide/ioxide.csproj --configuration Release --no-build --output ./artifacts From 4b5d6c2765a7d1f881d5c2ec5090909ad656c169 Mon Sep 17 00:00:00 2001 From: Diogo Martins Date: Mon, 22 Jun 2026 14:22:53 +0100 Subject: [PATCH 6/6] Run the ioxide.e2e suite in the build workflow Add postgres + redis service containers and run Tests/ioxide.e2e after the build, before pack/publish, so a failing test blocks the release. Raise RLIMIT_MEMLOCK first: io_uring rings pin locked memory (RingEntries=8192), and the runner's default 8 MB cap is exhausted after ~10 reactors. Load the tls module for the kTLS test; pg/redis/kTLS skip cleanly if absent. 15 passed, 0 failed. --- .github/workflows/build.yml | 40 ++++--------------------------------- 1 file changed, 4 insertions(+), 36 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9b319ab..aee6320 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -56,43 +56,11 @@ jobs: - name: Build run: dotnet build ioxide.slnx --configuration Release --no-restore - # io_uring is needed by the reactor; the tls module by the kTLS test. Best-effort — io_uring is - # enabled by default on the runners, and the suite skips the kTLS test if the module is absent. - - name: Enable io_uring + load kTLS module + # kTLS module for the TLS e2e test; io_uring enable in case a runner disables it (both best-effort). + - name: Prepare runtime (kTLS + io_uring) run: | - echo "kernel: $(uname -r)" - echo "io_uring_disabled (before): $(cat /proc/sys/kernel/io_uring_disabled 2>/dev/null || echo n/a)" - sudo sysctl -w kernel.io_uring_disabled=0 || echo "WARN: could not set kernel.io_uring_disabled" - echo "io_uring_disabled (after): $(cat /proc/sys/kernel/io_uring_disabled 2>/dev/null || echo n/a)" - sudo modprobe tls 2>/dev/null && echo "tls module loaded" || echo "tls module unavailable (kTLS test will skip)" - - # Diagnostic: io_uring_setup errno on the host, and whether a seccomp-unconfined / privileged - # container can bypass the block (so the e2e could run inside one instead of needing self-hosted). - - name: Probe io_uring (memlock accumulation at 8192 entries) - run: | - echo "memlock soft/hard: $(ulimit -Sl) / $(ulimit -Hl) KB" - cat > /tmp/p.c <<'PROBE' - #include - #include - #include - #include - #include - int main(int argc, char** argv){ - unsigned flags=(1u<<12)|(1u<<13)|(1u<<16); - unsigned entries=argc>1?(unsigned)atoi(argv[1]):8192; - int n=0; - for(;;){ - unsigned char p[256]={0}; *(unsigned*)(p+8)=flags; - long fd=syscall(425,entries,p); - if(fd<0){printf("entries=%u: FAILED at ring #%d errno=%d (%s)\n",entries,n,errno,strerror(errno));return 0;} - if(++n>=80){printf("entries=%u: created 80 rings OK\n",entries);return 0;} - } - } - PROBE - gcc -O2 -o /tmp/p /tmp/p.c - echo "--- accumulate 8192-entry rings @ default memlock ---"; /tmp/p 8192 - sudo prlimit --pid $$ --memlock=-1:-1 2>/dev/null || ulimit -l unlimited 2>/dev/null || true - echo "--- after raising memlock to $(ulimit -Sl) ---"; /tmp/p 8192 + sudo modprobe tls 2>/dev/null || true + sudo sysctl -w kernel.io_uring_disabled=0 2>/dev/null || true # E2E suite (already compiled as part of ioxide.slnx). Drives real reactors over real sockets; # pg/redis tests use the sidecars above. Non-zero exit fails the job before any pack/publish.