Send a zero from an inline RWF_NOWAIT read to the pool

Linux 5.9 and 5.10 answer 0 instead of EAGAIN for an RWF_NOWAIT read of
data that is not cached. The inline read took that 0 as end of file and
ended the ReadStream early. A zero from the inline read now goes to the
pool, whose read returns the data or confirms end of file; this costs one
pool round trip at the end of each file.

fs-stream-nowait.test.ts preloads a preadv2 shim that answers 0 for every
RWF_NOWAIT read and checks that a ReadStream still reads the whole file,
as Node does.
This commit is contained in:
Chris Tate
2026-10-29 07:05:55 -05:00
parent 1acdda7328
commit d478dc9ec5
4 changed files with 84 additions and 1 deletions
+4 -1
View File
@@ -186,7 +186,10 @@ static bool scr_fss_try_inline(ScrFssOp *op) {
if (errno == EOPNOTSUPP || errno == EINVAL || errno == ENOSYS) f->inline_ok = false;
return false;
}
if (n == 0 && op->kind == SCR_FSS_WRITE) return false;
/* A zero is not taken as EOF here: some kernels (Linux 5.9 and 5.10)
* answer 0 instead of EAGAIN for an uncached RWF_NOWAIT read, so the
* pool's read decides, as it does for a zero-byte write. */
if (n == 0) return false;
if (op->kind == SCR_FSS_READ && n > 0 && (size_t)n < op->len) {
op->got = (size_t)n;
return false;
+23
View File
@@ -0,0 +1,23 @@
// Reads a file it writes first through a ReadStream and prints the byte
// count and a checksum (fs-stream-nowait.test.ts runs it with a preadv2
// shim).
import { createReadStream, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const dir = mkdtempSync(join(tmpdir(), "scr-nowait-"));
const file = join(dir, "data.bin");
const data = Buffer.alloc(300000);
for (let i = 0; i < data.length; i++) data[i] = (i * 13) & 0xff;
writeFileSync(file, data);
let bytes = 0;
let sum = 0;
createReadStream(file, { highWaterMark: 16384 })
.on("data", (c: Buffer) => {
bytes += c.length;
for (const b of c) sum = (sum + b) % 65521;
})
.on("end", () => {
console.log(`bytes=${bytes} sum=${sum}`);
rmSync(dir, { recursive: true });
});
+13
View File
@@ -0,0 +1,13 @@
/* LD_PRELOAD shim for fs-stream-nowait.test.ts: preadv2 with RWF_NOWAIT
* answers 0, as Linux 5.9 and 5.10 do for an uncached RWF_NOWAIT read;
* other calls go to the real preadv2. */
#define _GNU_SOURCE
#include <dlfcn.h>
#include <sys/uio.h>
ssize_t preadv2(int fd, const struct iovec *iov, int iovcnt, off_t offset, int flags) {
if (flags & RWF_NOWAIT) return 0;
ssize_t (*real)(int, const struct iovec *, int, off_t, int) =
(ssize_t (*)(int, const struct iovec *, int, off_t, int))dlsym(RTLD_NEXT, "preadv2");
return real(fd, iov, iovcnt, offset, flags);
}
+44
View File
@@ -0,0 +1,44 @@
/* fs streams read a regular file with RWF_NOWAIT on the loop thread first
* (scr_fs_stream.c). Linux 5.9 and 5.10 answer 0 instead of EAGAIN for
* an uncached RWF_NOWAIT read; a preloaded preadv2 shim reproduces that
* answer, and the stream must still read the whole file, as Node does. */
import { execFileSync, spawnSync } from "node:child_process";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
import { compile } from "@scriptc/compiler";
const repoRoot = join(import.meta.dirname, "../..");
const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests");
const sanitize = process.env["SCRIPTC_SAN"] === "1";
describe.skipIf(process.platform !== "linux" || sanitize)("fs stream RWF_NOWAIT reads", () => {
test("a zero from an RWF_NOWAIT read is not taken as EOF", async () => {
const outDir = join(cacheDir, "fs-stream-nowait");
const result = await compile(join(repoRoot, "tests/fixtures/fs-stream-nowait-read.ts"), {
outPath: join(outDir, "read"),
outDir,
backend: "llvm",
});
if (!result.ok) {
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
}
const shim = join(outDir, "nowait-zero.so");
execFileSync("clang", [
"-shared",
"-fPIC",
join(repoRoot, "tests/fixtures/fs-stream-nowait-zero.c"),
"-o",
shim,
"-ldl",
]);
const expected = "bytes=300000 sum=50681\n";
const plain = spawnSync(result.binaryPath, [], { encoding: "utf8" });
expect(plain.stdout).toBe(expected);
const shimmed = spawnSync(result.binaryPath, [], {
encoding: "utf8",
env: { ...process.env, LD_PRELOAD: shim },
});
expect(shimmed.status).toBe(0);
expect(shimmed.stdout).toBe(expected);
});
});