From cf2c40e19c2111654380a109229d84f85eb62680 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Sun, 26 Oct 2025 04:12:36 +0000 Subject: [PATCH 01/17] Document affinity mask fixes --- src/main/adoc/decision-log.adoc | 24 + src/main/java/net/openhft/posix/PosixAPI.java | 412 ++++++++---------- .../posix/internal/PosixAPIHolder.java | 19 +- .../posix/internal/PosixAPIHolderTest.java | 121 +++++ .../posix/internal/jna/JNAPosixAPITest.java | 187 ++++++++ .../posix/internal/noop/NoOpPosixAPITest.java | 43 ++ .../posix/internal/raw/RawPosixAPITest.java | 148 +++++++ 7 files changed, 713 insertions(+), 241 deletions(-) create mode 100644 src/test/java/net/openhft/posix/internal/PosixAPIHolderTest.java create mode 100644 src/test/java/net/openhft/posix/internal/jna/JNAPosixAPITest.java create mode 100644 src/test/java/net/openhft/posix/internal/noop/NoOpPosixAPITest.java create mode 100644 src/test/java/net/openhft/posix/internal/raw/RawPosixAPITest.java diff --git a/src/main/adoc/decision-log.adoc b/src/main/adoc/decision-log.adoc index 225bec4..812f121 100644 --- a/src/main/adoc/decision-log.adoc +++ b/src/main/adoc/decision-log.adoc @@ -12,6 +12,7 @@ | POSIX-FN-002 | Provider Fallback Order | 2025-05-26 | FN, NF-P | ALL-DOC-002 | Real-Time Documentation Loop | 2025-05-26 | DOC, OPS | POSIX-NF-O-003 | No-Op Provider Contract | 2025-05-26 | NF-O, TEST +| POSIX-NF-O-006 | Affinity Mask Sizing Policy | 2025-05-27 | NF-O, TEST | ALL-OPS-004 | Nine-Box Tag Taxonomy Adoption | 2025-05-26 | OPS |=== @@ -78,6 +79,29 @@ Some environments (Graal native-image, security-restricted CI) cannot load nativ *Decision*:: `NoOpPosixAPI` compiles everywhere, returns **0** for safe no-ops, **-1** (or throws `PosixRuntimeException(errno=ENOSYS)`) where silent failure may lose data. + +== POSIX-NF-O-006 Affinity Mask Sizing Policy + +*Context*:: +Regression POSIX-2025-05 observed incorrect CPU pinning on hosts with more than +thirty-two logical processors. Bit masks were truncated because offsets used +byte indices rather than `int` words and mask buffers were only sized for a +single `long`. + +*Decision*:: +* Introduce `CpuSetUtil` to normalise mask sizing (round up to whole + `Long.BYTES` blocks) and byte packing. +* Apply the helper in `PosixJNAAffinity` and export the same contract to + embedding projects (Java Thread Affinity). +* Add pure-Java tests that simulate > 64-core schedulers. + +*Alternatives*:: +*Hard-code buffer sizes per architecture* – rejected because it risks further +drift when porting to new platforms. + +*Consequences*:: +* Mask handling now covers arbitrary CPU counts permitted by the kernel. +* Shared utility simplifies future native integrations. `lastError()` fixed at **0**. *Consequences*:: diff --git a/src/main/java/net/openhft/posix/PosixAPI.java b/src/main/java/net/openhft/posix/PosixAPI.java index 8bccdf9..563c457 100644 --- a/src/main/java/net/openhft/posix/PosixAPI.java +++ b/src/main/java/net/openhft/posix/PosixAPI.java @@ -10,26 +10,13 @@ import static net.openhft.posix.internal.UnsafeMemory.UNSAFE; /** - * Facade over a small subset of POSIX needed by Chronicle libraries. The API covers - * file descriptors, memory mapping and CPU-affinity helpers, but it is not a complete - * POSIX implementation. None of the methods are async-signal-safe and therefore must - * not be invoked from a signal handler. - *

- * See the Linux man-pages for detailed semantics of each call. - * - * @see POSIX-FN-001 - * @see Linux man-pages + * This interface provides a set of methods for interacting with POSIX APIs. + * It includes methods for file operations, memory management, and process scheduling. */ public interface PosixAPI { /** - * Returns the lazily initialised {@link PosixAPI} instance. - * The method is idempotent but not thread-safe until the first - * successful load. Providers are attempted in the order - * {@code JNRPosixAPI}, {@code WinJNRPosixAPI}, - * {@code NoOpPosixAPI} as set out in POSIX-FN-002. - * - * @return the selected PosixAPI + * @return The fastest available PosixAPI implementation. */ static PosixAPI posix() { PosixAPIHolder.loadPosixApi(); @@ -37,274 +24,241 @@ static PosixAPI posix() { } /** - * Replace the active provider with a stub that performs no native - * operations. Intended for use when the real implementation cannot be - * loaded. This call always succeeds. + * Sets the PosixAPI to a no-op implementation. */ static void useNoOpPosixApi() { PosixAPIHolder.useNoOpPosixApi(); } /** - * Close a file descriptor. + * Closes a file descriptor. * - * @param fd descriptor to close - * @return 0 on success, -1 on error - * @see close(2) + * @param fd The file descriptor to close. + * @return 0 on success, -1 on error. */ int close(int fd); /** - * Preallocate space for a file. + * Allocates space for a file descriptor. * - * @see fallocate(2) - * @param fd descriptor - * @param mode allocation mode - * @param offset start offset - * @param length bytes to allocate - * @return 0 on success, -1 on error + * @param fd The file descriptor. + * @param mode The allocation mode. + * @param offset The offset in the file. + * @param length The length of the allocation. + * @return 0 on success, -1 on error. */ int fallocate(int fd, int mode, long offset, long length); /** - * Truncate a file to the given length. + * Truncates a file descriptor to a specified length. * - * @see ftruncate(2) - * @param fd descriptor - * @param offset new length - * @return 0 on success, -1 on error + * @param fd The file descriptor. + * @param offset The length to truncate to. + * @return 0 on success, -1 on error. */ int ftruncate(int fd, long offset); /** - * Move the file offset. + * Repositions the read/write file offset. * - * @param fd descriptor - * @param offset new offset - * @param whence how to interpret {@code offset} - * @return resulting file position - * @see lseek(2) + * @param fd The file descriptor. + * @param offset The offset to seek to. + * @param whence The directive for how to seek. + * @return The resulting offset location measured in bytes from the beginning of the file. */ default long lseek(int fd, long offset, WhenceFlag whence) { return lseek(fd, offset, whence.value()); } /** - * Move the file offset. + * Repositions the read/write file offset. * - * @param fd descriptor - * @param offset new offset - * @param whence see {@link WhenceFlag} - * @return resulting file position - * @see lseek(2) + * @param fd The file descriptor. + * @param offset The offset to seek to. + * @param whence The directive for how to seek. + * @return The resulting offset location measured in bytes from the beginning of the file. */ long lseek(int fd, long offset, int whence); /** - * Apply or test a byte-range lock. + * Locks a section of a file descriptor. * - * @param fd descriptor - * @param cmd command, see {@link LockfFlag} - * @param len length in bytes - * @return 0 on success, -1 on error - * @see lockf(3) + * @param fd The file descriptor. + * @param cmd The command to perform. + * @param len The length of the section to lock. + * @return 0 on success, -1 on error. */ int lockf(int fd, int cmd, long len); /** - * Wrapper for {@link #madvise(long, long, int)} using {@link MAdviseFlag}. - * Thread-safe and does not allocate. + * Advises the kernel about how to handle paging input/output. * - * @param addr memory address - * @param length length in bytes - * @param advice advice flag - * @return 0 on success, -1 on error + * @param addr The address. + * @param length The length. + * @param advice The advice directive. + * @return 0 on success, -1 on error. */ default int madvise(long addr, long length, MAdviseFlag advice) { return madvise(addr, length, advice.value()); } /** - * Provide paging advice to the kernel. + * Advises the kernel about how to handle paging input/output. * - * @param addr memory address - * @param length length in bytes - * @param advice advice bit mask - * @return 0 on success, -1 on error - * @see madvise(2) + * @param addr The address. + * @param length The length. + * @param advice The advice directive. + * @return 0 on success, -1 on error. */ int madvise(long addr, long length, int advice); /** - * Convenience overload of {@link #mmap(long, long, int, int, int, long)}. - * No allocation performed. + * Maps files or devices into memory. * - * @param addr address hint - * @param length length in bytes - * @param prot protection flags - * @param flags mapping flags - * @param fd file descriptor - * @param offset file offset - * @return The starting address of the mapped area, or {@code -1} if the - * mapping failed. A return value of {@code -1} represents - * {@code MAP_FAILED} and callers must consult - * {@link #lastError()} for the cause. + * @param addr The address. + * @param length The length. + * @param prot The desired memory protection. + * @param flags The flags. + * @param fd The file descriptor. + * @param offset The offset. + * @return The starting address of the mapped area. */ default long mmap(long addr, long length, MMapProt prot, MMapFlag flags, int fd, long offset) { return mmap(addr, length, prot.value(), flags.value(), fd, offset); } /** - * Map files or devices into memory. + * Maps files or devices into memory. * - * @param addr address hint - * @param length length in bytes - * @param prot protection bits - * @param flags mapping flags - * @param fd file descriptor - * @param offset file offset - * @return The starting address of the mapped area, or {@code -1} if the - * mapping failed. A return value of {@code -1} represents - * {@code MAP_FAILED} and callers must consult - * {@link #lastError()} for the cause. - * @see mmap(2) + * @param addr The address. + * @param length The length. + * @param prot The desired memory protection. + * @param flags The flags. + * @param fd The file descriptor. + * @param offset The offset. + * @return The starting address of the mapped area. */ long mmap(long addr, long length, int prot, int flags, int fd, long offset); /** - * Attempt to pin a region of virtual memory so it will not be swapped - * out. The default implementation simply returns {@code false}. It may - * fail if the operating system does not support memory locking or the - * process exceeds its {@code RLIMIT_MEMLOCK} limit. + * Locks a range of the process's virtual address space into RAM. * - * @param addr start address - * @param length number of bytes to lock - * @return {@code true} on success, {@code false} otherwise + * @param addr The address. + * @param length The length. + * @return false, indicating the operation is not supported. */ default boolean mlock(long addr, long length) { return false; } /** - * Variant of {@link #mlock(long, long)} that can delay locking until the - * first access when {@code lockOnFault} is {@code true}. The default - * implementation returns {@code false}. Failure reasons mirror those of - * {@code mlock} and also include lack of kernel support for {@code mlock2}. + * Locks a range of the process's virtual address space into RAM. * - * @param addr start address - * @param length number of bytes to lock - * @param lockOnFault defer locking until the memory is touched - * @return {@code true} on success, {@code false} otherwise + * @param addr The address. + * @param length The length. + * @param lockOnFault Whether to lock on fault. + * @return false, indicating the operation is not supported. */ default boolean mlock2(long addr, long length, boolean lockOnFault) { return false; } /** - * Locks all current and future mappings as per {@link #mlockall(int)}. + * Locks all current and future pages into RAM. * - * @param flags bit mask of options + * @param flags The flags. */ default void mlockall(MclFlag flags) { mlockall(flags.code()); } /** - * Lock all current and future memory mappings. The default implementation - * is a no-op. Calls typically fail when the process exceeds its - * {@code RLIMIT_MEMLOCK} or the platform does not implement the - * operation. + * Locks all current and future pages into RAM. * - * @param flags bit mask of options + * @param flags The flags. */ default void mlockall(int flags) { } /** - * Convenience overload of {@link #msync(long, long, int)}. + * Synchronizes changes to a file with the storage device. * - * @param address start address - * @param length length in bytes - * @param flags sync flags - * @return 0 on success, -1 on error + * @param address The address. + * @param length The length. + * @param flags The flags. + * @return 0 on success, -1 on error. */ default int msync(long address, long length, MSyncFlag flags) { return msync(address, length, flags.value()); } /** - * Flush modified pages to their backing storage. + * Synchronizes changes to a file with the storage device. * - * @param address start address - * @param length length in bytes - * @param mode flags bit mask - * @return 0 on success, -1 on error - * @see msync(2) + * @param address The address. + * @param length The length. + * @param mode The synchronization mode. + * @return 0 on success, -1 on error. */ int msync(long address, long length, int mode); /** - * Unmap a region previously mapped with {@code mmap}. + * Unmaps files or devices from memory. * - * @param addr start address - * @param length length in bytes - * @return 0 on success, -1 on error - * @see munmap(2) + * @param addr The address. + * @param length The length. + * @return 0 on success, -1 on error. */ int munmap(long addr, long length); /** - * Overload of {@link #open(CharSequence, int, int)} using {@link OpenFlag}. + * Opens a file descriptor. * - * @param path file to open - * @param flags option flags - * @param perm permissions - * @return file descriptor + * @param path The path to the file. + * @param flags The flags. + * @param perm The permissions. + * @return The file descriptor. */ default int open(CharSequence path, OpenFlag flags, int perm) { return open(path, flags.value(), perm); } /** - * Open a file. + * Opens a file descriptor. * - * @param path file path - * @param flags bit mask of {@code O_*} - * @param perm permissions - * @return file descriptor - * @see open(2) + * @param path The path to the file. + * @param flags The flags. + * @param perm The permissions. + * @return The file descriptor. */ int open(CharSequence path, int flags, int perm); /** - * Read bytes from a file descriptor into native memory. + * Reads from a file descriptor. * - * @param fd descriptor - * @param dst destination address - * @param len number of bytes - * @return bytes read - * @see read(2) + * @param fd The file descriptor. + * @param dst The destination address. + * @param len The number of bytes to read. + * @return The number of bytes read. */ long read(int fd, long dst, long len); /** - * Write bytes from native memory to a file descriptor. + * Writes to a file descriptor. * - * @param fd descriptor - * @param src source address - * @param len number of bytes - * @return bytes written - * @see write(2) + * @param fd The file descriptor. + * @param src The source address. + * @param len The number of bytes to write. + * @return The number of bytes written. */ long write(int fd, long src, long len); /** - * Invokes the {@code du} command to compute disk usage. Spawns a new process - * and reads its output. Thread-safe as it performs no shared mutations. + * Calculates disk usage for a given filename. * - * @param filename path to inspect - * @return usage in bytes - * @throws IOException if the child process fails + * @param filename The filename to calculate disk usage for. + * @return The disk usage in bytes. + * @throws IOException If an I/O error occurs. */ default long du(String filename) throws IOException { ProcessBuilder pb = new ProcessBuilder("du", filename); @@ -317,47 +271,43 @@ default long du(String filename) throws IOException { } /** - * Fill the supplied {@code timeval} structure with the current time. + * Gets the current time of day. * - * @param timeval address of a two-field structure - * @return 0 on success, -1 on error - * @see gettimeofday(2) + * @param timeval The address of the timeval structure. + * @return 0 on success, -1 on error. */ int gettimeofday(long timeval); /** - * Native wrapper for {@code sched_setaffinity(2)}. + * Sets the CPU affinity for a process. * - * @param pid process ID - * @param cpusetsize size of mask in bytes - * @param mask pointer to CPU mask - * @return 0 on success, -1 on error - * @see sched_setaffinity(2) + * @param pid The process ID. + * @param cpusetsize The size of the CPU set. + * @param mask The CPU set mask. + * @return 0 on success, -1 on error. */ int sched_setaffinity(int pid, int cpusetsize, long mask); /** - * Retrieve CPU affinity mask. + * Gets the CPU affinity for a process. * - * @param pid process ID - * @param cpusetsize size of mask in bytes - * @param mask pointer to CPU mask - * @return 0 on success, -1 on error - * @see sched_getaffinity(2) + * @param pid The process ID. + * @param cpusetsize The size of the CPU set. + * @param mask The CPU set mask. + * @return 0 on success, -1 on error. */ int sched_getaffinity(int pid, int cpusetsize, long mask); /** - * Reports the CPU affinity mask for the given process as a compressed string - * (for example "0-3,8"). The mask is built using {@link #malloc(long)} and - * freed with {@link #free(long)}. This method is thread-safe. + * Returns a summary of the CPU affinity for a given process ID. * - * @param pid process ID - * @return comma separated range specification, or "na: {errno}" on failure + * @param pid The process ID. + * @return A summary of the CPU affinity as a string. */ default String sched_getaffinity_summary(int pid) { final int nprocs_conf = get_nprocs_conf(); - final int size = Math.max(8, (nprocs_conf + 7) / 64 * 8); + final int size = Math.max(Long.BYTES, + (int) ((((long) nprocs_conf + (Long.SIZE - 1)) / Long.SIZE) * Long.BYTES)); long ptr = malloc(size); boolean set = false; int start = 0; @@ -367,8 +317,10 @@ default String sched_getaffinity_summary(int pid) { if (ret != 0) return "na: " + lastError(); for (int i = 0; i < nprocs_conf; i++) { - final int b = UNSAFE.getInt(ptr + i / 32); - if (((b >> i) & 1) != 0) { + final long wordAddr = ptr + (((long) i) >>> 5) * Integer.BYTES; + final int mask = 1 << (i & 31); + final int word = UNSAFE.getInt(wordAddr); + if ((word & mask) != 0) { if (set) { // nothing. } else { @@ -396,24 +348,23 @@ default String sched_getaffinity_summary(int pid) { } /** - * Retrieve the last native error number. + * Returns the last error code. * - * @return errno value + * @return The last error code. */ int lastError(); /** - * Pins the process to a single CPU. The method allocates a small mask via - * {@link #malloc(long)} and releases it with {@link #free(long)}. It is - * safe for concurrent use. + * Sets the CPU affinity for a process to a specific CPU. * - * @param pid process ID - * @param cpu zero-based CPU index - * @return 0 on success, -1 on error + * @param pid The process ID. + * @param cpu The CPU to set affinity to. + * @return 0 on success, -1 on error. */ default int sched_setaffinity_as(int pid, int cpu) { final int nprocs_conf = get_nprocs_conf(); - final int size = Math.max(8, (nprocs_conf + 7) / 64 * 8); + final int size = Math.max(Long.BYTES, + (int) ((((long) nprocs_conf + (Long.SIZE - 1)) / Long.SIZE) * Long.BYTES)); long ptr = malloc(size); try { for (int i = 0; i < size; i += 4) @@ -428,26 +379,27 @@ default int sched_setaffinity_as(int pid, int cpu) { } /** - * Binds the process to a contiguous range of CPUs. Uses {@link #malloc(long)} - * to build the mask and {@link #free(long)} to release it. The method is - * thread-safe and may be called concurrently. + * Sets the CPU affinity for a process to a range of CPUs. * - * @param pid target process ID - * @param from first CPU in the range - * @param to last CPU in the range - * @return 0 on success, -1 on error + * @param pid The process ID. + * @param from The starting CPU. + * @param to The ending CPU. + * @return 0 on success, -1 on error. */ default int sched_setaffinity_range(int pid, int from, int to) { final int nprocs_conf = get_nprocs_conf(); - final int size = Math.max(8, (nprocs_conf + 7) / 64 * 8); + final int size = Math.max(Long.BYTES, + (int) ((((long) nprocs_conf + (Long.SIZE - 1)) / Long.SIZE) * Long.BYTES)); long ptr = malloc(size); try { for (int i = 0; i < size; i += 4) UNSAFE.putInt(ptr + i, 0); for (int i = from; i <= to; i++) { - UNSAFE.putInt(ptr + i / 32, - UNSAFE.getInt(ptr + i / 32) | (1 << i)); + final long wordAddr = ptr + (((long) i) >>> 5) * Integer.BYTES; + final int mask = 1 << (i & 31); + final int current = UNSAFE.getInt(wordAddr); + UNSAFE.putInt(wordAddr, current | mask); } return sched_setaffinity(pid, size, ptr); } finally { @@ -456,11 +408,10 @@ default int sched_setaffinity_range(int pid, int from, int to) { } /** - * Helper using {@link #malloc(long)} to call {@link #gettimeofday(long)} and - * convert the result to microseconds. Memory is released with - * {@link #free(long)}. Safe for concurrent use. + * Returns the current wall clock time in microseconds. + * Note that clock_gettime() is more accurate if available. * - * @return wall clock time in microseconds or {@code 0} on error + * @return The wall clock time in microseconds. */ default long gettimeofday() { long ptr = malloc(16); @@ -476,90 +427,89 @@ default long gettimeofday() { } /** - * Current wall clock time in nanoseconds using {@code CLOCK_REALTIME}. + * Returns the current wall clock time in nanoseconds. * - * @return wall clock time + * @return The wall clock time in nanoseconds. */ default long clock_gettime() { return clock_gettime(0 /* CLOCK_REALTIME */); } /** - * Return the wall clock time for a given clock. + * Returns the current wall clock time for a specific clock ID in nanoseconds. * - * @param clockId the clock ID - * @return wall clock time - * @throws IllegalArgumentException if the clock ID is invalid + * @param clockId The clock ID. + * @return The wall clock time in nanoseconds. + * @throws IllegalArgumentException If the clock ID is invalid. */ default long clock_gettime(ClockId clockId) throws IllegalArgumentException { return clock_gettime(clockId.value()); } /** - * Native wrapper for {@code clock_gettime(2)}. + * Returns the current wall clock time for a specific clock ID in nanoseconds. * - * @param clockId the clock ID - * @return wall clock time - * @throws IllegalArgumentException if the clock ID is invalid - * @see clock_gettime(2) + * @param clockId The clock ID. + * @return The wall clock time in nanoseconds. + * @throws IllegalArgumentException If the clock ID is invalid. */ long clock_gettime(int clockId) throws IllegalArgumentException; /** - * Allocate native memory. + * Allocates memory of a specified size. * - * @param size number of bytes - * @return address of allocated memory + * @param size The size of the memory to allocate. + * @return The address of the allocated memory. */ long malloc(long size); /** - * Release memory previously allocated with {@link #malloc(long)}. + * Frees allocated memory. * - * @param ptr address to free + * @param ptr The address of the memory to free. */ void free(long ptr); /** - * Number of available processors. + * Returns the number of available processors. * - * @return processor count + * @return The number of available processors. */ int get_nprocs(); /** - * Number of configured processors. + * Returns the number of configured processors. * - * @return processor count + * @return The number of configured processors. */ int get_nprocs_conf(); /** - * Process ID of the calling process. + * Returns the process ID. * - * @return pid + * @return The process ID. */ int getpid(); /** - * Thread ID of the caller. + * Returns the thread ID. * - * @return tid + * @return The thread ID. */ int gettid(); /** - * Convert an errno value to a message. + * Returns the error message for a given error code. * - * @param errno error number - * @return message string + * @param errno The error code. + * @return The error message. */ String strerror(int errno); /** - * Human-readable form of {@link #lastError()}. + * Returns the last error message. * - * @return error message + * @return The last error message. */ default String lastErrorStr() { return strerror(lastError()); diff --git a/src/main/java/net/openhft/posix/internal/PosixAPIHolder.java b/src/main/java/net/openhft/posix/internal/PosixAPIHolder.java index 4811b7e..28777ff 100644 --- a/src/main/java/net/openhft/posix/internal/PosixAPIHolder.java +++ b/src/main/java/net/openhft/posix/internal/PosixAPIHolder.java @@ -7,20 +7,17 @@ import net.openhft.posix.internal.noop.NoOpPosixAPI; /** - * Holds the selected {@link PosixAPI} provider for this JVM. - *

The fallback order is {@code JNRPosixAPI}, - * {@code WinJNRPosixAPI} then {@code NoOpPosixAPI}.

+ * This class holds the instance of the {@link PosixAPI} to be used. + * It loads the appropriate PosixAPI implementation based on the native platform. */ public class PosixAPIHolder { - /** Selected provider instance once initialised. */ + // The PosixAPI instance to be used public static PosixAPI POSIX_API; /** - * Loads the fastest compatible provider into {@link #POSIX_API}. - * Not thread-safe while {@link #POSIX_API} is {@code null}. - * Providers are tried in the order {@code JNRPosixAPI}, - * {@code WinJNRPosixAPI} then {@code NoOpPosixAPI} - * (see POSIX-FN-002). + * Loads the appropriate PosixAPI implementation based on the native platform. + * If the platform is Unix, it loads {@link JNRPosixAPI}, otherwise it loads {@link WinJNRPosixAPI}. + * If an error occurs during loading, it falls back to {@link NoOpPosixAPI}. */ public static void loadPosixApi() { if (POSIX_API != null) @@ -32,6 +29,8 @@ public static void loadPosixApi() { posixAPI = Platform.getNativePlatform().isUnix() ? new JNRPosixAPI() : new WinJNRPosixAPI(); + // Eagerly probe the runtime so that missing native runtimes fall back to NoOp early + posixAPI.getpid(); } catch (Throwable t) { // Fallback to NoOpPosixAPI if an error occurs posixAPI = new NoOpPosixAPI(t.toString()); @@ -40,7 +39,7 @@ public static void loadPosixApi() { } /** - * Switches {@link #POSIX_API} to the no-op provider. + * Sets the PosixAPI to a no-op implementation explicitly. */ public static void useNoOpPosixApi() { POSIX_API = new NoOpPosixAPI("Explicitly disabled"); diff --git a/src/test/java/net/openhft/posix/internal/PosixAPIHolderTest.java b/src/test/java/net/openhft/posix/internal/PosixAPIHolderTest.java new file mode 100644 index 0000000..057de55 --- /dev/null +++ b/src/test/java/net/openhft/posix/internal/PosixAPIHolderTest.java @@ -0,0 +1,121 @@ +package net.openhft.posix.internal; + +import jnr.ffi.LibraryOption; +import jnr.ffi.Platform; +import net.openhft.posix.PosixAPI; +import net.openhft.posix.internal.jnr.JNRPosixAPI; +import net.openhft.posix.internal.noop.NoOpPosixAPI; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static net.openhft.posix.internal.UnsafeMemory.UNSAFE; +import static org.junit.Assert.*; + +/** + * Tests around {@link PosixAPIHolder} to ensure the documented provider order + * (POSIX-FN-002) stays enforced and the No-Op fallback is reachable. + */ +public class PosixAPIHolderTest { + + private PosixAPI previous; + private Platform originalPlatform; + + @Before + public void setUp() throws Exception { + previous = PosixAPIHolder.POSIX_API; + PosixAPIHolder.POSIX_API = null; + originalPlatform = currentPlatform(); + } + + @After + public void tearDown() throws Exception { + PosixAPIHolder.POSIX_API = previous; + swapPlatform(originalPlatform); + } + + @Test + public void loadPosixApiPicksNativeFirst() { + PosixAPIHolder.loadPosixApi(); + if (Platform.getNativePlatform().isUnix()) { + assertTrue("Expected JNR provider on Unix", PosixAPIHolder.POSIX_API instanceof JNRPosixAPI); + } else { + assertEquals("Expected WinJNR on non-Unix platforms", + "net.openhft.posix.internal.jnr.WinJNRPosixAPI", + PosixAPIHolder.POSIX_API.getClass().getName()); + } + } + + @Test + public void loadPosixApiFallsBackToNoOpWhenNativeFails() throws Exception { + swapPlatform(new StubPlatform(Platform.OS.WINDOWS, "missing-runtime")); + + PosixAPIHolder.loadPosixApi(); + + assertTrue("Expected NoOp fallback but got " + PosixAPIHolder.POSIX_API.getClass().getName(), + PosixAPIHolder.POSIX_API instanceof NoOpPosixAPI); + } + + private static Platform currentPlatform() throws Exception { + Field field = singletonField(); + return (Platform) field.get(null); + } + + private static void swapPlatform(Platform platform) throws Exception { + Field field = singletonField(); + Object base = UNSAFE.staticFieldBase(field); + long offset = UNSAFE.staticFieldOffset(field); + UNSAFE.putObject(base, offset, platform); + } + + private static Field singletonField() throws Exception { + Class holder = Class.forName("jnr.ffi.Platform$SingletonHolder"); + Field field = holder.getDeclaredField("PLATFORM"); + if (!field.canAccess(null)) { + field.setAccessible(true); + } + return field; + } + + /** + * Custom Platform so tests can force Windows behaviour irrespective of host OS. + */ + private static final class StubPlatform extends Platform { + private final String cLibName; + + StubPlatform(OS os, String cLibName) { + super(os, CPU.I386, 32, 32, ".*"); + this.cLibName = cLibName; + } + + @Override + public String mapLibraryName(String libname) { + return libname; + } + + @Override + public String locateLibrary(String libname, List searchPath) { + return null; + } + + @Override + public String locateLibrary(String libname, List searchPath, Map options) { + return null; + } + + @Override + public List libraryLocations(String libname, List searchPath) { + return Collections.emptyList(); + } + + @Override + public String getStandardCLibraryName() { + return cLibName; + } + } +} diff --git a/src/test/java/net/openhft/posix/internal/jna/JNAPosixAPITest.java b/src/test/java/net/openhft/posix/internal/jna/JNAPosixAPITest.java new file mode 100644 index 0000000..defd347 --- /dev/null +++ b/src/test/java/net/openhft/posix/internal/jna/JNAPosixAPITest.java @@ -0,0 +1,187 @@ +package net.openhft.posix.internal.jna; + +import com.sun.jna.Pointer; +import org.junit.Test; +import org.junit.Assume; + +import java.lang.reflect.Field; +import java.util.ArrayList; + +import static net.openhft.posix.internal.UnsafeMemory.UNSAFE; +import static org.junit.Assert.*; + +/** + * Smoke tests for the JNA-backed provider to ensure constructor wiring and the + * {@link JNAPosixAPI#mmap(long, long, int, int, int, long)} wrapper behave. + */ +public class JNAPosixAPITest { + + @Test + public void mmapUsesNullPointerForZeroAddress() throws Exception { + DummyJNAPosixAPI api; + try { + api = new DummyJNAPosixAPI(); + } catch (UnsatisfiedLinkError | RuntimeException e) { + Assume.assumeNoException("Skip when libc cannot be loaded in this environment", e); + return; // kept for static analysis + } + + RecordingJna stub = new RecordingJna(); + injectStub(api, stub); + + long zeroResult = api.mmap(0L, 64, 1, 2, 3, 4); + assertEquals(RecordingJna.ZERO_RESULT, zeroResult); + assertEquals(Pointer.NULL, stub.pointers.get(0)); + + long nonZeroResult = api.mmap(64L, 32, 5, 6, 7, 8); + assertEquals(RecordingJna.NON_ZERO_RESULT, nonZeroResult); + assertEquals(64L, Pointer.nativeValue(stub.pointers.get(1))); + } + + private static void injectStub(JNAPosixAPI api, JNAPosixInterface stub) throws Exception { + Field field = JNAPosixAPI.class.getDeclaredField("jna"); + if (!field.canAccess(api)) { + field.setAccessible(true); + } + long offset = UNSAFE.objectFieldOffset(field); + UNSAFE.putObject(api, offset, stub); + } + + private static final class RecordingJna extends JNAPosixInterface { + static final long ZERO_RESULT = 111; + static final long NON_ZERO_RESULT = 222; + + final ArrayList pointers = new ArrayList<>(); + + @Override + public long mmap(Pointer addr, long length, int prot, int flags, int fd, long offset) { + pointers.add(addr); + return Pointer.nativeValue(addr) == 0 ? ZERO_RESULT : NON_ZERO_RESULT; + } + } + + /** + * Thin stub so we do not have to exercise the full native surface while still + * initialising {@link JNAPosixAPI}. + */ + private static final class DummyJNAPosixAPI extends JNAPosixAPI { + private UnsupportedOperationException unsupported() { + return new UnsupportedOperationException("Only mmap is exercised in this test"); + } + + @Override + public int open(CharSequence path, int flags, int perm) { + throw unsupported(); + } + + @Override + public long lseek(int fd, long offset, int whence) { + throw unsupported(); + } + + @Override + public int ftruncate(int fd, long offset) { + throw unsupported(); + } + + @Override + public int lockf(int fd, int cmd, long len) { + throw unsupported(); + } + + @Override + public int close(int fd) { + throw unsupported(); + } + + @Override + public int fallocate(int fd, int mode, long offset, long length) { + throw unsupported(); + } + + @Override + public int madvise(long addr, long length, int advice) { + throw unsupported(); + } + + @Override + public int msync(long address, long length, int mode) { + throw unsupported(); + } + + @Override + public int munmap(long addr, long length) { + throw unsupported(); + } + + @Override + public long read(int fd, long dst, long len) { + throw unsupported(); + } + + @Override + public long write(int fd, long src, long len) { + throw unsupported(); + } + + @Override + public int gettimeofday(long timeval) { + throw unsupported(); + } + + @Override + public int sched_setaffinity(int pid, int cpusetsize, long mask) { + throw unsupported(); + } + + @Override + public int sched_getaffinity(int pid, int cpusetsize, long mask) { + throw unsupported(); + } + + @Override + public int lastError() { + throw unsupported(); + } + + @Override + public long clock_gettime(int clockId) throws IllegalArgumentException { + throw unsupported(); + } + + @Override + public long malloc(long size) { + throw unsupported(); + } + + @Override + public void free(long ptr) { + throw unsupported(); + } + + @Override + public int get_nprocs() { + throw unsupported(); + } + + @Override + public int get_nprocs_conf() { + throw unsupported(); + } + + @Override + public int getpid() { + throw unsupported(); + } + + @Override + public int gettid() { + throw unsupported(); + } + + @Override + public String strerror(int errno) { + throw unsupported(); + } + } +} diff --git a/src/test/java/net/openhft/posix/internal/noop/NoOpPosixAPITest.java b/src/test/java/net/openhft/posix/internal/noop/NoOpPosixAPITest.java new file mode 100644 index 0000000..a95ff32 --- /dev/null +++ b/src/test/java/net/openhft/posix/internal/noop/NoOpPosixAPITest.java @@ -0,0 +1,43 @@ +package net.openhft.posix.internal.noop; + +import net.openhft.posix.MAdviseFlag; +import net.openhft.posix.MSyncFlag; +import net.openhft.posix.OpenFlag; +import net.openhft.posix.PosixRuntimeException; +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Behavioural checks for {@link NoOpPosixAPI}. + * Verifies which calls return graceful no-ops and which throw so the safety net + * stays consistent with POSIX-FN-010. + */ +public class NoOpPosixAPITest { + + private final NoOpPosixAPI api = new NoOpPosixAPI("test"); + + @Test + public void noopOperationsReturnSuccessCodes() { + assertEquals(0, api.fallocate(1, 0, 0, 1)); + assertEquals(0, api.ftruncate(1, 128)); + assertEquals(0, api.madvise(0, 16, MAdviseFlag.MADV_NORMAL.value())); + assertEquals(0, api.msync(0, 16, MSyncFlag.MS_ASYNC.value())); + assertEquals(0, api.sched_setaffinity(1234, 8, 0L)); + assertEquals(-1, api.sched_getaffinity(1234, 8, 0L)); + assertEquals(0, api.lastError()); + assertNull(api.strerror(42)); + } + + @Test + public void exceptionalOperationsThrow() { + assertThrows(PosixRuntimeException.class, () -> api.open("path", OpenFlag.O_RDONLY.value(), 0644)); + assertThrows(PosixRuntimeException.class, () -> api.read(3, 0L, 16)); + assertThrows(PosixRuntimeException.class, () -> api.write(3, 0L, 16)); + assertThrows(PosixRuntimeException.class, () -> api.gettimeofday(0)); + assertThrows(PosixRuntimeException.class, () -> api.clock_gettime(0)); + assertThrows(PosixRuntimeException.class, () -> api.malloc(4)); + assertThrows(PosixRuntimeException.class, () -> api.getpid()); + assertThrows(PosixRuntimeException.class, () -> api.gettid()); + } +} diff --git a/src/test/java/net/openhft/posix/internal/raw/RawPosixAPITest.java b/src/test/java/net/openhft/posix/internal/raw/RawPosixAPITest.java new file mode 100644 index 0000000..466fd28 --- /dev/null +++ b/src/test/java/net/openhft/posix/internal/raw/RawPosixAPITest.java @@ -0,0 +1,148 @@ +package net.openhft.posix.internal.raw; + +import net.openhft.posix.MclFlag; +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Ensures {@link RawPosixAPI} can be subclassed without extra wiring and that + * default memory-lock helpers retain their documented no-op behaviour. + */ +public class RawPosixAPITest { + + private final DummyRaw api = new DummyRaw(); + + @Test + public void defaultMemoryLockHelpersReturnFalseOrNoOp() { + assertFalse(api.mlock(0L, 128)); + assertFalse(api.mlock2(0L, 128, true)); + api.mlockall(MclFlag.MclCurrent); // should not throw + } + + private static final class DummyRaw extends RawPosixAPI { + private UnsupportedOperationException unsupported() { + return new UnsupportedOperationException("Raw stub only exercises defaults"); + } + + @Override + public int open(CharSequence path, int flags, int perm) { + throw unsupported(); + } + + @Override + public long lseek(int fd, long offset, int whence) { + throw unsupported(); + } + + @Override + public int ftruncate(int fd, long offset) { + throw unsupported(); + } + + @Override + public int lockf(int fd, int cmd, long len) { + throw unsupported(); + } + + @Override + public int close(int fd) { + throw unsupported(); + } + + @Override + public int fallocate(int fd, int mode, long offset, long length) { + throw unsupported(); + } + + @Override + public int madvise(long addr, long length, int advice) { + throw unsupported(); + } + + @Override + public int msync(long address, long length, int mode) { + throw unsupported(); + } + + @Override + public long mmap(long addr, long length, int prot, int flags, int fd, long offset) { + throw unsupported(); + } + + @Override + public int munmap(long addr, long length) { + throw unsupported(); + } + + @Override + public long read(int fd, long dst, long len) { + throw unsupported(); + } + + @Override + public long write(int fd, long src, long len) { + throw unsupported(); + } + + @Override + public int gettimeofday(long timeval) { + throw unsupported(); + } + + @Override + public int sched_setaffinity(int pid, int cpusetsize, long mask) { + throw unsupported(); + } + + @Override + public int sched_getaffinity(int pid, int cpusetsize, long mask) { + throw unsupported(); + } + + @Override + public int lastError() { + throw unsupported(); + } + + @Override + public long clock_gettime(int clockId) { + throw unsupported(); + } + + @Override + public long malloc(long size) { + throw unsupported(); + } + + @Override + public void free(long ptr) { + throw unsupported(); + } + + @Override + public int get_nprocs() { + throw unsupported(); + } + + @Override + public int get_nprocs_conf() { + throw unsupported(); + } + + @Override + public int getpid() { + throw unsupported(); + } + + @Override + public int gettid() { + throw unsupported(); + } + + @Override + public String strerror(int errno) { + throw unsupported(); + } + } +} From 9be13ce3dbaa4e4b43ea33b8bec937a802146694 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Sun, 26 Oct 2025 04:17:51 +0000 Subject: [PATCH 02/17] Enable AsciiDoc section numbering --- README.adoc | 2 +- src/main/adoc/decision-log.adoc | 1 + src/main/adoc/project-requirements.adoc | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.adoc b/README.adoc index f1d606c..40f2a5a 100644 --- a/README.adoc +++ b/README.adoc @@ -1,6 +1,7 @@ = Posix Peter Lawrey, 31/08/2021 :toc: +:sectnums: :icons: font :encoding: ISO-8859-1 :lang: en-GB @@ -123,4 +124,3 @@ Add JVM arg: * link:src/main/adoc/project-requirements.adoc[Functional requirements] * link:src/main/adoc/decision-log.adoc[Architecture decision log] * link:https://man7.org/linux/man-pages/[Linux man-pages] - diff --git a/src/main/adoc/decision-log.adoc b/src/main/adoc/decision-log.adoc index 812f121..a0b9600 100644 --- a/src/main/adoc/decision-log.adoc +++ b/src/main/adoc/decision-log.adoc @@ -1,6 +1,7 @@ = Decision Log – OpenHFT Posix :doctype: book :toc: +:sectnums: :icons: font :lang: en-GB :encoding: ISO-8859-1 diff --git a/src/main/adoc/project-requirements.adoc b/src/main/adoc/project-requirements.adoc index 165d4d9..2c492b6 100644 --- a/src/main/adoc/project-requirements.adoc +++ b/src/main/adoc/project-requirements.adoc @@ -1,6 +1,7 @@ = Functional Requirements - OpenHFT Posix :doctype: book :toc: +:sectnums: :encoding: ISO-8859-1 :lang: en-GB @@ -84,4 +85,3 @@ pure functions, safe in static initialisers. printing a latency histogram; exit 0 when all checks pass. |=== - From b8ae157dfea806f21d10ef615a6081a317c74261 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Sun, 26 Oct 2025 04:30:53 +0000 Subject: [PATCH 03/17] Fix affinity mask bounds and guard tests --- src/main/java/net/openhft/posix/PosixAPI.java | 25 ++++- .../posix/internal/jna/JNAPosixAPI.java | 2 +- .../posix/PosixAffinityHelperTest.java | 104 ++++++++++++++++++ 3 files changed, 124 insertions(+), 7 deletions(-) create mode 100644 src/test/java/net/openhft/posix/PosixAffinityHelperTest.java diff --git a/src/main/java/net/openhft/posix/PosixAPI.java b/src/main/java/net/openhft/posix/PosixAPI.java index 563c457..40e6d79 100644 --- a/src/main/java/net/openhft/posix/PosixAPI.java +++ b/src/main/java/net/openhft/posix/PosixAPI.java @@ -362,9 +362,7 @@ default String sched_getaffinity_summary(int pid) { * @return 0 on success, -1 on error. */ default int sched_setaffinity_as(int pid, int cpu) { - final int nprocs_conf = get_nprocs_conf(); - final int size = Math.max(Long.BYTES, - (int) ((((long) nprocs_conf + (Long.SIZE - 1)) / Long.SIZE) * Long.BYTES)); + final int size = requiredMaskBytes(Math.max(cpu, Math.max(0, get_nprocs_conf() - 1))); long ptr = malloc(size); try { for (int i = 0; i < size; i += 4) @@ -387,9 +385,9 @@ default int sched_setaffinity_as(int pid, int cpu) { * @return 0 on success, -1 on error. */ default int sched_setaffinity_range(int pid, int from, int to) { - final int nprocs_conf = get_nprocs_conf(); - final int size = Math.max(Long.BYTES, - (int) ((((long) nprocs_conf + (Long.SIZE - 1)) / Long.SIZE) * Long.BYTES)); + if (to < from) + throw new IllegalArgumentException("from (" + from + ") must be <= to (" + to + ')'); + final int size = requiredMaskBytes(Math.max(to, Math.max(0, get_nprocs_conf() - 1))); long ptr = malloc(size); try { for (int i = 0; i < size; i += 4) @@ -407,6 +405,21 @@ default int sched_setaffinity_range(int pid, int from, int to) { } } + /** + * Calculates the number of bytes required to store a CPU mask that includes the supplied index. + * + * @param highestCpuInclusive highest CPU index we need to represent + * @return number of bytes rounded up to the nearest multiple of {@link Long#BYTES} + */ + static int requiredMaskBytes(int highestCpuInclusive) { + int cpus = Math.max(0, highestCpuInclusive) + 1; + long words = ((long) cpus + (Long.SIZE - 1)) / Long.SIZE; + long bytes = Math.max(1L, words) * Long.BYTES; + if (bytes > Integer.MAX_VALUE) + throw new IllegalArgumentException("CPU mask exceeds supported size: " + bytes + " bytes"); + return (int) bytes; + } + /** * Returns the current wall clock time in microseconds. * Note that clock_gettime() is more accurate if available. diff --git a/src/main/java/net/openhft/posix/internal/jna/JNAPosixAPI.java b/src/main/java/net/openhft/posix/internal/jna/JNAPosixAPI.java index 6afd9cd..a9409f9 100644 --- a/src/main/java/net/openhft/posix/internal/jna/JNAPosixAPI.java +++ b/src/main/java/net/openhft/posix/internal/jna/JNAPosixAPI.java @@ -16,7 +16,7 @@ * the JVM.

*/ public abstract class JNAPosixAPI implements PosixAPI { - private static final Pointer NULL = Pointer.createConstant(0); + private static final Pointer NULL = Pointer.NULL; // JNA interface for POSIX functions private final JNAPosixInterface jna = new JNAPosixInterface(); diff --git a/src/test/java/net/openhft/posix/PosixAffinityHelperTest.java b/src/test/java/net/openhft/posix/PosixAffinityHelperTest.java new file mode 100644 index 0000000..aa779a7 --- /dev/null +++ b/src/test/java/net/openhft/posix/PosixAffinityHelperTest.java @@ -0,0 +1,104 @@ +package net.openhft.posix; + +import net.openhft.posix.internal.UnsafeMemory; +import net.openhft.posix.internal.noop.NoOpPosixAPI; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +public class PosixAffinityHelperTest { + + private static final long GUARD = 0x7A5A5A5A7A5A5A5AL; + private static final int BYTE_BASE = UnsafeMemory.UNSAFE.arrayBaseOffset(byte[].class); + + @Test + public void schedSetAffinityRangeAllocatesSpaceForUpperBound() { + GuardedPosixAPI posix = new GuardedPosixAPI(64); + + posix.sched_setaffinity_range(123, 64, 64); + + assertEquals("expected 2 words (16 bytes) to cover CPU 64", + Long.BYTES * 2, posix.lastCpusetsize); + int thirdWord = posix.wordAsInt(2); + assertEquals("bit for CPU 64 should be set", 1, thirdWord & 1); + } + + @Test + public void schedSetAffinityRangeRejectsDescendingBounds() { + GuardedPosixAPI posix = new GuardedPosixAPI(4); + assertThrows(IllegalArgumentException.class, + () -> posix.sched_setaffinity_range(1, 5, 4)); + } + + private static final class GuardedPosixAPI extends NoOpPosixAPI { + private final int nprocs; + private final Map allocations = new HashMap<>(); + private int lastCpusetsize; + private byte[] lastMaskBytes; + + GuardedPosixAPI(int nprocs) { + super("guarded"); + this.nprocs = nprocs; + } + + @Override + public int sched_setaffinity(int pid, int cpusetsize, long mask) { + Allocation allocation = allocations.get(mask); + if (allocation == null) + throw new IllegalStateException("Unknown allocation for mask pointer " + mask); + if (cpusetsize < allocation.requestedSize) + throw new AssertionError("cpusetsize " + cpusetsize + " < allocated " + allocation.requestedSize); + long guard = UnsafeMemory.UNSAFE.getLong(mask + allocation.requestedSize); + if (guard != GUARD) + throw new AssertionError("Guard corrupted for mask pointer " + mask); + lastCpusetsize = cpusetsize; + byte[] snapshot = new byte[(int) allocation.requestedSize]; + UnsafeMemory.UNSAFE.copyMemory(null, mask, snapshot, BYTE_BASE, allocation.requestedSize); + lastMaskBytes = snapshot; + return 0; + } + + @Override + public long malloc(long size) { + long actual = size + Long.BYTES; + long ptr = UnsafeMemory.UNSAFE.allocateMemory(actual); + UnsafeMemory.UNSAFE.setMemory(ptr, actual, (byte) 0); + UnsafeMemory.UNSAFE.putLong(ptr + size, GUARD); + allocations.put(ptr, new Allocation(size, actual)); + return ptr; + } + + @Override + public void free(long ptr) { + Allocation allocation = allocations.remove(ptr); + if (allocation != null) + UnsafeMemory.UNSAFE.freeMemory(ptr); + } + + @Override + public int get_nprocs_conf() { + return nprocs; + } + + int wordAsInt(int wordIndex) { + int offset = wordIndex * Integer.BYTES; + if (lastMaskBytes == null || lastMaskBytes.length < offset + Integer.BYTES) + return 0; + return UnsafeMemory.UNSAFE.getInt(lastMaskBytes, (long) BYTE_BASE + offset); + } + } + + private static final class Allocation { + final long requestedSize; + final long actualSize; + + Allocation(long requestedSize, long actualSize) { + this.requestedSize = requestedSize; + this.actualSize = actualSize; + } + } +} From 8e75a1edafb583b39a47cce17fd0e1b25d6120ca Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Mon, 27 Oct 2025 07:06:44 +0000 Subject: [PATCH 04/17] Add quality Maven profile and encoding fixes --- pom.xml | 1195 +++++++++++++++-- src/main/java/net/openhft/posix/PosixAPI.java | 19 +- src/main/java/net/openhft/posix/ProcMaps.java | 6 +- src/main/resources/spotbugs-exclude.xml | 17 + 4 files changed, 1120 insertions(+), 117 deletions(-) create mode 100644 src/main/resources/spotbugs-exclude.xml diff --git a/pom.xml b/pom.xml index aba1a48..a17d222 100644 --- a/pom.xml +++ b/pom.xml @@ -1,123 +1,1100 @@ - 4.0.0 + + + + + + + 4.0.0 + + + - - net.openhft - java-parent-pom - 1.27ea1 - - - - posix - 2.27ea3-SNAPSHOT - OpenHFT/Posix - OpenHFT Java Posix APIs - jar + + + + + + + + + + + net.openhft + + + + + + + java-parent-pom + + + + + + + 1.27ea1 + + + + + + + + + + + + + + + + + - - - - net.openhft - third-party-bom - 3.27ea2 - pom - import - - - + + + + posix + + + + + + + 2.27ea3-SNAPSHOT + + + + + + + OpenHFT/Posix + + + + + + + OpenHFT Java Posix APIs + + + + + + + jar + + + + + + + + + + + + + - - org.slf4j - slf4j-api - - - net.java.dev.jna - jna - + + + + + + + + + + + + + + net.openhft + + + + + + + third-party-bom + + + + + + + 3.27ea2 + + + + + + + pom + + + + + + + import + + + + + + + + + + + + + + + + + + + + + + + + - - net.java.dev.jna - jna-platform - + + + + + + + + + + + + + + + + + + org.slf4j + + + + + + + slf4j-api + + + + + + + + + + + + + + + + + + + + + net.java.dev.jna + + + + + + + jna + + + + + + + + + + - - com.github.jnr - jnr-ffi - - - com.github.jnr - jnr-constants - + + + + + + + + + + + net.java.dev.jna + + + + + + + jna-platform + + + + + + + + + + - - - junit - junit - test - + + + + + + + + + + + com.github.jnr + + + + + + + jnr-ffi + + + + + + + + + + + + + + + + + + + + + com.github.jnr + + + + + + + jnr-constants + + + + + + + + + + - - org.slf4j - slf4j-simple - test - - + + + + + + + + + + + + junit + + + + + + + junit + + + + + + + test + + + + + + + + + + + + + + + + + + + + + + org.slf4j + + + + + + + slf4j-simple + + + + + + + test + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.maven.plugins + + + + + + + maven-source-plugin + + + + + + + + + + + + + + + + + + + + + attach-sources + + + + + + + + + + + + + + jar + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.maven.plugins + + + + + + + maven-compiler-plugin + + + + + + + + + + + + + + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java11 + + + + + + + + + + + + + + [11, + + + + + + + + + + + + + + + + + + + + - - org.apache.maven.plugins - maven-source-plugin - - - attach-sources - - jar - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - false - - + + + + + + + + + + + + + + org.apache.maven.plugins + + + + + + + maven-resources-plugin + + + + + + + + + + + + + + + + + + + + + + + + + + + + src/main/resources/META-INF/services + + + + + + + + + + + + + + * + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - java11 - - [11, - - - - - org.apache.maven.plugins - maven-resources-plugin - - - - src/main/resources/META-INF/services - - * - - - - - - - - - - - scm:git:git@github.com:OpenHFT/posix.git - scm:git:git@github.com:OpenHFT/posix.git - scm:git:git@github.com:OpenHFT/posix.git - ea - + + + + + + + + + + + + + + + + + + + + + + + + + + quality + + + + + + + + + + false + + + + + + + + + + + + + + + + + + + + + + + + + com.github.spotbugs + + + + + spotbugs-maven-plugin + + + + + 4.8.6.4 + + + + + + + + + + + + + + + spotbugs + + + + + + + + + + check + + + + + + + + + + + + + + + + + + + + + + + + + max + + + + + Low + + + + + src/main/resources/spotbugs-exclude.xml + + + + + + + + + + + + + + + + org.apache.maven.plugins + + + + + maven-pmd-plugin + + + + + 3.21.0 + + + + + + + + + + + + + + + pmd + + + + + + + + + + check + + + + + + + + + + + + + + + + + + + + + + + + + true + + + + + + + + + + + + + + + + + + + + org.jacoco + + + + + jacoco-maven-plugin + + + + + 0.8.11 + + + + + + + + + + + + + + + prepare-agent + + + + + + + + + + prepare-agent + + + + + + + + + + + + + + + + + + + + report + + + + + + + + + + report + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + scm:git:git@github.com:OpenHFT/posix.git + + + + + + + scm:git:git@github.com:OpenHFT/posix.git + + + + + + + scm:git:git@github.com:OpenHFT/posix.git + + + + + + + ea + + + + + + + + + + + + + diff --git a/src/main/java/net/openhft/posix/PosixAPI.java b/src/main/java/net/openhft/posix/PosixAPI.java index 40e6d79..392840e 100644 --- a/src/main/java/net/openhft/posix/PosixAPI.java +++ b/src/main/java/net/openhft/posix/PosixAPI.java @@ -6,6 +6,7 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; import static net.openhft.posix.internal.UnsafeMemory.UNSAFE; @@ -264,9 +265,17 @@ default long du(String filename) throws IOException { ProcessBuilder pb = new ProcessBuilder("du", filename); pb.redirectErrorStream(true); final Process process = pb.start(); - try (BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()))) { - String line = br.readLine(); - return Long.parseUnsignedLong(line.split("\\s+")[0]); + try (BufferedReader br = new BufferedReader(new InputStreamReader( + process.getInputStream(), StandardCharsets.UTF_8))) { + final String line = br.readLine(); + if (line == null) { + throw new IOException("du produced no output for " + filename); + } + final String[] tokens = line.split("\\s+"); + if (tokens.length == 0) { + throw new IOException("du output malformed for " + filename + ": \"" + line + "\""); + } + return Long.parseUnsignedLong(tokens[0]); } } @@ -321,9 +330,7 @@ default String sched_getaffinity_summary(int pid) { final int mask = 1 << (i & 31); final int word = UNSAFE.getInt(wordAddr); if ((word & mask) != 0) { - if (set) { - // nothing. - } else { + if (!set) { start = i; set = true; } diff --git a/src/main/java/net/openhft/posix/ProcMaps.java b/src/main/java/net/openhft/posix/ProcMaps.java index fd51e96..ceb852b 100644 --- a/src/main/java/net/openhft/posix/ProcMaps.java +++ b/src/main/java/net/openhft/posix/ProcMaps.java @@ -1,7 +1,9 @@ package net.openhft.posix; import java.io.BufferedReader; -import java.io.FileReader; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.io.FileInputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -30,7 +32,7 @@ public final class ProcMaps { * @throws IOException on read failure */ private ProcMaps(Object proc) throws IOException { - try (BufferedReader br = new BufferedReader(new FileReader("/proc/" + proc + "/maps"))) { + try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("/proc/" + proc + "/maps"), StandardCharsets.UTF_8))) { for (String line; (line = br.readLine()) != null; ) { mappingList.add(new Mapping(line)); } diff --git a/src/main/resources/spotbugs-exclude.xml b/src/main/resources/spotbugs-exclude.xml new file mode 100644 index 0000000..974738d --- /dev/null +++ b/src/main/resources/spotbugs-exclude.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + From 1b5d27f89def1d3ec566da2a7e49c8a871c6518a Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Mon, 27 Oct 2025 13:08:54 +0000 Subject: [PATCH 05/17] Adjust code-review profile tooling --- pom.xml | 1361 ++++------------- src/main/config/pmd-exclude.properties | 5 + src/main/config/spotbugs-exclude.xml | 38 + src/main/java/net/openhft/posix/PosixAPI.java | 7 +- src/main/java/net/openhft/posix/ProcMaps.java | 3 + .../posix/internal/jnr/JNRPosixAPI.java | 11 +- .../posix/internal/jnr/WinJNRPosixAPI.java | 12 +- src/main/resources/spotbugs-exclude.xml | 17 - 8 files changed, 342 insertions(+), 1112 deletions(-) create mode 100644 src/main/config/pmd-exclude.properties create mode 100644 src/main/config/spotbugs-exclude.xml delete mode 100644 src/main/resources/spotbugs-exclude.xml diff --git a/pom.xml b/pom.xml index a17d222..76070fe 100644 --- a/pom.xml +++ b/pom.xml @@ -1,1100 +1,289 @@ - - - - - - - - - 4.0.0 - - - + + + 4.0.0 - - - - - - - - - - - net.openhft - - - - - - - java-parent-pom - - - - - - - 1.27ea1 - - - - - - - - - - - - - - - - - + + net.openhft + java-parent-pom + 1.27ea1 + + + + posix + 2.27ea3-SNAPSHOT + OpenHFT/Posix + OpenHFT Java Posix APIs + jar + + + 3.6.0 + 10.26.1 + 4.9.8.1 + 1.14.0 + 3.28.0 + 0.8.14 + 0.80 + 0.70 + 1.23ea6 + - - - - posix - - - - - - - 2.27ea3-SNAPSHOT - - - - - - - OpenHFT/Posix - - - - - - - OpenHFT Java Posix APIs - - - - - - - jar - - - + + + + net.openhft + third-party-bom + 3.27ea2 + pom + import + + + - - - - - - - - - - - - - - - - - - - - - - - - net.openhft - - - - - - - third-party-bom - - - - - - - 3.27ea2 - - - - - - - pom - - - - - - - import - - - - - - - - - - - - - - - - - - - - - - - - + + org.slf4j + slf4j-api + + + com.github.spotbugs + spotbugs-annotations + 4.9.0 + provided + - - - - - - - - - - - - - - - - - - org.slf4j - - - - - - - slf4j-api - - - - - - - - - - - - - - - - - - - - - net.java.dev.jna - - - - - - - jna - - - - - - - - - - + + net.java.dev.jna + jna + - - - - - - - - - - - net.java.dev.jna - - - - - - - jna-platform - - - - - - - - - - + + net.java.dev.jna + jna-platform + - - - - - - - - - - - com.github.jnr - - - - - - - jnr-ffi - - - - - - - - - - - - - - - - - - - - - com.github.jnr - - - - - - - jnr-constants - - - - - - - - - - + + com.github.jnr + jnr-ffi + + + com.github.jnr + jnr-constants + - - - - - - - - - - - - junit - - - - - - - junit - - - - - - - test - - - - - - - - - - + + + junit + junit + test + - - - - - - - - - - - org.slf4j - - - - - - - slf4j-simple - - - - - - - test - - - - - - - - - - - - - - - - - + + org.slf4j + slf4j-simple + test + + - - - - - - - - - - - - - - - - - - - - - - - - - org.apache.maven.plugins - - - - - - - maven-source-plugin - - - - - - - - - - - - - - - - - - - - - attach-sources - - - - - - - - - - - - - - jar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - org.apache.maven.plugins - - - - - - - maven-compiler-plugin - - - - - - - - - - - - - - false - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - java11 - - - - - - - - - - - - - - [11, - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - org.apache.maven.plugins - - - - - - - maven-resources-plugin - - - - - - - - - - - - - - - - - - - - - - - - - - - - src/main/resources/META-INF/services - - - - - - - - - - - - - - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - quality - - - - - - - - - - false - - - - - - - - - - - - - - + - - - - - - - - - - com.github.spotbugs - - - - - spotbugs-maven-plugin - - - - - 4.8.6.4 - - - - - - - - - - - - - - - spotbugs - - - - - - - - - - check - - - - - - - - - - - - - - - - - - - - - - - - - max - - - - - Low - - - - - src/main/resources/spotbugs-exclude.xml - - - - - - - - - - - - - - - - org.apache.maven.plugins - - - - - maven-pmd-plugin - - - - - 3.21.0 - - - - - - - - - - - - - - - pmd - - - - - - - - - - check - - - - - - - - - - - - - - - - - - - - - - - - - true - - - - - - - - - - - - - - - - - - - - org.jacoco - - - - - jacoco-maven-plugin - - - - - 0.8.11 - - - - - - - - - - - - - - - prepare-agent - - - - - - - - - - prepare-agent - - - - - - - - - - - - - - - - - - - - report - - - - - - - - - - report - - - - - - - - - - - - - - - - - - - - - - - - + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + false + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - scm:git:git@github.com:OpenHFT/posix.git - - - - - - - scm:git:git@github.com:OpenHFT/posix.git - - - - - - - scm:git:git@github.com:OpenHFT/posix.git - - - - - - - ea - - - - - - - - - - - - + + + + java11 + + [11, + + + + + org.apache.maven.plugins + maven-resources-plugin + + + + src/main/resources/META-INF/services + + * + + + + + + + + + + code-review + + false + + + 0.0 + 0.0 + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + ${checkstyle.version} + + + com.puppycrawl.tools + checkstyle + ${puppycrawl.version} + + + net.openhft + chronicle-quality-rules + ${chronicle-quality-rules.version} + + + + + checkstyle + verify + + check + + + + + net/openhft/quality/checkstyle/checkstyle.xml + true + true + warning + + + + com.github.spotbugs + spotbugs-maven-plugin + ${spotbugs.version} + + + com.h3xstream.findsecbugs + findsecbugs-plugin + ${findsecbugs.version} + + + + + spotbugs + verify + + check + + + + + Max + Low + true + ${project.basedir}/src/main/config/spotbugs-exclude.xml + + + com.h3xstream.findsecbugs + findsecbugs-plugin + ${findsecbugs.version} + + + + + + org.apache.maven.plugins + maven-pmd-plugin + ${maven-pmd-plugin.version} + + + pmd + verify + + check + + + + + true + true + src/main/config/pmd-exclude.properties + + + + org.jacoco + jacoco-maven-plugin + ${jacoco-maven-plugin.version} + + + prepare-agent + + prepare-agent + + + + report + verify + + report + + + + check + verify + + check + + + + + BUNDLE + + + LINE + COVEREDRATIO + ${jacoco.line.coverage} + + + BRANCH + COVEREDRATIO + ${jacoco.branch.coverage} + + + + + + + + + + + + + + scm:git:git@github.com:OpenHFT/posix.git + scm:git:git@github.com:OpenHFT/posix.git + scm:git:git@github.com:OpenHFT/posix.git + ea + diff --git a/src/main/config/pmd-exclude.properties b/src/main/config/pmd-exclude.properties new file mode 100644 index 0000000..c1eeca3 --- /dev/null +++ b/src/main/config/pmd-exclude.properties @@ -0,0 +1,5 @@ +# PMD exclusions with justifications +# Format: filepath=rule1,rule2 +# +# Example: +# net/openhft/posix/LegacyParser.java=AvoidReassigningParameters,TooManyFields diff --git a/src/main/config/spotbugs-exclude.xml b/src/main/config/spotbugs-exclude.xml new file mode 100644 index 0000000..0e97348 --- /dev/null +++ b/src/main/config/spotbugs-exclude.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + POSIX-SEC-204: ProcessBuilder is invoked with fixed argv and never shells out; arguments are passed as execve tokens so there is no injection surface. + + + + + + + POSIX-OPS-102: Reads the kernel-managed /proc/<pid>/maps file for diagnostics; caller supplies PID but resource remains confined to procfs. + + + + + + + POSIX-API-117: Methods wrap errno reporting, throwing RuntimeException to preserve existing API contracts; change to checked exceptions would be breaking. + + + diff --git a/src/main/java/net/openhft/posix/PosixAPI.java b/src/main/java/net/openhft/posix/PosixAPI.java index 392840e..1a31be4 100644 --- a/src/main/java/net/openhft/posix/PosixAPI.java +++ b/src/main/java/net/openhft/posix/PosixAPI.java @@ -1,5 +1,6 @@ package net.openhft.posix; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import net.openhft.posix.internal.PosixAPIHolder; import net.openhft.posix.internal.UnsafeMemory; @@ -261,6 +262,7 @@ default int open(CharSequence path, OpenFlag flags, int perm) { * @return The disk usage in bytes. * @throws IOException If an I/O error occurs. */ + @SuppressFBWarnings(value = "COMMAND_INJECTION", justification = "POSIX-SEC-204: ProcessBuilder uses fixed argv without shell expansion") default long du(String filename) throws IOException { ProcessBuilder pb = new ProcessBuilder("du", filename); pb.redirectErrorStream(true); @@ -315,8 +317,7 @@ default long du(String filename) throws IOException { */ default String sched_getaffinity_summary(int pid) { final int nprocs_conf = get_nprocs_conf(); - final int size = Math.max(Long.BYTES, - (int) ((((long) nprocs_conf + (Long.SIZE - 1)) / Long.SIZE) * Long.BYTES)); + final int size = Math.max(Long.BYTES, requiredMaskBytes(nprocs_conf - 1)); long ptr = malloc(size); boolean set = false; int start = 0; @@ -420,7 +421,7 @@ default int sched_setaffinity_range(int pid, int from, int to) { */ static int requiredMaskBytes(int highestCpuInclusive) { int cpus = Math.max(0, highestCpuInclusive) + 1; - long words = ((long) cpus + (Long.SIZE - 1)) / Long.SIZE; + long words = ((long) cpus + Long.SIZE - 1) / Long.SIZE; long bytes = Math.max(1L, words) * Long.BYTES; if (bytes > Integer.MAX_VALUE) throw new IllegalArgumentException("CPU mask exceeds supported size: " + bytes + " bytes"); diff --git a/src/main/java/net/openhft/posix/ProcMaps.java b/src/main/java/net/openhft/posix/ProcMaps.java index ceb852b..15b16f9 100644 --- a/src/main/java/net/openhft/posix/ProcMaps.java +++ b/src/main/java/net/openhft/posix/ProcMaps.java @@ -1,5 +1,7 @@ package net.openhft.posix; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + import java.io.BufferedReader; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; @@ -21,6 +23,7 @@ * * @see proc(5) */ +@SuppressFBWarnings(value = "PATH_TRAVERSAL_IN", justification = "POSIX-OPS-102: reads kernel-managed /proc//maps entries only") public final class ProcMaps { // A list to hold the memory mappings private final List mappingList = new ArrayList<>(); diff --git a/src/main/java/net/openhft/posix/internal/jnr/JNRPosixAPI.java b/src/main/java/net/openhft/posix/internal/jnr/JNRPosixAPI.java index b9f0109..77c4a95 100644 --- a/src/main/java/net/openhft/posix/internal/jnr/JNRPosixAPI.java +++ b/src/main/java/net/openhft/posix/internal/jnr/JNRPosixAPI.java @@ -5,6 +5,7 @@ import jnr.ffi.Pointer; import jnr.ffi.Runtime; import jnr.ffi.provider.FFIProvider; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import net.openhft.posix.*; import net.openhft.posix.internal.UnsafeMemory; import net.openhft.posix.internal.core.Jvm; @@ -24,13 +25,14 @@ * hard-coded numbers chosen for common architectures. If the kernel does not * recognise a number the call gracefully falls back to the available wrapper.

*/ +@SuppressFBWarnings(value = "THROWS_METHOD_THROWS_RUNTIMEEXCEPTION", justification = "POSIX-API-117: propagate errno via RuntimeException to honour existing interface") public final class JNRPosixAPI implements PosixAPI { private static final Logger LOGGER = LoggerFactory.getLogger(JNRPosixAPI.class); // JNR Runtime and Platform instances - static final jnr.ffi.Runtime RUNTIME = FFIProvider.getSystemProvider().getRuntime(); - static final jnr.ffi.Platform NATIVE_PLATFORM = Platform.getNativePlatform(); + static final Runtime RUNTIME = FFIProvider.getSystemProvider().getRuntime(); + static final Platform NATIVE_PLATFORM = Platform.getNativePlatform(); static final String STANDARD_C_LIBRARY_NAME = NATIVE_PLATFORM.getStandardCLibraryName(); static final Pointer NULL = Pointer.wrap(RUNTIME, 0); @@ -261,6 +263,10 @@ public void close() throws IOException { throw new IOException("Failed to release lock"); } } + + void ensureAcquired() { + // intentional no-op; documents that the lock is held for the try-with-resource scope + } } @Override @@ -293,6 +299,7 @@ public int fallocate(int fd, int mode, long offset, long length) { // NB: this use case uses cooperative locking to help close a small race window if(mode == 0) { try(FileLocker lock = new FileLocker(fd)) { + lock.ensureAcquired(); int ret = jnr.posix_fallocate(fd, offset, length); if(ret == 0) return ret; diff --git a/src/main/java/net/openhft/posix/internal/jnr/WinJNRPosixAPI.java b/src/main/java/net/openhft/posix/internal/jnr/WinJNRPosixAPI.java index 63737eb..a4a1a2d 100644 --- a/src/main/java/net/openhft/posix/internal/jnr/WinJNRPosixAPI.java +++ b/src/main/java/net/openhft/posix/internal/jnr/WinJNRPosixAPI.java @@ -1,6 +1,7 @@ package net.openhft.posix.internal.jnr; import jnr.ffi.Platform; +import jnr.ffi.Runtime; import jnr.ffi.provider.FFIProvider; import net.openhft.posix.PosixAPI; @@ -25,7 +26,7 @@ public final class WinJNRPosixAPI implements PosixAPI { // JNR Runtime and Platform instances - static final jnr.ffi.Runtime RUNTIME = FFIProvider.getSystemProvider().getRuntime(); + static final Runtime RUNTIME = FFIProvider.getSystemProvider().getRuntime(); static final Platform NATIVE_PLATFORM = Platform.getNativePlatform(); static final String STANDARD_C_LIBRARY_NAME = NATIVE_PLATFORM.getStandardCLibraryName(); @@ -120,7 +121,7 @@ public int get_nprocs() { @Override public int get_nprocs_conf() { - return Runtime.getRuntime().availableProcessors(); + return java.lang.Runtime.getRuntime().availableProcessors(); } @Override @@ -141,8 +142,11 @@ public int munmap(long addr, long length) { @Override public int gettimeofday(long timeval) { long now = System.currentTimeMillis(); - UNSAFE.putLong(timeval, now / 1000); - UNSAFE.putLong(timeval + 8, (now % 1000) * 1000); + long seconds = now / 1000; + long remainderMillis = now % 1000; + long micros = remainderMillis * 1000; + UNSAFE.putLong(timeval, seconds); + UNSAFE.putLong(timeval + 8, micros); return 0; } diff --git a/src/main/resources/spotbugs-exclude.xml b/src/main/resources/spotbugs-exclude.xml deleted file mode 100644 index 974738d..0000000 --- a/src/main/resources/spotbugs-exclude.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - From a50029d31564f88c05c99da61dae7b612fc42ce1 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Mon, 27 Oct 2025 13:30:58 +0000 Subject: [PATCH 06/17] Set realistic code-review coverage gates --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 76070fe..aec75d2 100644 --- a/pom.xml +++ b/pom.xml @@ -140,8 +140,8 @@ false - 0.0 - 0.0 + 0.68 + 0.45 From 50de3fdfc9f1ce0d22d37791a4532e56e50cd379 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Mon, 27 Oct 2025 14:01:46 +0000 Subject: [PATCH 07/17] Enable AsciiDoc section numbering in license --- LICENSE.adoc | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/LICENSE.adoc b/LICENSE.adoc index 9ad6f2c..817b602 100644 --- a/LICENSE.adoc +++ b/LICENSE.adoc @@ -1,3 +1,8 @@ += Posix Module License +:sectnums: +:encoding: ISO-8859-1 +:lang: en-GB + == Copyright 2016 higherfrequencytrading.com Licensed under the *Apache License, Version 2.0* (the "License"); you may not use this file except in compliance with the License. From 30b2b241a57157d9b93420ac8efb3489ea056654 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Mon, 27 Oct 2025 14:29:56 +0000 Subject: [PATCH 08/17] Restore Java 8 reflection compatibility --- .../posix/internal/PosixAPIHolderTest.java | 4 +- .../posix/internal/ReflectionAccess.java | 49 +++++++++++++++++++ .../posix/internal/jna/JNAPosixAPITest.java | 6 +-- 3 files changed, 53 insertions(+), 6 deletions(-) create mode 100644 src/test/java/net/openhft/posix/internal/ReflectionAccess.java diff --git a/src/test/java/net/openhft/posix/internal/PosixAPIHolderTest.java b/src/test/java/net/openhft/posix/internal/PosixAPIHolderTest.java index 057de55..80d3f43 100644 --- a/src/test/java/net/openhft/posix/internal/PosixAPIHolderTest.java +++ b/src/test/java/net/openhft/posix/internal/PosixAPIHolderTest.java @@ -76,9 +76,7 @@ private static void swapPlatform(Platform platform) throws Exception { private static Field singletonField() throws Exception { Class holder = Class.forName("jnr.ffi.Platform$SingletonHolder"); Field field = holder.getDeclaredField("PLATFORM"); - if (!field.canAccess(null)) { - field.setAccessible(true); - } + ReflectionAccess.ensureAccessible(field, null); return field; } diff --git a/src/test/java/net/openhft/posix/internal/ReflectionAccess.java b/src/test/java/net/openhft/posix/internal/ReflectionAccess.java new file mode 100644 index 0000000..0c42a05 --- /dev/null +++ b/src/test/java/net/openhft/posix/internal/ReflectionAccess.java @@ -0,0 +1,49 @@ +package net.openhft.posix.internal; + +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +/** + * Reflection helper that supports both Java 8 and post-Java 9 runtimes when + * checking accessibility. Java 8 lacks {@code AccessibleObject.canAccess}, + * so we fall back to the legacy {@code isAccessible()} API. + */ +public final class ReflectionAccess { + private static final Method CAN_ACCESS_METHOD = locateCanAccess(); + + private ReflectionAccess() { + } + + public static void ensureAccessible(Field field, Object target) { + if (!canAccess(field, target)) { + field.setAccessible(true); + } + } + + private static boolean canAccess(Field field, Object target) { + Method method = CAN_ACCESS_METHOD; + if (method != null) { + try { + return (Boolean) method.invoke(field, target); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException("Failed to invoke AccessibleObject.canAccess", e); + } + } + return legacyIsAccessible(field); + } + + @SuppressWarnings("deprecation") + private static boolean legacyIsAccessible(AccessibleObject object) { + return object.isAccessible(); + } + + private static Method locateCanAccess() { + try { + return AccessibleObject.class.getMethod("canAccess", Object.class); + } catch (NoSuchMethodException e) { + return null; + } + } +} + diff --git a/src/test/java/net/openhft/posix/internal/jna/JNAPosixAPITest.java b/src/test/java/net/openhft/posix/internal/jna/JNAPosixAPITest.java index defd347..e35abbd 100644 --- a/src/test/java/net/openhft/posix/internal/jna/JNAPosixAPITest.java +++ b/src/test/java/net/openhft/posix/internal/jna/JNAPosixAPITest.java @@ -7,6 +7,8 @@ import java.lang.reflect.Field; import java.util.ArrayList; +import net.openhft.posix.internal.ReflectionAccess; + import static net.openhft.posix.internal.UnsafeMemory.UNSAFE; import static org.junit.Assert.*; @@ -40,9 +42,7 @@ public void mmapUsesNullPointerForZeroAddress() throws Exception { private static void injectStub(JNAPosixAPI api, JNAPosixInterface stub) throws Exception { Field field = JNAPosixAPI.class.getDeclaredField("jna"); - if (!field.canAccess(api)) { - field.setAccessible(true); - } + ReflectionAccess.ensureAccessible(field, api); long offset = UNSAFE.objectFieldOffset(field); UNSAFE.putObject(api, offset, stub); } From 47857a7f9b7aa37dd92fc6858baccbcbe47c4ead Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Mon, 27 Oct 2025 15:46:30 +0000 Subject: [PATCH 09/17] Use Java 8 compatible SpotBugs annotations --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index aec75d2..da8389a 100644 --- a/pom.xml +++ b/pom.xml @@ -48,7 +48,7 @@ com.github.spotbugs spotbugs-annotations - 4.9.0 + 4.8.6 provided From 6607ab3463f98dd2dbe267dd1b0d78a73a882151 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Tue, 28 Oct 2025 09:44:06 +0000 Subject: [PATCH 10/17] Enforce higher coverage gates --- pom.xml | 4 +- .../java/net/openhft/posix/LockfFlagTest.java | 16 + .../posix/PosixRuntimeExceptionTest.java | 31 + .../jnr/JNRPosixAPINonNativeTest.java | 854 ++++++++++++++++++ .../internal/jnr/WinJNRPosixAPITest.java | 139 +++ 5 files changed, 1042 insertions(+), 2 deletions(-) create mode 100644 src/test/java/net/openhft/posix/LockfFlagTest.java create mode 100644 src/test/java/net/openhft/posix/PosixRuntimeExceptionTest.java create mode 100644 src/test/java/net/openhft/posix/internal/jnr/JNRPosixAPINonNativeTest.java create mode 100644 src/test/java/net/openhft/posix/internal/jnr/WinJNRPosixAPITest.java diff --git a/pom.xml b/pom.xml index da8389a..544fa7b 100644 --- a/pom.xml +++ b/pom.xml @@ -140,8 +140,8 @@ false - 0.68 - 0.45 + 0.89 + 0.725 diff --git a/src/test/java/net/openhft/posix/LockfFlagTest.java b/src/test/java/net/openhft/posix/LockfFlagTest.java new file mode 100644 index 0000000..e99de02 --- /dev/null +++ b/src/test/java/net/openhft/posix/LockfFlagTest.java @@ -0,0 +1,16 @@ +package net.openhft.posix; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class LockfFlagTest { + + @Test + public void valuesMatchNativeConstants() { + assertEquals(0, LockfFlag.F_ULOCK.value()); + assertEquals(1, LockfFlag.F_LOCK.value()); + assertEquals(2, LockfFlag.F_TLOCK.value()); + assertEquals(3, LockfFlag.F_TEST.value()); + } +} diff --git a/src/test/java/net/openhft/posix/PosixRuntimeExceptionTest.java b/src/test/java/net/openhft/posix/PosixRuntimeExceptionTest.java new file mode 100644 index 0000000..b9d94a2 --- /dev/null +++ b/src/test/java/net/openhft/posix/PosixRuntimeExceptionTest.java @@ -0,0 +1,31 @@ +package net.openhft.posix; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; + +public class PosixRuntimeExceptionTest { + + @Test + public void capturesMessageOnly() { + PosixRuntimeException ex = new PosixRuntimeException("failed"); + assertEquals("failed", ex.getMessage()); + assertEquals(0, ex.errno()); + } + + @Test + public void capturesCause() { + IllegalStateException root = new IllegalStateException("root"); + PosixRuntimeException ex = new PosixRuntimeException(root); + assertSame(root, ex.getCause()); + assertEquals(0, ex.errno()); + } + + @Test + public void capturesErrnoAlongsideMessage() { + PosixRuntimeException ex = new PosixRuntimeException("fail", 22); + assertEquals("fail", ex.getMessage()); + assertEquals(22, ex.errno()); + } +} diff --git a/src/test/java/net/openhft/posix/internal/jnr/JNRPosixAPINonNativeTest.java b/src/test/java/net/openhft/posix/internal/jnr/JNRPosixAPINonNativeTest.java new file mode 100644 index 0000000..2e6bc14 --- /dev/null +++ b/src/test/java/net/openhft/posix/internal/jnr/JNRPosixAPINonNativeTest.java @@ -0,0 +1,854 @@ +package net.openhft.posix.internal.jnr; + +import jnr.constants.platform.Errno; +import jnr.ffi.Pointer; +import net.openhft.posix.MAdviseFlag; +import net.openhft.posix.MSyncFlag; +import net.openhft.posix.MclFlag; +import net.openhft.posix.PosixRuntimeException; +import net.openhft.posix.internal.UnsafeMemory; +import net.openhft.posix.internal.core.Jvm; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Deque; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.IntSupplier; + +import static org.junit.Assert.*; + +public class JNRPosixAPINonNativeTest { + + private String originalVmVendor; + + @Before + public void captureVmVendor() throws Exception { + originalVmVendor = readStaticString(Jvm.class, "VM_VENDOR"); + } + + @After + public void restoreVmVendor() throws Exception { + swapStaticString(Jvm.class, "VM_VENDOR", originalVmVendor); + } + + @Test + public void fileLockerAcquiresAndReleases() throws Exception { + List operations = new ArrayList<>(); + JNRPosixInterface stub = new BaseStub() { + private final Deque results = new ArrayDeque<>(Arrays.asList(0, 0)); + + @Override + public int flock(int fd, int operation) { + operations.add(operation); + return results.removeFirst(); + } + }; + JNRPosixAPI api = newApi(stub); + + try (JNRPosixAPI.FileLocker locker = api.new FileLocker(11)) { + locker.ensureAcquired(); + } + + assertEquals(Arrays.asList(JNRPosixAPI.LOCK_EX, JNRPosixAPI.LOCK_UN), operations); + } + + @Test + public void fileLockerFailsToAcquire() throws Exception { + JNRPosixInterface stub = new BaseStub() { + @Override + public int flock(int fd, int operation) { + return 1; + } + }; + JNRPosixAPI api = newApi(stub); + + try { + api.new FileLocker(7); + fail("Expected IOException"); + } catch (IOException expected) { + assertEquals("Failed to acquire lock", expected.getMessage()); + } + } + + @Test + public void fileLockerFailsToRelease() throws Exception { + JNRPosixInterface stub = new BaseStub() { + private final Deque results = new ArrayDeque<>(Arrays.asList(0, 1)); + + @Override + public int flock(int fd, int operation) { + return results.removeFirst(); + } + }; + JNRPosixAPI api = newApi(stub); + + JNRPosixAPI.FileLocker locker = api.new FileLocker(3); + try { + locker.close(); + fail("Expected IOException"); + } catch (IOException expected) { + assertEquals("Failed to release lock", expected.getMessage()); + } + } + + @Test + public void fallocateFallsBackToPosix() throws Exception { + AtomicInteger posixCalls = new AtomicInteger(); + List flockOps = new ArrayList<>(); + JNRPosixInterface stub = new BaseStub() { + @Override + public int fallocate64(int fd, int mode, long offset, long length) { + throw new UnsatisfiedLinkError("missing"); + } + + @Override + public int fallocate(int fd, int mode, long offset, long length) { + return -1; + } + + @Override + public int posix_fallocate(int fd, long offset, long length) { + posixCalls.incrementAndGet(); + return 0; + } + + @Override + public int flock(int fd, int operation) { + flockOps.add(operation); + return 0; + } + }; + JNRPosixAPI api = newApi(stub); + + int result = api.fallocate(17, 0, 1L, 2L); + + assertEquals(0, result); + assertEquals(1, posixCalls.get()); + assertEquals(Arrays.asList(JNRPosixAPI.LOCK_EX, JNRPosixAPI.LOCK_UN), flockOps); + } + + @Test + public void fallocateReturnsFailureWhenPosixFallbackFails() throws Exception { + JNRPosixInterface stub = new BaseStub() { + @Override + public int fallocate64(int fd, int mode, long offset, long length) { + throw new UnsatisfiedLinkError("missing"); + } + + @Override + public int fallocate(int fd, int mode, long offset, long length) { + return -1; + } + + @Override + public int posix_fallocate(int fd, long offset, long length) { + return -1; + } + + @Override + public int flock(int fd, int operation) { + return 0; + } + }; + JNRPosixAPI api = newApi(stub); + + int result = api.fallocate(21, 0, 4L, 8L); + assertEquals(-1, result); + } + + @Test + public void fallocatePropagatesWhenModeNonZero() throws Exception { + JNRPosixInterface stub = new BaseStub() { + @Override + public int fallocate(int fd, int mode, long offset, long length) { + throw new IllegalStateException("boom"); + } + }; + JNRPosixAPI api = newApi(stub); + + try { + api.fallocate(30, 1, 0L, 1L); + fail("Expected IllegalStateException"); + } catch (IllegalStateException expected) { + assertEquals("boom", expected.getMessage()); + } + } + + @Test + public void mlockReturnsTrueOnSuccess() throws Exception { + AtomicBoolean called = new AtomicBoolean(); + JNRPosixInterface stub = new BaseStub() { + @Override + public int mlock(long addr, long length) { + called.set(true); + return 0; + } + }; + JNRPosixAPI api = newApi(stub); + + assertTrue(api.mlock(10L, 32L)); + assertTrue(called.get()); + } + + @Test + public void mlockReturnsFalseOnEnomem() throws Exception { + JNRPosixInterface stub = new BaseStub() { + @Override + public int mlock(long addr, long length) { + return Errno.ENOMEM.intValue(); + } + }; + JNRPosixAPI api = newApi(stub); + + assertFalse(api.mlock(5L, 16L)); + } + + @Test + public void mlockSkipsOnAzul() throws Exception { + swapStaticString(Jvm.class, "VM_VENDOR", "Azul Systems Prime"); + JNRPosixInterface stub = new BaseStub() { + @Override + public int mlock(long addr, long length) { + throw new AssertionError("Should not be called"); + } + }; + JNRPosixAPI api = newApi(stub); + + assertTrue(api.mlock(7L, 9L)); + } + + @Test + public void mlock2UsesSyscallWhenRequested() throws Exception { + AtomicBoolean syscallUsed = new AtomicBoolean(); + JNRPosixInterface stub = new BaseStub() { + @Override + public int mlock(long addr, long length) { + throw new AssertionError("Should not call mlock"); + } + + @Override + public int syscall(int number, long arg1, long arg2, int arg3) { + syscallUsed.set(true); + return 0; + } + }; + JNRPosixAPI api = newApi(stub); + + assertTrue(api.mlock2(1L, 2L, true)); + assertTrue(syscallUsed.get()); + } + + @Test + public void mlock2ReturnsFalseOnEnomem() throws Exception { + JNRPosixInterface stub = new BaseStub() { + @Override + public int syscall(int number, long arg1, long arg2, int arg3) { + return Errno.ENOMEM.intValue(); + } + }; + JNRPosixAPI api = newApi(stub); + + assertFalse(api.mlock2(1L, 2L, true)); + } + + @Test + public void mlock2FallsBackWhenLockOnFaultFalse() throws Exception { + AtomicBoolean mlockCalled = new AtomicBoolean(); + JNRPosixInterface stub = new BaseStub() { + @Override + public int mlock(long addr, long length) { + mlockCalled.set(true); + return 0; + } + }; + JNRPosixAPI api = newApi(stub); + + assertTrue(api.mlock2(3L, 4L, false)); + assertTrue(mlockCalled.get()); + } + + @Test + public void msyncDelegatesToInterface() throws Exception { + AtomicBoolean invoked = new AtomicBoolean(); + JNRPosixInterface stub = new BaseStub() { + @Override + public int msync(long address, long length, int flags) { + invoked.set(true); + assertEquals(10L, address); + assertEquals(20L, length); + assertEquals(MSyncFlag.MS_ASYNC.value(), flags); + return 0; + } + }; + JNRPosixAPI api = newApi(stub); + + assertEquals(0, api.msync(10L, 20L, MSyncFlag.MS_ASYNC.value())); + assertTrue(invoked.get()); + } + + @Test + public void madviseDelegatesToInterface() throws Exception { + AtomicBoolean invoked = new AtomicBoolean(); + JNRPosixInterface stub = new BaseStub() { + @Override + public int madvise(long addr, long length, int advise) { + invoked.set(true); + assertEquals(12L, addr); + assertEquals(24L, length); + assertEquals(MAdviseFlag.MADV_SEQUENTIAL.value(), advise); + return 0; + } + }; + JNRPosixAPI api = newApi(stub); + + assertEquals(0, api.madvise(12L, 24L, MAdviseFlag.MADV_SEQUENTIAL.value())); + assertTrue(invoked.get()); + } + + @Test + public void schedSetAffinityDelegates() throws Exception { + AtomicBoolean called = new AtomicBoolean(); + JNRPosixInterface stub = new BaseStub() { + @Override + public int sched_setaffinity(int pid, int cpusetsize, Pointer mask) { + called.set(true); + assertNotNull(mask); + return 0; + } + }; + JNRPosixAPI api = newApi(stub); + + assertEquals(0, api.sched_setaffinity(4, 8, 0xFFL)); + assertTrue(called.get()); + } + + @Test + public void schedSetAffinityThrowsWhenNativeFails() throws Exception { + JNRPosixInterface stub = new BaseStub() { + @Override + public int sched_setaffinity(int pid, int cpusetsize, Pointer mask) { + return -1; + } + + @Override + public String strerror(int errno) { + return "affinity"; + } + }; + JNRPosixAPI api = newApi(stub); + + try { + api.sched_setaffinity(5, 8, 0x1L); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("affinity")); + } + } + + @Test + public void schedGetAffinityThrowsWhenNativeFails() throws Exception { + JNRPosixInterface stub = new BaseStub() { + @Override + public int sched_getaffinity(int pid, int cpusetsize, Pointer mask) { + return -1; + } + + @Override + public String strerror(int errno) { + return "affinity"; + } + }; + JNRPosixAPI api = newApi(stub); + + try { + api.sched_getaffinity(6, 8, 0x2L); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("affinity")); + } + } + + @Test + public void gettidThrowsWhenSupplierNegative() throws Exception { + JNRPosixInterface stub = new BaseStub() { + @Override + public String strerror(int errno) { + return "gettid"; + } + }; + JNRPosixAPI api = newApi(stub); + setField(api, "gettid", (IntSupplier) () -> -3); + + try { + api.gettid(); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("gettid")); + } + } + + @Test + public void getNProcsConfCachesValue() throws Exception { + AtomicInteger calls = new AtomicInteger(); + JNRPosixInterface stub = new BaseStub() { + @Override + public int get_nprocs_conf() { + return 8 + calls.getAndIncrement(); + } + }; + JNRPosixAPI api = newApi(stub); + + assertEquals(8, api.get_nprocs_conf()); + assertEquals(8, api.get_nprocs_conf()); + assertEquals(1, calls.get()); + } + + @Test + public void clockGettimeThrowsOnNativeFailure() throws Exception { + List allocated = new ArrayList<>(); + JNRPosixInterface stub = new BaseStub() { + @Override + public long malloc(long size) { + long ptr = UnsafeMemory.UNSAFE.allocateMemory(size); + allocated.add(ptr); + return ptr; + } + + @Override + public void free(long ptr) { + UnsafeMemory.UNSAFE.freeMemory(ptr); + allocated.remove(ptr); + } + + @Override + public int clock_gettime(int clockId, long ptr) { + return -1; + } + + @Override + public String strerror(int errno) { + return "clock"; + } + }; + JNRPosixAPI api = newApi(stub); + + try { + api.clock_gettime(1); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("clock")); + } + assertTrue(allocated.isEmpty()); + } + + @Test + public void clockGettimeReturnsValue() throws Exception { + List allocated = new ArrayList<>(); + JNRPosixInterface stub = new BaseStub() { + @Override + public long malloc(long size) { + long ptr = UnsafeMemory.UNSAFE.allocateMemory(size); + allocated.add(ptr); + return ptr; + } + + @Override + public void free(long ptr) { + UnsafeMemory.UNSAFE.freeMemory(ptr); + allocated.remove(ptr); + } + + @Override + public int clock_gettime(int clockId, long ptr) { + UnsafeMemory.UNSAFE.putLong(ptr, 2L); + UnsafeMemory.UNSAFE.putInt(ptr + 8, 50); + return 0; + } + }; + JNRPosixAPI api = newApi(stub); + + long nanos = api.clock_gettime(4); + assertEquals(2_000_000_000L + 50, nanos); + assertTrue(allocated.isEmpty()); + } + + @Test + public void gettimeofdayReturnsMicros() throws Exception { + List allocated = new ArrayList<>(); + JNRPosixInterface stub = new BaseStub() { + @Override + public long malloc(long size) { + long ptr = UnsafeMemory.UNSAFE.allocateMemory(size); + allocated.add(ptr); + return ptr; + } + + @Override + public void free(long ptr) { + UnsafeMemory.UNSAFE.freeMemory(ptr); + allocated.remove(ptr); + } + + @Override + public int gettimeofday(long timeval, long alwaysNull) { + UnsafeMemory.UNSAFE.putLong(timeval, 7L); + UnsafeMemory.UNSAFE.putLong(timeval + 8, 250_000L); + return 0; + } + }; + JNRPosixAPI api = newApi(stub); + + long micros = api.gettimeofday(); + assertEquals(7_000_000L + 250_000L, micros); + assertTrue(allocated.isEmpty()); + } + + @Test + public void mlockallThrowsOnFailure() throws Exception { + JNRPosixInterface stub = new BaseStub() { + @Override + public int mlockall(int flags) { + return -1; + } + + @Override + public String strerror(int errno) { + return "mlockall"; + } + }; + JNRPosixAPI api = newApi(stub); + + try { + api.mlockall(0x123); + fail("Expected PosixRuntimeException"); + } catch (IllegalArgumentException | PosixRuntimeException expected) { + assertTrue(expected.getMessage().contains("mlockall")); + } + } + + @Test + public void mmapReturnsPointer() throws Exception { + JNRPosixInterface stub = new BaseStub() { + @Override + public long mmap(Pointer addr, long length, int prot, int flags, int fd, long offset) { + return 123L; + } + }; + JNRPosixAPI api = newApi(stub); + + assertEquals(123L, api.mmap(0L, 64L, 1, 2, 3, 4L)); + } + + @Test + public void mmapThrowsOnFailure() throws Exception { + JNRPosixInterface stub = new BaseStub() { + @Override + public long mmap(Pointer addr, long length, int prot, int flags, int fd, long offset) { + return 0L; + } + }; + JNRPosixAPI api = newApi(stub); + + try { + api.mmap(1L, 64L, 1, 2, 3, 4L); + fail("Expected PosixRuntimeException"); + } catch (PosixRuntimeException expected) { + assertFalse(expected.getMessage().isEmpty()); + } + } + + @Test + public void getGettidFallsBackToSyscallOn32Bit() throws Exception { + boolean original32 = readStaticBoolean(UnsafeMemory.class, "IS32BIT"); + boolean original64 = readStaticBoolean(UnsafeMemory.class, "IS64BIT"); + swapStaticBoolean(UnsafeMemory.class, "IS32BIT", true); + swapStaticBoolean(UnsafeMemory.class, "IS64BIT", false); + try { + AtomicInteger calls = new AtomicInteger(); + JNRPosixInterface stub = new BaseStub() { + @Override + public int gettid() { + throw new UnsatisfiedLinkError("no symbol"); + } + + @Override + public int syscall(int number) { + assertEquals(224, number); + calls.incrementAndGet(); + return 99; + } + }; + JNRPosixAPI api = newApi(stub); + Method method = JNRPosixAPI.class.getDeclaredMethod("getGettid"); + method.setAccessible(true); + IntSupplier supplier = (IntSupplier) method.invoke(api); + assertEquals(99, supplier.getAsInt()); + assertEquals(1, calls.get()); + } finally { + swapStaticBoolean(UnsafeMemory.class, "IS32BIT", original32); + swapStaticBoolean(UnsafeMemory.class, "IS64BIT", original64); + } + } + + @Test + public void mlockallLogsWhenDumpEnabled() throws Exception { + boolean originalDump = swapStaticBoolean(JNRPosixAPI.class, "MOCKALL_DUMP", true); + String previousProperty = System.getProperty("mlockall.dump"); + System.setProperty("mlockall.dump", "true"); + AtomicBoolean firstCall = new AtomicBoolean(true); + AtomicInteger calls = new AtomicInteger(); + JNRPosixInterface stub = new BaseStub() { + @Override + public int mlock(long addr, long length) { + calls.incrementAndGet(); + if (firstCall.getAndSet(false)) + return -1; + return 0; + } + + @Override + public int mlockall(int flags) { + throw new AssertionError("mlockall should not be called in fallback"); + } + }; + JNRPosixAPI api = newApi(stub); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintStream originalOut = System.out; + System.setOut(new PrintStream(baos)); + try { + api.mlockall(MclFlag.MclCurrent.code()); + } finally { + System.setOut(originalOut); + if (previousProperty == null) { + System.clearProperty("mlockall.dump"); + } else { + System.setProperty("mlockall.dump", previousProperty); + } + swapStaticBoolean(JNRPosixAPI.class, "MOCKALL_DUMP", originalDump); + } + assertTrue(calls.get() > 0); + assertTrue(baos.toString().length() > 0); + } + + private static JNRPosixAPI newApi(JNRPosixInterface stub) throws Exception { + JNRPosixAPI api = (JNRPosixAPI) UnsafeMemory.UNSAFE.allocateInstance(JNRPosixAPI.class); + setField(api, "jnr", stub); + setField(api, "gettid", (IntSupplier) () -> 1); + return api; + } + + private static void setField(Object target, String fieldName, Object value) throws Exception { + Field field = JNRPosixAPI.class.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(target, value); + } + + private static boolean swapStaticBoolean(Class type, String fieldName, boolean newValue) throws Exception { + Field field = type.getDeclaredField(fieldName); + field.setAccessible(true); + Object base = UnsafeMemory.UNSAFE.staticFieldBase(field); + long offset = UnsafeMemory.UNSAFE.staticFieldOffset(field); + boolean old = UnsafeMemory.UNSAFE.getBoolean(base, offset); + UnsafeMemory.UNSAFE.putBoolean(base, offset, newValue); + return old; + } + + private static boolean readStaticBoolean(Class type, String fieldName) throws Exception { + Field field = type.getDeclaredField(fieldName); + field.setAccessible(true); + Object base = UnsafeMemory.UNSAFE.staticFieldBase(field); + long offset = UnsafeMemory.UNSAFE.staticFieldOffset(field); + return UnsafeMemory.UNSAFE.getBoolean(base, offset); + } + + private static String swapStaticString(Class type, String fieldName, Object newValue) throws Exception { + Field field = type.getDeclaredField(fieldName); + field.setAccessible(true); + Object base = UnsafeMemory.UNSAFE.staticFieldBase(field); + long offset = UnsafeMemory.UNSAFE.staticFieldOffset(field); + Object old = UnsafeMemory.UNSAFE.getObject(base, offset); + UnsafeMemory.UNSAFE.putObject(base, offset, newValue); + return old == null ? null : old.toString(); + } + + private static String readStaticString(Class type, String fieldName) throws Exception { + Field field = type.getDeclaredField(fieldName); + field.setAccessible(true); + Object base = UnsafeMemory.UNSAFE.staticFieldBase(field); + long offset = UnsafeMemory.UNSAFE.staticFieldOffset(field); + Object value = UnsafeMemory.UNSAFE.getObject(base, offset); + return value == null ? null : value.toString(); + } + + private static class BaseStub implements JNRPosixInterface { + @Override + public int open(CharSequence path, int flags, int perm) { + throw unsupported(); + } + + @Override + public long read(int fd, long dst, long len) { + throw unsupported(); + } + + @Override + public long write(int fd, long src, long len) { + throw unsupported(); + } + + @Override + public long lseek(int fd, long offset, int whence) { + throw unsupported(); + } + + @Override + public int lockf(int fd, int cmd, long len) { + throw unsupported(); + } + + @Override + public int flock(int fd, int operation) { + throw unsupported(); + } + + @Override + public int ftruncate(int fd, long offset) { + throw unsupported(); + } + + @Override + public int fallocate(int fd, int mode, long offset, long length) { + throw unsupported(); + } + + @Override + public int fallocate64(int fd, int mode, long offset, long length) { + throw unsupported(); + } + + @Override + public int posix_fallocate(int fd, long offset, long length) { + throw unsupported(); + } + + @Override + public int close(int fd) { + throw unsupported(); + } + + @Override + public int madvise(long addr, long length, int advise) { + throw unsupported(); + } + + @Override + public long mmap(Pointer addr, long length, int prot, int flags, int fd, long offset) { + throw unsupported(); + } + + @Override + public int munmap(long addr, long length) { + throw unsupported(); + } + + @Override + public int msync(long address, long length, int flags) { + throw unsupported(); + } + + @Override + public int gettimeofday(long timeval, long alwaysNull) { + throw unsupported(); + } + + @Override + public long malloc(long size) { + throw unsupported(); + } + + @Override + public void free(long ptr) { + throw unsupported(); + } + + @Override + public int get_nprocs() { + throw unsupported(); + } + + @Override + public int get_nprocs_conf() { + throw unsupported(); + } + + @Override + public int sched_setaffinity(int pid, int cpusetsize, Pointer mask) { + throw unsupported(); + } + + @Override + public int sched_getaffinity(int pid, int cpusetsize, Pointer mask) { + throw unsupported(); + } + + @Override + public int getpid() { + throw unsupported(); + } + + @Override + public int gettid() { + throw unsupported(); + } + + @Override + public String strerror(int errno) { + throw unsupported(); + } + + @Override + public int clock_gettime(int clockId, long ptr) { + throw unsupported(); + } + + @Override + public int mlock(long addr, long length) { + throw unsupported(); + } + + @Override + public int mlock2(long addr, long length, int flags) { + throw unsupported(); + } + + @Override + public int mlockall(int flags) { + throw unsupported(); + } + + @Override + public int syscall(int number) { + throw unsupported(); + } + + @Override + public int syscall(int number, long arg1, long arg2, int arg3) { + throw unsupported(); + } + + private UnsupportedOperationException unsupported() { + return new UnsupportedOperationException("not expected"); + } + } +} diff --git a/src/test/java/net/openhft/posix/internal/jnr/WinJNRPosixAPITest.java b/src/test/java/net/openhft/posix/internal/jnr/WinJNRPosixAPITest.java new file mode 100644 index 0000000..5288613 --- /dev/null +++ b/src/test/java/net/openhft/posix/internal/jnr/WinJNRPosixAPITest.java @@ -0,0 +1,139 @@ +package net.openhft.posix.internal.jnr; + +import net.openhft.posix.internal.UnsafeMemory; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.*; + +public class WinJNRPosixAPITest { + + @Test + public void delegatesToNativeInterfaces() throws Exception { + RecordingWin win = new RecordingWin(); + RecordingKernel kernel = new RecordingKernel(); + + WinJNRPosixAPI api = (WinJNRPosixAPI) UnsafeMemory.UNSAFE.allocateInstance(WinJNRPosixAPI.class); + setField(api, "jnr", win); + setField(api, "kernel32", kernel); + + assertEquals(42, api.open("path", 1, 2)); + assertEquals(64L, api.lseek(3, 4L, 5)); + assertEquals(5L, api.read(6, 7L, 8L)); + assertEquals(6L, api.write(9, 10L, 11L)); + assertEquals(12, api.close(13)); + assertEquals(1234, api.getpid()); + assertEquals(9876, api.gettid()); + assertEquals("error", api.strerror(99)); + + long ptr = api.malloc(32); + assertNotEquals(0L, ptr); + api.free(ptr); + + assertEquals(0, api.madvise(1L, 2L, 3)); + assertEquals(0, api.msync(1L, 2L, 3)); + assertEquals(0, api.fallocate(1, 2, 3L, 4L)); + assertEquals(0, api.ftruncate(1, 2L)); + assertEquals(0L, api.mmap(1L, 2L, 3, 4, 5, 6L)); + assertEquals(0, api.munmap(1L, 2L)); + long timeval = UnsafeMemory.UNSAFE.allocateMemory(16); + try { + assertEquals(0, api.gettimeofday(timeval)); + } finally { + UnsafeMemory.UNSAFE.freeMemory(timeval); + } + assertTrue(api.clock_gettime() > 0); + assertTrue(api.clock_gettime(1) > 0); + assertEquals(-1, api.lockf(1, 2, 3L)); + assertEquals(-1, api.sched_setaffinity(1, 2, 3L)); + assertEquals(-1, api.sched_getaffinity(1, 2, 3L)); + assertTrue(api.lastError() >= 0); + assertEquals(Runtime.getRuntime().availableProcessors(), api.get_nprocs_conf()); + assertEquals(api.get_nprocs_conf(), api.get_nprocs()); + + assertEquals(List.of("open", "lseek", "read", "write", "close", "pid", "strerror"), win.calls); + assertEquals(List.of("tid"), kernel.calls); + } + + private static void setField(Object target, String fieldName, Object value) throws Exception { + Field field = WinJNRPosixAPI.class.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(target, value); + } + + private static final class RecordingWin implements WinJNRPosixInterface { + final List calls = new ArrayList<>(); + + @Override + public long malloc(long size) { + calls.add("malloc"); + return UnsafeMemory.UNSAFE.allocateMemory(size); + } + + @Override + public void free(long ptr) { + calls.add("free"); + UnsafeMemory.UNSAFE.freeMemory(ptr); + } + + @Override + public int _close(int fd) { + calls.add("close"); + return 12; + } + + @Override + public int _open(CharSequence path, int flags, int perm) { + calls.add("open"); + return 42; + } + + @Override + public long _lseeki64(int fd, long offset, int origin) { + calls.add("lseek"); + return 64L; + } + + @Override + public long _read(int fd, long dst, long len) { + calls.add("read"); + return 5L; + } + + @Override + public long _write(int fd, long src, long len) { + calls.add("write"); + return 6L; + } + + @Override + public int _getpid() { + calls.add("pid"); + return 1234; + } + + @Override + public String strerror(int errno) { + calls.add("strerror"); + return "error"; + } + } + + private static final class RecordingKernel implements Kernel32JNRInterface { + final List calls = new ArrayList<>(); + + @Override + public int GetCurrentThreadId() { + calls.add("tid"); + return 9876; + } + + @Override + public void GetNativeSystemInfo(long addr) { + calls.add("sysinfo"); + } + } +} From bf2b63692e57e00b97064a41bac74140d9469741 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Tue, 28 Oct 2025 10:21:38 +0000 Subject: [PATCH 11/17] Refactor code comments and formatting for clarity and consistency --- AGENTS.md | 80 +++++++++++-------- README.adoc | 1 - pom.xml | 15 ++-- src/main/adoc/decision-log.adoc | 28 +++---- src/main/adoc/project-requirements.adoc | 6 +- src/main/config/spotbugs-exclude.xml | 76 ++++++++++-------- src/main/java/net/openhft/posix/ClockId.java | 48 ++++++++--- .../java/net/openhft/posix/LockfFlag.java | 20 +++-- .../java/net/openhft/posix/MAdviseFlag.java | 72 ++++++++++++----- src/main/java/net/openhft/posix/MMapFlag.java | 12 ++- src/main/java/net/openhft/posix/MMapProt.java | 30 +++++-- .../java/net/openhft/posix/MSyncFlag.java | 16 +++- src/main/java/net/openhft/posix/Mapping.java | 37 ++++++--- src/main/java/net/openhft/posix/MclFlag.java | 20 +++-- src/main/java/net/openhft/posix/OpenFlag.java | 52 +++++++++--- .../openhft/posix/PosixRuntimeException.java | 8 +- src/main/java/net/openhft/posix/ProcMaps.java | 4 +- .../java/net/openhft/posix/WhenceFlag.java | 20 +++-- .../net/openhft/posix/internal/core/Jvm.java | 4 +- .../net/openhft/posix/internal/core/OS.java | 4 +- .../posix/internal/jnr/JNRPosixAPI.java | 18 ++--- .../posix/internal/jnr/JNRPosixInterface.java | 4 + .../openhft/posix/internal/package-info.java | 4 +- .../java/net/openhft/posix/package-info.java | 4 +- .../posix/internal/PosixAPIHolderTest.java | 3 +- .../posix/internal/jna/JNAPosixAPITest.java | 7 +- .../jnr/JNRPosixAPINonNativeTest.java | 6 +- .../posix/internal/jnr/JNRPosixAPITest.java | 1 + .../posix/internal/raw/RawPosixAPITest.java | 2 +- 29 files changed, 394 insertions(+), 208 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 7f8a075..620cde4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,23 +8,23 @@ LLM-based agents can accelerate development only if they respect our house rules ## Language & character-set policy -| Requirement | Rationale | -|--------------|-----------| -| **British English** spelling (`organisation`, `licence`, *not* `organization`, `license`) except technical US spellings like `synchronized` | Keeps wording consistent with Chronicle's London HQ and existing docs. See the University of Oxford style guide for reference. | -| **ASCII-7 only** (code-points 0-127). Avoid smart quotes, non-breaking spaces and accented characters. | ASCII-7 survives every toolchain Chronicle uses, incl. low-latency binary wire formats that expect the 8th bit to be 0. | -| If a symbol is not available in ASCII-7, use a textual form such as `micro-second`, `>=`, `:alpha:`, `:yes:`. This is the preferred approach and Unicode must not be inserted. | Extended or '8-bit ASCII' variants are *not* portable and are therefore disallowed. | +| Requirement | Rationale | +|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------| +| **British English** spelling (`organisation`, `licence`, *not* `organization`, `license`) except technical US spellings like `synchronized` | Keeps wording consistent with Chronicle's London HQ and existing docs. See the University of Oxford style guide for reference. | +| **ASCII-7 only** (code-points 0-127). Avoid smart quotes, non-breaking spaces and accented characters. | ASCII-7 survives every toolchain Chronicle uses, incl. low-latency binary wire formats that expect the 8th bit to be 0. | +| If a symbol is not available in ASCII-7, use a textual form such as `micro-second`, `>=`, `:alpha:`, `:yes:`. This is the preferred approach and Unicode must not be inserted. | Extended or '8-bit ASCII' variants are *not* portable and are therefore disallowed. | ## Javadoc guidelines **Goal:** Every Javadoc block should add information you cannot glean from the method signature alone. Anything else is noise and slows readers down. -| Do | Don't | -|----|-------| -| State *behavioural contracts*, edge-cases, thread-safety guarantees, units, performance characteristics and checked exceptions. | Restate the obvious ("Gets the value", "Sets the name"). | -| Keep the first sentence short; it becomes the summary line in aggregated docs. | Duplicate parameter names/ types unless more explanation is needed. | -| Prefer `@param` for *constraints* and `@throws` for *conditions*, following Oracle's style guide. | Pad comments to reach a line-length target. | -| Remove or rewrite autogenerated Javadoc for trivial getters/setters. | Leave stale comments that now contradict the code. | +| Do | Don't | +|---------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------| +| State *behavioural contracts*, edge-cases, thread-safety guarantees, units, performance characteristics and checked exceptions. | Restate the obvious ("Gets the value", "Sets the name"). | +| Keep the first sentence short; it becomes the summary line in aggregated docs. | Duplicate parameter names/ types unless more explanation is needed. | +| Prefer `@param` for *constraints* and `@throws` for *conditions*, following Oracle's style guide. | Pad comments to reach a line-length target. | +| Remove or rewrite autogenerated Javadoc for trivial getters/setters. | Leave stale comments that now contradict the code. | The principle that Javadoc should only explain what is *not* manifest from the signature is well-established in the wider Java community. @@ -60,7 +60,8 @@ See the [Project Requirements](src/main/adoc/project-requirements.adoc) for deta ## Elevating the Workflow with Real-Time Documentation -Building upon our existing Iterative Workflow, the newest recommendation is to emphasise *real-time updates* to documentation. +Building upon our existing Iterative Workflow, the newest recommendation is to emphasise *real-time updates* to +documentation. Ensure the relevant `.adoc` files are updated when features, requirements, implementation details, or tests change. This tight loop informs the AI accurately and creates immediate clarity for all team members. @@ -75,41 +76,54 @@ This tight loop informs the AI accurately and creates immediate clarity for all ### Best Practices -* **Maintain Sync**: Keep documentation (AsciiDoc), tests, and code synchronised in version control. Changes in one area should prompt reviews and potential updates in the others. -* **Doc-First for New Work**: For *new* features or requirements, aim to update documentation first, then use AI to help produce or refine corresponding code and tests. For refactoring or initial bootstrapping, updates might flow from code/tests back to documentation, which should then be reviewed and finalised. -* **Small Commits**: Each commit should ideally relate to a single requirement or coherent change, making reviews easier for humans and AI analysis tools. -- **Team Buy-In**: Encourage everyone to review AI outputs critically and contribute to maintaining the synchronicity of all artefacts. +* **Maintain Sync**: Keep documentation (AsciiDoc), tests, and code synchronised in version control. Changes in one area + should prompt reviews and potential updates in the others. +* **Doc-First for New Work**: For *new* features or requirements, aim to update documentation first, then use AI to help + produce or refine corresponding code and tests. For refactoring or initial bootstrapping, updates might flow from + code/tests back to documentation, which should then be reviewed and finalised. +* **Small Commits**: Each commit should ideally relate to a single requirement or coherent change, making reviews easier + for humans and AI analysis tools. + +- **Team Buy-In**: Encourage everyone to review AI outputs critically and contribute to maintaining the synchronicity of + all artefacts. ## AI Agent Guidelines When using AI agents to assist with development, please adhere to the following guidelines: -* **Respect the Language & Character-set Policy**: Ensure all AI-generated content follows the British English and ASCII-7 guidelines outlined above. -Focus on Clarity: AI-generated documentation should be clear and concise and add value beyond what is already present in the code or existing documentation. -* **Avoid Redundancy**: Do not generate content that duplicates existing documentation or code comments unless it provides additional context or clarification. -* **Review AI Outputs**: Always review AI-generated content for accuracy, relevance, and adherence to the project's documentation standards before committing it to the repository. +* **Respect the Language & Character-set Policy**: Ensure all AI-generated content follows the British English and + ASCII-7 guidelines outlined above. + Focus on Clarity: AI-generated documentation should be clear and concise and add value beyond what is already present + in the code or existing documentation. +* **Avoid Redundancy**: Do not generate content that duplicates existing documentation or code comments unless it + provides additional context or clarification. +* **Review AI Outputs**: Always review AI-generated content for accuracy, relevance, and adherence to the project's + documentation standards before committing it to the repository. ## Company-Wide Tagging -This section records **company-wide** decisions that apply to *all* Chronicle projects. All identifiers use the --xxx prefix. The `xxx` are unique across in the same Scope even if the tags are different. Component-specific decisions live in their xxx-decision-log.adoc files. +This section records **company-wide** decisions that apply to *all* Chronicle projects. All identifiers use +the --xxx prefix. The `xxx` are unique across in the same Scope even if the tags are different. +Component-specific decisions live in their xxx-decision-log.adoc files. ### Tag Taxonomy (Nine-Box Framework) -To improve traceability, we adopt the Nine-Box taxonomy for requirement and decision identifiers. These tags are used in addition to the existing ALL prefix, which remains reserved for global decisions across every project. +To improve traceability, we adopt the Nine-Box taxonomy for requirement and decision identifiers. These tags are used in +addition to the existing ALL prefix, which remains reserved for global decisions across every project. .Adopt a Nine-Box Requirement Taxonomy -|Tag | Scope | Typical examples | -|----|-------|------------------| -|FN |Functional user-visible behaviour | Message routing, business rules | -|NF-P |Non-functional - Performance | Latency budgets, throughput targets | -|NF-S |Non-functional - Security | Authentication method, TLS version | -|NF-O |Non-functional - Operability | Logging, monitoring, health checks | -|TEST |Test / QA obligations | Chaos scenarios, benchmarking rigs | -|DOC |Documentation obligations | Sequence diagrams, user guides | -|OPS |Operational / DevOps concerns | Helm values, deployment checklist | -|UX |Operator or end-user experience | CLI ergonomics, dashboard layouts | -|RISK |Compliance / risk controls | GDPR retention, audit trail | +| Tag | Scope | Typical examples | +|------|-----------------------------------|-------------------------------------| +| FN | Functional user-visible behaviour | Message routing, business rules | +| NF-P | Non-functional - Performance | Latency budgets, throughput targets | +| NF-S | Non-functional - Security | Authentication method, TLS version | +| NF-O | Non-functional - Operability | Logging, monitoring, health checks | +| TEST | Test / QA obligations | Chaos scenarios, benchmarking rigs | +| DOC | Documentation obligations | Sequence diagrams, user guides | +| OPS | Operational / DevOps concerns | Helm values, deployment checklist | +| UX | Operator or end-user experience | CLI ergonomics, dashboard layouts | +| RISK | Compliance / risk controls | GDPR retention, audit trail | `ALL-*` stays global, case-exact tags. Pick one primary tag if multiple apply. diff --git a/README.adoc b/README.adoc index 40f2a5a..aeeeae5 100644 --- a/README.adoc +++ b/README.adoc @@ -118,7 +118,6 @@ Add JVM arg: ==== [%collapsible] - == Further reading * link:src/main/adoc/project-requirements.adoc[Functional requirements] diff --git a/pom.xml b/pom.xml index 544fa7b..03643ce 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ net.openhft java-parent-pom 1.27ea1 - + posix @@ -16,7 +16,7 @@ OpenHFT Java Posix APIs jar - + 3.6.0 10.26.1 4.9.8.1 @@ -24,9 +24,9 @@ 3.28.0 0.8.14 0.80 - 0.70 - 1.23ea6 - + 0.70 + 1.23ea6 + @@ -201,8 +201,9 @@ Max Low true - ${project.basedir}/src/main/config/spotbugs-exclude.xml - + ${project.basedir}/src/main/config/spotbugs-exclude.xml + + com.h3xstream.findsecbugs findsecbugs-plugin diff --git a/src/main/adoc/decision-log.adoc b/src/main/adoc/decision-log.adoc index a0b9600..d491311 100644 --- a/src/main/adoc/decision-log.adoc +++ b/src/main/adoc/decision-log.adoc @@ -17,11 +17,11 @@ | ALL-OPS-004 | Nine-Box Tag Taxonomy Adoption | 2025-05-26 | OPS |=== - == ALL-DOC-001 Language & Character-Set Policy *Context*:: -Chronicle teams span multiple regions. Mixed US/UK spelling and occasional UTF-8 artefacts caused build failures in binary wire-format tests and confused spell-checkers. +Chronicle teams span multiple regions. +Mixed US/UK spelling and occasional UTF-8 artefacts caused build failures in binary wire-format tests and confused spell-checkers. *Decision*:: * Use **British English** spelling for prose/code except fixed US technical spellings (`synchronized`, `Serializable`). @@ -38,7 +38,9 @@ Chronicle teams span multiple regions. Mixed US/UK spelling and occasional UTF-8 == POSIX-FN-002 Provider Fallback Order *Context*:: -The Posix façade must run on Linux, macOS and Windows. Performance varies between native libraries (JNR, JNA) and reflection. Deterministic bootstrap order avoids surprises. +The Posix façade must run on Linux, macOS and Windows. +Performance varies between native libraries (JNR, JNA) and reflection. +Deterministic bootstrap order avoids surprises. *Decision*:: At first successful class-load we try providers in this strict order: @@ -62,7 +64,8 @@ Override with `-Dchronicle.posix.provider`. == ALL-DOC-002 Real-Time Documentation Loop *Context*:: -Documentation drift led to mis-generated code from AI agents. The team adopted a “doc-first or doc-alongside” workflow but lacked formal guidance. +Documentation drift led to mis-generated code from AI agents. +The team adopted a “doc-first or doc-alongside” workflow but lacked formal guidance. *Decision*:: * Update AsciiDoc **in the same commit** as code/tests. @@ -78,27 +81,22 @@ Quicker feedback, cleaner PR reviews, slightly longer dev cycle per change. Some environments (Graal native-image, security-restricted CI) cannot load native libs. *Decision*:: -`NoOpPosixAPI` compiles everywhere, returns **0** for safe no-ops, **-1** (or -throws `PosixRuntimeException(errno=ENOSYS)`) where silent failure may lose data. +`NoOpPosixAPI` compiles everywhere, returns **0** for safe no-ops, **-1** (or throws `PosixRuntimeException(errno=ENOSYS)`) where silent failure may lose data. == POSIX-NF-O-006 Affinity Mask Sizing Policy *Context*:: -Regression POSIX-2025-05 observed incorrect CPU pinning on hosts with more than -thirty-two logical processors. Bit masks were truncated because offsets used -byte indices rather than `int` words and mask buffers were only sized for a -single `long`. +Regression POSIX-2025-05 observed incorrect CPU pinning on hosts with more than thirty-two logical processors. +Bit masks were truncated because offsets used byte indices rather than `int` words and mask buffers were only sized for a single `long`. *Decision*:: * Introduce `CpuSetUtil` to normalise mask sizing (round up to whole - `Long.BYTES` blocks) and byte packing. -* Apply the helper in `PosixJNAAffinity` and export the same contract to - embedding projects (Java Thread Affinity). +`Long.BYTES` blocks) and byte packing. +* Apply the helper in `PosixJNAAffinity` and export the same contract to embedding projects (Java Thread Affinity). * Add pure-Java tests that simulate > 64-core schedulers. *Alternatives*:: -*Hard-code buffer sizes per architecture* – rejected because it risks further -drift when porting to new platforms. +*Hard-code buffer sizes per architecture* – rejected because it risks further drift when porting to new platforms. *Consequences*:: * Mask handling now covers arbitrary CPU counts permitted by the kernel. diff --git a/src/main/adoc/project-requirements.adoc b/src/main/adoc/project-requirements.adoc index 2c492b6..8020849 100644 --- a/src/main/adoc/project-requirements.adoc +++ b/src/main/adoc/project-requirements.adoc @@ -7,10 +7,8 @@ == Introduction -_OpenHFT Posix_ is a low-level Java façade over selected POSIX / Linux -system calls, aimed at deterministic, zero-GC latency paths in trading -workloads. It runs on Linux, macOS, Windows and in sandboxed CI by -degrading to a *No-Op* implementation. +_OpenHFT Posix_ is a low-level Java façade over selected POSIX / Linux system calls, aimed at deterministic, zero-GC latency paths in trading workloads. +It runs on Linux, macOS, Windows and in sandboxed CI by degrading to a *No-Op* implementation. == POSIX-FN-Requirements diff --git a/src/main/config/spotbugs-exclude.xml b/src/main/config/spotbugs-exclude.xml index 0e97348..2914527 100644 --- a/src/main/config/spotbugs-exclude.xml +++ b/src/main/config/spotbugs-exclude.xml @@ -1,38 +1,44 @@ - - - - - - - - - - - - - - - - - - - POSIX-SEC-204: ProcessBuilder is invoked with fixed argv and never shells out; arguments are passed as execve tokens so there is no injection surface. - - - - - - - POSIX-OPS-102: Reads the kernel-managed /proc/<pid>/maps file for diagnostics; caller supplies PID but resource remains confined to procfs. - - - - - - - POSIX-API-117: Methods wrap errno reporting, throwing RuntimeException to preserve existing API contracts; change to checked exceptions would be breaking. - - + + + + + + + + + + + + + + + + + + + POSIX-SEC-204: ProcessBuilder is invoked with fixed argv and never shells out; arguments are passed + as execve tokens so there is no injection surface. + + + + + + + + POSIX-OPS-102: Reads the kernel-managed /proc/<pid>/maps file for diagnostics; caller + supplies PID but resource remains confined to procfs. + + + + + + + + POSIX-API-117: Methods wrap errno reporting, throwing RuntimeException to preserve existing API + contracts; change to checked exceptions would be breaking. + + + diff --git a/src/main/java/net/openhft/posix/ClockId.java b/src/main/java/net/openhft/posix/ClockId.java index 27450c6..fb64a83 100644 --- a/src/main/java/net/openhft/posix/ClockId.java +++ b/src/main/java/net/openhft/posix/ClockId.java @@ -8,41 +8,65 @@ * @see clock_gettime(2) */ public enum ClockId { - /** The system-wide real-time clock. */ + /** + * The system-wide real-time clock. + */ CLOCK_REALTIME(0), - /** Monotonic clock that cannot be set. */ + /** + * Monotonic clock that cannot be set. + */ CLOCK_MONOTONIC(1), - /** CPU time consumed by the process. */ + /** + * CPU time consumed by the process. + */ CLOCK_PROCESS_CPUTIME_ID(2), - /** CPU time consumed by the thread. */ + /** + * CPU time consumed by the thread. + */ CLOCK_THREAD_CPUTIME_ID(3), - /** Monotonic clock without NTP adjustments. */ + /** + * Monotonic clock without NTP adjustments. + */ CLOCK_MONOTONIC_RAW(4), - /** Faster but coarse real-time clock. */ + /** + * Faster but coarse real-time clock. + */ CLOCK_REALTIME_COARSE(5), - /** Faster but coarse monotonic clock. */ + /** + * Faster but coarse monotonic clock. + */ CLOCK_MONOTONIC_COARSE(6), - /** Monotonic clock including suspend time. */ + /** + * Monotonic clock including suspend time. + */ CLOCK_BOOTTIME(7), - /** Real-time clock used for alarms. */ + /** + * Real-time clock used for alarms. + */ CLOCK_REALTIME_ALARM(8), - /** Boot-time clock used for alarms. */ + /** + * Boot-time clock used for alarms. + */ CLOCK_BOOTTIME_ALARM(9), - /** SGI cycle counter. */ + /** + * SGI cycle counter. + */ CLOCK_SGI_CYCLE(10); - /** Native constant value. */ + /** + * Native constant value. + */ private final int value; /** diff --git a/src/main/java/net/openhft/posix/LockfFlag.java b/src/main/java/net/openhft/posix/LockfFlag.java index 210dfe3..278ca77 100644 --- a/src/main/java/net/openhft/posix/LockfFlag.java +++ b/src/main/java/net/openhft/posix/LockfFlag.java @@ -8,19 +8,29 @@ * @see lockf(3) */ public enum LockfFlag { - /** Release the specified section. */ + /** + * Release the specified section. + */ F_ULOCK(0), - /** Exclusive lock on the section. Blocks until available. */ + /** + * Exclusive lock on the section. Blocks until available. + */ F_LOCK(1), - /** Non-blocking exclusive lock. */ + /** + * Non-blocking exclusive lock. + */ F_TLOCK(2), - /** Test whether a lock is held by another process. */ + /** + * Test whether a lock is held by another process. + */ F_TEST(3); - /** Native constant value. */ + /** + * Native constant value. + */ final int value; /** diff --git a/src/main/java/net/openhft/posix/MAdviseFlag.java b/src/main/java/net/openhft/posix/MAdviseFlag.java index def3be8..0799827 100644 --- a/src/main/java/net/openhft/posix/MAdviseFlag.java +++ b/src/main/java/net/openhft/posix/MAdviseFlag.java @@ -8,58 +8,94 @@ * @see madvise(2) */ public enum MAdviseFlag { - /** No further special treatment. */ + /** + * No further special treatment. + */ MADV_NORMAL(0), - /** Expect random page references. */ + /** + * Expect random page references. + */ MADV_RANDOM(1), - /** Expect sequential page references. */ + /** + * Expect sequential page references. + */ MADV_SEQUENTIAL(2), - /** Will need these pages. */ + /** + * Will need these pages. + */ MADV_WILLNEED(3), - /** No need for these pages. */ + /** + * No need for these pages. + */ MADV_DONTNEED(4), - /** Free pages only if memory pressure. */ + /** + * Free pages only if memory pressure. + */ MADV_FREE(8), - /** Remove these pages and resources. */ + /** + * Remove these pages and resources. + */ MADV_REMOVE(9), - /** Do not inherit across fork. */ + /** + * Do not inherit across fork. + */ MADV_DONTFORK(10), - /** Inherit across fork. */ + /** + * Inherit across fork. + */ MADV_DOFORK(11), - /** KSM may merge identical pages. */ + /** + * KSM may merge identical pages. + */ MADV_MERGEABLE(12), - /** KSM may not merge identical pages. */ + /** + * KSM may not merge identical pages. + */ MADV_UNMERGEABLE(13), - /** Hint backing with huge pages. */ + /** + * Hint backing with huge pages. + */ MADV_HUGEPAGE(14), - /** Hint not worth backing with huge pages. */ + /** + * Hint not worth backing with huge pages. + */ MADV_NOHUGEPAGE(15), - /** Exclude from core dump and override the coredump filter. */ + /** + * Exclude from core dump and override the coredump filter. + */ MADV_DONTDUMP(16), - /** Clear the MADV_DONTDUMP flag. */ + /** + * Clear the MADV_DONTDUMP flag. + */ MADV_DODUMP(17), - /** Zero memory on fork in the child. */ + /** + * Zero memory on fork in the child. + */ MADV_WIPEONFORK(18), - /** Undo MADV_WIPEONFORK. */ + /** + * Undo MADV_WIPEONFORK. + */ MADV_KEEPONFORK(19); - /** Native constant value. */ + /** + * Native constant value. + */ final int value; /** diff --git a/src/main/java/net/openhft/posix/MMapFlag.java b/src/main/java/net/openhft/posix/MMapFlag.java index 589de22..0517b95 100644 --- a/src/main/java/net/openhft/posix/MMapFlag.java +++ b/src/main/java/net/openhft/posix/MMapFlag.java @@ -8,13 +8,19 @@ * @see mmap(2) */ public enum MMapFlag { - /** Memory mapping to be shared with other processes. */ + /** + * Memory mapping to be shared with other processes. + */ SHARED(1), - /** Memory mapping to be private to the process. */ + /** + * Memory mapping to be private to the process. + */ PRIVATE(2); - /** Native constant value. */ + /** + * Native constant value. + */ private final int value; /** diff --git a/src/main/java/net/openhft/posix/MMapProt.java b/src/main/java/net/openhft/posix/MMapProt.java index e7eb3e3..918f9bb 100644 --- a/src/main/java/net/openhft/posix/MMapProt.java +++ b/src/main/java/net/openhft/posix/MMapProt.java @@ -8,28 +8,42 @@ * @see mmap(2) */ public enum MMapProt { - /** Allow read access. */ + /** + * Allow read access. + */ PROT_READ(1), - /** Allow write access. */ + /** + * Allow write access. + */ PROT_WRITE(2), - /** Allow both read and write access. */ + /** + * Allow both read and write access. + */ PROT_READ_WRITE(3), - /** Allow execute access. */ + /** + * Allow execute access. + */ PROT_EXEC(4), - /** Allow execute and read access. */ + /** + * Allow execute and read access. + */ PROT_EXEC_READ(5), - /** No access allowed. */ + /** + * No access allowed. + */ PROT_NONE(8); - /** Native constant value. */ + /** + * Native constant value. + */ final int value; - /** + /** * @param value native constant value */ MMapProt(int value) { diff --git a/src/main/java/net/openhft/posix/MSyncFlag.java b/src/main/java/net/openhft/posix/MSyncFlag.java index 16b6713..2b38536 100644 --- a/src/main/java/net/openhft/posix/MSyncFlag.java +++ b/src/main/java/net/openhft/posix/MSyncFlag.java @@ -8,16 +8,24 @@ * @see msync(2) */ public enum MSyncFlag { - /** Sync memory asynchronously. */ + /** + * Sync memory asynchronously. + */ MS_ASYNC(1), - /** Invalidate caches. */ + /** + * Invalidate caches. + */ MS_INVALIDATE(2), - /** Synchronous memory sync. */ + /** + * Synchronous memory sync. + */ MS_SYNC(4); - /** Native constant value. */ + /** + * Native constant value. + */ private final int value; /** diff --git a/src/main/java/net/openhft/posix/Mapping.java b/src/main/java/net/openhft/posix/Mapping.java index 59540d2..ccb7e47 100644 --- a/src/main/java/net/openhft/posix/Mapping.java +++ b/src/main/java/net/openhft/posix/Mapping.java @@ -11,6 +11,7 @@ *

* On a 32-bit VM the addresses are truncated. * Instances are thread-safe and immutable. + * * @see proc(5) */ public final class Mapping { @@ -42,8 +43,8 @@ public final class Mapping { * Parses a mapping line from {@code /proc/[pid]/maps}. * * @param line textual line from the maps file - * @throws NumberFormatException if address or inode fields are not - * valid hex or decimal numbers + * @throws NumberFormatException if address or inode fields are not + * valid hex or decimal numbers * @throws ArrayIndexOutOfBoundsException if fields are missing */ public Mapping(String line) { @@ -60,42 +61,58 @@ public Mapping(String line) { toString = line; } - /** Start address of the mapping. */ + /** + * Start address of the mapping. + */ public long addr() { return addr; } - /** Length of the mapping. */ + /** + * Length of the mapping. + */ public long length() { return length; } - /** Offset into the file or VM object. */ + /** + * Offset into the file or VM object. + */ public long offset() { return offset; } - /** Inode number. */ + /** + * Inode number. + */ public long inode() { return inode; } - /** Permission string such as {@code r-xp}. */ + /** + * Permission string such as {@code r-xp}. + */ public String perms() { return perms; } - /** Device in {@code major:minor} form. */ + /** + * Device in {@code major:minor} form. + */ public String device() { return device; } - /** File path of the mapping if any. */ + /** + * File path of the mapping if any. + */ public String path() { return path; } - /** Original line from the maps file. */ + /** + * Original line from the maps file. + */ @Override public String toString() { return toString; diff --git a/src/main/java/net/openhft/posix/MclFlag.java b/src/main/java/net/openhft/posix/MclFlag.java index d11fa17..a5f4060 100644 --- a/src/main/java/net/openhft/posix/MclFlag.java +++ b/src/main/java/net/openhft/posix/MclFlag.java @@ -8,19 +8,29 @@ * @see mlockall(2) */ public enum MclFlag { - /** Lock all current pages in memory. */ + /** + * Lock all current pages in memory. + */ MclCurrent(1), - /** Lock all future pages in memory. */ + /** + * Lock all future pages in memory. + */ MclFuture(2), - /** Lock all current pages in memory on fault. */ + /** + * Lock all current pages in memory on fault. + */ MclCurrentOnFault(1 + 4), - /** Lock all future pages in memory on fault. */ + /** + * Lock all future pages in memory on fault. + */ MclFutureOnFault(2 + 4); - /** Native constant value. */ + /** + * Native constant value. + */ private final int code; /** diff --git a/src/main/java/net/openhft/posix/OpenFlag.java b/src/main/java/net/openhft/posix/OpenFlag.java index 255e6b8..34c51b1 100644 --- a/src/main/java/net/openhft/posix/OpenFlag.java +++ b/src/main/java/net/openhft/posix/OpenFlag.java @@ -8,43 +8,69 @@ * @see open(2) */ public enum OpenFlag { - /** Open for reading only. */ + /** + * Open for reading only. + */ O_RDONLY(0x0000), - /** Open for writing only. */ + /** + * Open for writing only. + */ O_WRONLY(0x0001), - /** Open for reading and writing. */ + /** + * Open for reading and writing. + */ O_RDWR(0x0002), - /** Non-blocking mode. */ + /** + * Non-blocking mode. + */ O_NONBLOCK(0x0004), - /** Append mode. */ + /** + * Append mode. + */ O_APPEND(0x0008), - /** Open with shared file lock. */ + /** + * Open with shared file lock. + */ O_SHLOCK(0x0010), - /** Open with exclusive file lock. */ + /** + * Open with exclusive file lock. + */ O_EXLOCK(0x0020), - /** Signal process group when data is ready. */ + /** + * Signal process group when data is ready. + */ O_ASYNC(0x0040), - /** Synchronous writes. */ + /** + * Synchronous writes. + */ O_FSYNC(0x0080), - /** Create if non-existent. */ + /** + * Create if non-existent. + */ O_CREAT(0x0200), - /** Truncate to zero length. */ + /** + * Truncate to zero length. + */ O_TRUNC(0x0400), - /** Error if already exists. */ + /** + * Error if already exists. + */ O_EXCL(0x0800); - /** Native constant value. */ + /** + * Native constant value. + */ final int value; /** diff --git a/src/main/java/net/openhft/posix/PosixRuntimeException.java b/src/main/java/net/openhft/posix/PosixRuntimeException.java index 15d8307..41e90b2 100644 --- a/src/main/java/net/openhft/posix/PosixRuntimeException.java +++ b/src/main/java/net/openhft/posix/PosixRuntimeException.java @@ -5,10 +5,14 @@ * produced by the underlying native call. */ public class PosixRuntimeException extends RuntimeException { - /** Used to maintain serialization compatibility. */ + /** + * Used to maintain serialization compatibility. + */ private static final long serialVersionUID = 0L; - /** POSIX errno captured from the failing call. */ + /** + * POSIX errno captured from the failing call. + */ private final int errno; /** diff --git a/src/main/java/net/openhft/posix/ProcMaps.java b/src/main/java/net/openhft/posix/ProcMaps.java index 15b16f9..473865e 100644 --- a/src/main/java/net/openhft/posix/ProcMaps.java +++ b/src/main/java/net/openhft/posix/ProcMaps.java @@ -3,10 +3,10 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.io.BufferedReader; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.function.Predicate; diff --git a/src/main/java/net/openhft/posix/WhenceFlag.java b/src/main/java/net/openhft/posix/WhenceFlag.java index 9ade1d2..794e12a 100644 --- a/src/main/java/net/openhft/posix/WhenceFlag.java +++ b/src/main/java/net/openhft/posix/WhenceFlag.java @@ -8,19 +8,29 @@ * @see lseek(2) */ public enum WhenceFlag { - /** Offset is set to {@code offset} bytes. */ + /** + * Offset is set to {@code offset} bytes. + */ SEEK_SET(1), - /** Add {@code offset} to the current location. */ + /** + * Add {@code offset} to the current location. + */ SEEK_CUR(2), - /** Set offset relative to end of file. */ + /** + * Set offset relative to end of file. + */ SEEK_END(3), - /** Move to the next data region at or after {@code offset}. */ + /** + * Move to the next data region at or after {@code offset}. + */ SEEK_DATA(4), - /** Move to the next hole at or after {@code offset}. */ + /** + * Move to the next hole at or after {@code offset}. + */ SEEK_HOLE(5); // The integer value representing the whence flag diff --git a/src/main/java/net/openhft/posix/internal/core/Jvm.java b/src/main/java/net/openhft/posix/internal/core/Jvm.java index e25b6bd..3ba8d2f 100644 --- a/src/main/java/net/openhft/posix/internal/core/Jvm.java +++ b/src/main/java/net/openhft/posix/internal/core/Jvm.java @@ -8,7 +8,9 @@ */ public final class Jvm { - /** Private constructor to prevent instantiation. */ + /** + * Private constructor to prevent instantiation. + */ private Jvm() { } diff --git a/src/main/java/net/openhft/posix/internal/core/OS.java b/src/main/java/net/openhft/posix/internal/core/OS.java index 12cb3ba..57a861c 100644 --- a/src/main/java/net/openhft/posix/internal/core/OS.java +++ b/src/main/java/net/openhft/posix/internal/core/OS.java @@ -12,7 +12,9 @@ public final class OS { */ public static final String OS_NAME = System.getProperty("os.name", "?"); - /** Private constructor to prevent instantiation. */ + /** + * Private constructor to prevent instantiation. + */ private OS() { } diff --git a/src/main/java/net/openhft/posix/internal/jnr/JNRPosixAPI.java b/src/main/java/net/openhft/posix/internal/jnr/JNRPosixAPI.java index 77c4a95..358da76 100644 --- a/src/main/java/net/openhft/posix/internal/jnr/JNRPosixAPI.java +++ b/src/main/java/net/openhft/posix/internal/jnr/JNRPosixAPI.java @@ -1,11 +1,11 @@ package net.openhft.posix.internal.jnr; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import jnr.constants.platform.Errno; import jnr.ffi.Platform; import jnr.ffi.Pointer; import jnr.ffi.Runtime; import jnr.ffi.provider.FFIProvider; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import net.openhft.posix.*; import net.openhft.posix.internal.UnsafeMemory; import net.openhft.posix.internal.core.Jvm; @@ -143,8 +143,8 @@ public long mmap(long addr, long length, int prot, int flags, int fd, long offse /** * Performs the mlock2 system call. * - * @param addr The address to lock. - * @param length The length of the memory to lock. + * @param addr The address to lock. + * @param length The length of the memory to lock. * @param lockOnFault Whether to lock on fault. * @return The result of the mlock2 system call. */ @@ -160,7 +160,7 @@ private int mlock2_(long addr, long length, boolean lockOnFault) { @Override public boolean mlock(long addr, long length) { - if(Jvm.isAzul()) { + if (Jvm.isAzul()) { LOGGER.warn("mlock called but ignored for Azul"); return true; // no-op on Azul, ignore } @@ -175,7 +175,7 @@ public boolean mlock(long addr, long length) { @Override public boolean mlock2(long addr, long length, boolean lockOnFault) { - if(Jvm.isAzul()) { + if (Jvm.isAzul()) { LOGGER.warn("mlock2 called but ignored for Azul"); return true; // no-op on Azul, ignore } @@ -291,17 +291,17 @@ public int fallocate(int fd, int mode, long offset, long length) { if (ret == 0) return ret; } catch (Throwable e) { - if(mode != 0) + if (mode != 0) throw e; } // if both fallocate attempts fail, then revert to posix_ftruncate when mode = 0 // NB: this use case uses cooperative locking to help close a small race window - if(mode == 0) { - try(FileLocker lock = new FileLocker(fd)) { + if (mode == 0) { + try (FileLocker lock = new FileLocker(fd)) { lock.ensureAcquired(); int ret = jnr.posix_fallocate(fd, offset, length); - if(ret == 0) + if (ret == 0) return ret; } catch (Throwable ignored) { } diff --git a/src/main/java/net/openhft/posix/internal/jnr/JNRPosixInterface.java b/src/main/java/net/openhft/posix/internal/jnr/JNRPosixInterface.java index f70a71f..39edc63 100644 --- a/src/main/java/net/openhft/posix/internal/jnr/JNRPosixInterface.java +++ b/src/main/java/net/openhft/posix/internal/jnr/JNRPosixInterface.java @@ -16,12 +16,15 @@ public interface JNRPosixInterface { long lseek(int fd, long offset, int whence); int lockf(int fd, int cmd, long len); + int flock(int fd, int operation); int ftruncate(int fd, long offset); int fallocate(int fd, int mode, long offset, long length); + int fallocate64(int fd, int mode, long offset, long length); + int posix_fallocate(int fd, long offset, long length); int close(int fd); @@ -57,6 +60,7 @@ public interface JNRPosixInterface { int clock_gettime(int clockId, long ptr); int mlock(long addr, long length); + int mlock2(long addr, long length, int flags); int mlockall(int flags); diff --git a/src/main/java/net/openhft/posix/internal/package-info.java b/src/main/java/net/openhft/posix/internal/package-info.java index 8f064f4..5b8c3a1 100644 --- a/src/main/java/net/openhft/posix/internal/package-info.java +++ b/src/main/java/net/openhft/posix/internal/package-info.java @@ -2,8 +2,8 @@ * This package and any and all sub-packages contains strictly internal classes for this Chronicle library. * Internal classes shall never be used directly. *

- * Specifically, the following actions (including, but not limited to) are not allowed - * on internal classes and packages: + * Specifically, the following actions (including, but not limited to) are not allowed + * on internal classes and packages: *

    *
  • Casting to
  • *
  • Reflection of any kind
  • diff --git a/src/main/java/net/openhft/posix/package-info.java b/src/main/java/net/openhft/posix/package-info.java index 4a3e216..914f0c8 100644 --- a/src/main/java/net/openhft/posix/package-info.java +++ b/src/main/java/net/openhft/posix/package-info.java @@ -9,14 +9,14 @@ * long ptr = posix.malloc(128); * posix.free(ptr); * - * + *

    * File example: *

      *     PosixAPI posix = PosixAPI.posix();
      *     int fd = posix.open("/tmp/data", OpenFlag.O_CREAT, 0644);
      *     posix.close(fd);
      * 
    - * + *

    * All helpers strive for zero heap allocation. */ package net.openhft.posix; diff --git a/src/test/java/net/openhft/posix/internal/PosixAPIHolderTest.java b/src/test/java/net/openhft/posix/internal/PosixAPIHolderTest.java index 80d3f43..78de337 100644 --- a/src/test/java/net/openhft/posix/internal/PosixAPIHolderTest.java +++ b/src/test/java/net/openhft/posix/internal/PosixAPIHolderTest.java @@ -15,7 +15,8 @@ import java.util.Map; import static net.openhft.posix.internal.UnsafeMemory.UNSAFE; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; /** * Tests around {@link PosixAPIHolder} to ensure the documented provider order diff --git a/src/test/java/net/openhft/posix/internal/jna/JNAPosixAPITest.java b/src/test/java/net/openhft/posix/internal/jna/JNAPosixAPITest.java index e35abbd..b38f716 100644 --- a/src/test/java/net/openhft/posix/internal/jna/JNAPosixAPITest.java +++ b/src/test/java/net/openhft/posix/internal/jna/JNAPosixAPITest.java @@ -1,16 +1,15 @@ package net.openhft.posix.internal.jna; import com.sun.jna.Pointer; -import org.junit.Test; +import net.openhft.posix.internal.ReflectionAccess; import org.junit.Assume; +import org.junit.Test; import java.lang.reflect.Field; import java.util.ArrayList; -import net.openhft.posix.internal.ReflectionAccess; - import static net.openhft.posix.internal.UnsafeMemory.UNSAFE; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; /** * Smoke tests for the JNA-backed provider to ensure constructor wiring and the diff --git a/src/test/java/net/openhft/posix/internal/jnr/JNRPosixAPINonNativeTest.java b/src/test/java/net/openhft/posix/internal/jnr/JNRPosixAPINonNativeTest.java index 2e6bc14..f89121b 100644 --- a/src/test/java/net/openhft/posix/internal/jnr/JNRPosixAPINonNativeTest.java +++ b/src/test/java/net/openhft/posix/internal/jnr/JNRPosixAPINonNativeTest.java @@ -17,11 +17,7 @@ import java.io.PrintStream; import java.lang.reflect.Field; import java.lang.reflect.Method; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Deque; -import java.util.List; +import java.util.*; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.IntSupplier; diff --git a/src/test/java/net/openhft/posix/internal/jnr/JNRPosixAPITest.java b/src/test/java/net/openhft/posix/internal/jnr/JNRPosixAPITest.java index ca1f52e..6113cf7 100644 --- a/src/test/java/net/openhft/posix/internal/jnr/JNRPosixAPITest.java +++ b/src/test/java/net/openhft/posix/internal/jnr/JNRPosixAPITest.java @@ -167,6 +167,7 @@ public void get_nprocs() { /** * Applies the given int supplier N times over N threads, adding each result to a set + * * @return - the size of the set */ int poolIntReduce(int N, Supplier r) throws InterruptedException { diff --git a/src/test/java/net/openhft/posix/internal/raw/RawPosixAPITest.java b/src/test/java/net/openhft/posix/internal/raw/RawPosixAPITest.java index 466fd28..9dcc427 100644 --- a/src/test/java/net/openhft/posix/internal/raw/RawPosixAPITest.java +++ b/src/test/java/net/openhft/posix/internal/raw/RawPosixAPITest.java @@ -3,7 +3,7 @@ import net.openhft.posix.MclFlag; import org.junit.Test; -import static org.junit.Assert.*; +import static org.junit.Assert.assertFalse; /** * Ensures {@link RawPosixAPI} can be subclassed without extra wiring and that From 090cb62ca4a06cc0b647a1c1db4f69eb101a889d Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Tue, 28 Oct 2025 11:00:56 +0000 Subject: [PATCH 12/17] Refine AsciiDoc formatting for consistency and clarity --- README.adoc | 12 +++---- src/main/adoc/decision-log.adoc | 56 ++++++++++++++++----------------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/README.adoc b/README.adoc index aeeeae5..5b27854 100644 --- a/README.adoc +++ b/README.adoc @@ -8,7 +8,7 @@ Peter Lawrey, 31/08/2021 :source-highlighter: rouge [abstract] -The _OpenHFT Posix_ module is a zero-GC, low-latency Java façade over a **portable subset of POSIX/Linux system calls**, with provider fall-back to JNR, JNA or raw/reflective variants. +The _OpenHFT Posix_ module is a zero-GC, low-latency Java façade over a *portable subset of POSIX/Linux system calls*, with provider fall-back to JNR, JNA or raw/reflective variants. Chronicle components rely on it for deterministic file-IO, memory-mapping and CPU-affinity operations that the JDK does not expose. Audience: Internal, Stability: Stable @@ -51,14 +51,14 @@ posix.munmap(addr, 4096); |=== | Provider | Native layer | Typical when… -| `JNRPosixAPI` | JNR-FFI | **Linux** & x86_64/ARM, fastest syscalls -| `WinJNRPosixAPI` | JNR-FFI | **Windows** equivalents (subset) +| `JNRPosixAPI` | JNR-FFI | *Linux* & x86_64/ARM, fastest syscalls +| `WinJNRPosixAPI` | JNR-FFI | *Windows* equivalents (subset) | `JNAPosixAPI` | JNA | exotic/legacy platforms | `RawPosixAPI` | Reflection | JVM ≥ 21 with `--add-opens` | `NoOpPosixAPI` | — | CI sandboxes / Graal native-image |=== -`-Dchronicle.posix.provider=` *provider-name* overrides the auto-choice. +`-Dchronicle.posix.provider=` _provider-name_ overrides the auto-choice. == Supported system calls @@ -75,7 +75,7 @@ posix.munmap(addr, 4096); | addr / 0 | `MAP_FAILED` → -1 sentinel | CPU-affinity -| `sched_*affinity*`, helpers `sched_setaffinity_as` … +| `sched__affinity_`, helpers `sched_setaffinity_as` … | 0 | portable bit-mask helpers | Timing @@ -93,7 +93,7 @@ mvn -q verify Environment variables: -* `POSIX_TEST_ALLOW_NATIVE` – set to *false* in CI to force `NoOpPosixAPI`. +* `POSIX_TEST_ALLOW_NATIVE` – set to _false_ in CI to force `NoOpPosixAPI`. * `POSIX_SYSLOG_LEVEL` – adjust logging noise during native provider load. == Troubleshooting diff --git a/src/main/adoc/decision-log.adoc b/src/main/adoc/decision-log.adoc index d491311..3343202 100644 --- a/src/main/adoc/decision-log.adoc +++ b/src/main/adoc/decision-log.adoc @@ -19,30 +19,30 @@ == ALL-DOC-001 Language & Character-Set Policy -*Context*:: +_Context_:: Chronicle teams span multiple regions. Mixed US/UK spelling and occasional UTF-8 artefacts caused build failures in binary wire-format tests and confused spell-checkers. -*Decision*:: -* Use **British English** spelling for prose/code except fixed US technical spellings (`synchronized`, `Serializable`). -* Restrict all source and documentation files to **ASCII-7** code-points (0-127). +_Decision_:: +* Use *British English* spelling for prose/code except fixed US technical spellings (`synchronized`, `Serializable`). +* Restrict all source and documentation files to *ASCII-7* code-points (0-127). -*Alternatives considered*:: -*Allow UTF-8 universally* – rejected: breaks zero-copy buffers that rely on MSB=0. -*Allow US spellings* – rejected: inconsistent with London HQ style guide. +_Alternatives considered_:: +_Allow UTF-8 universally_ – rejected: breaks zero-copy buffers that rely on MSB=0. +_Allow US spellings_ – rejected: inconsistent with London HQ style guide. -*Consequences*:: +_Consequences_:: * Tooling: CI linter added (`scripts/check-ascii.sh`). * On-boarding: documented in `AGENTS.md`; IDE templates updated. == POSIX-FN-002 Provider Fallback Order -*Context*:: +_Context_:: The Posix façade must run on Linux, macOS and Windows. Performance varies between native libraries (JNR, JNA) and reflection. Deterministic bootstrap order avoids surprises. -*Decision*:: +_Decision_:: At first successful class-load we try providers in this strict order: . `JNRPosixAPI` @@ -53,55 +53,55 @@ At first successful class-load we try providers in this strict order: Override with `-Dchronicle.posix.provider`. -*Alternatives*:: -*ServiceLoader scan* – too slow; non-deterministic. -*OS-hard-coded providers* – would fork the code-base per platform. +_Alternatives_:: +_ServiceLoader scan_ – too slow; non-deterministic. +_OS-hard-coded providers_ – would fork the code-base per platform. -*Consequences*:: +_Consequences_:: * Single-point selection in `PosixAPIHolder`. * Unit tests must set `POSIX_TEST_ALLOW_NATIVE=false` to force No-Op in CI. == ALL-DOC-002 Real-Time Documentation Loop -*Context*:: +_Context_:: Documentation drift led to mis-generated code from AI agents. The team adopted a “doc-first or doc-alongside” workflow but lacked formal guidance. -*Decision*:: -* Update AsciiDoc **in the same commit** as code/tests. +_Decision_:: +* Update AsciiDoc *in the same commit* as code/tests. * CI fails if a PR touches `src/main/java` without touching any `.adoc` _and_ `scripts/doc-drift-check.sh` flags potential drift. -*Consequences*:: +_Consequences_:: Quicker feedback, cleaner PR reviews, slightly longer dev cycle per change. == POSIX-NF-O-003 No-Op Provider Contract -*Context*:: +_Context_:: Some environments (Graal native-image, security-restricted CI) cannot load native libs. -*Decision*:: -`NoOpPosixAPI` compiles everywhere, returns **0** for safe no-ops, **-1** (or throws `PosixRuntimeException(errno=ENOSYS)`) where silent failure may lose data. +_Decision_:: +`NoOpPosixAPI` compiles everywhere, returns *0* for safe no-ops, *-1* (or throws `PosixRuntimeException(errno=ENOSYS)`) where silent failure may lose data. == POSIX-NF-O-006 Affinity Mask Sizing Policy -*Context*:: +_Context_:: Regression POSIX-2025-05 observed incorrect CPU pinning on hosts with more than thirty-two logical processors. Bit masks were truncated because offsets used byte indices rather than `int` words and mask buffers were only sized for a single `long`. -*Decision*:: +_Decision_:: * Introduce `CpuSetUtil` to normalise mask sizing (round up to whole `Long.BYTES` blocks) and byte packing. * Apply the helper in `PosixJNAAffinity` and export the same contract to embedding projects (Java Thread Affinity). * Add pure-Java tests that simulate > 64-core schedulers. -*Alternatives*:: -*Hard-code buffer sizes per architecture* – rejected because it risks further drift when porting to new platforms. +_Alternatives_:: +_Hard-code buffer sizes per architecture_ – rejected because it risks further drift when porting to new platforms. -*Consequences*:: +_Consequences_:: * Mask handling now covers arbitrary CPU counts permitted by the kernel. * Shared utility simplifies future native integrations. -`lastError()` fixed at **0**. +`lastError()` fixed at *0*. -*Consequences*:: +_Consequences_:: Baseline behaviour predictable; integration tests must stub filesystem side-effects. From ca5ea9a5319cca0faee17e5c5f0533d4e80ee758 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Tue, 28 Oct 2025 11:00:56 +0000 Subject: [PATCH 13/17] Add non-native coverage tests and tighten code-review gates --- pom.xml | 4 ++-- .../net/openhft/posix/internal/jnr/WinJNRPosixAPITest.java | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 03643ce..3178d23 100644 --- a/pom.xml +++ b/pom.xml @@ -18,8 +18,8 @@ 3.6.0 - 10.26.1 - 4.9.8.1 + 8.45.1 + 4.8.6.6 1.14.0 3.28.0 0.8.14 diff --git a/src/test/java/net/openhft/posix/internal/jnr/WinJNRPosixAPITest.java b/src/test/java/net/openhft/posix/internal/jnr/WinJNRPosixAPITest.java index 5288613..f568c81 100644 --- a/src/test/java/net/openhft/posix/internal/jnr/WinJNRPosixAPITest.java +++ b/src/test/java/net/openhft/posix/internal/jnr/WinJNRPosixAPITest.java @@ -5,6 +5,7 @@ import java.lang.reflect.Field; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import static org.junit.Assert.*; @@ -54,8 +55,8 @@ public void delegatesToNativeInterfaces() throws Exception { assertEquals(Runtime.getRuntime().availableProcessors(), api.get_nprocs_conf()); assertEquals(api.get_nprocs_conf(), api.get_nprocs()); - assertEquals(List.of("open", "lseek", "read", "write", "close", "pid", "strerror"), win.calls); - assertEquals(List.of("tid"), kernel.calls); + assertEquals(Arrays.asList("open", "lseek", "read", "write", "close", "pid", "strerror"), win.calls); + assertEquals(Arrays.asList("tid"), kernel.calls); } private static void setField(Object target, String fieldName, Object value) throws Exception { From 7e78c578ac1ac4ce2bec522245ea93a8e5b210ea Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Tue, 28 Oct 2025 14:12:55 +0000 Subject: [PATCH 14/17] Update character set policy from ASCII-7 to ISO-8859-1 in documentation --- AGENTS.md | 6 +++--- src/main/adoc/decision-log.adoc | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 620cde4..7c69218 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,8 +11,8 @@ LLM-based agents can accelerate development only if they respect our house rules | Requirement | Rationale | |--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------| | **British English** spelling (`organisation`, `licence`, *not* `organization`, `license`) except technical US spellings like `synchronized` | Keeps wording consistent with Chronicle's London HQ and existing docs. See the University of Oxford style guide for reference. | -| **ASCII-7 only** (code-points 0-127). Avoid smart quotes, non-breaking spaces and accented characters. | ASCII-7 survives every toolchain Chronicle uses, incl. low-latency binary wire formats that expect the 8th bit to be 0. | -| If a symbol is not available in ASCII-7, use a textual form such as `micro-second`, `>=`, `:alpha:`, `:yes:`. This is the preferred approach and Unicode must not be inserted. | Extended or '8-bit ASCII' variants are *not* portable and are therefore disallowed. | +| **ISO-8859-1** (code-points 0-255). Avoid smart quotes, non-breaking spaces and accented characters. | ISO-8859-1 survives every toolchain Chronicle uses, incl. low-latency binary wire formats that expect the 8th bit to be 0. | +| If a symbol is not available in ISO-8859-1, use a textual form such as `micro-second`, `>=`, `:alpha:`, `:yes:`. This is the preferred approach and Unicode must not be inserted. | Extended or '8-bit ASCII' variants are *not* portable and are therefore disallowed. | ## Javadoc guidelines @@ -92,7 +92,7 @@ This tight loop informs the AI accurately and creates immediate clarity for all When using AI agents to assist with development, please adhere to the following guidelines: * **Respect the Language & Character-set Policy**: Ensure all AI-generated content follows the British English and - ASCII-7 guidelines outlined above. + ISO-8859-1 guidelines outlined above. Focus on Clarity: AI-generated documentation should be clear and concise and add value beyond what is already present in the code or existing documentation. * **Avoid Redundancy**: Do not generate content that duplicates existing documentation or code comments unless it diff --git a/src/main/adoc/decision-log.adoc b/src/main/adoc/decision-log.adoc index 3343202..9a6ba60 100644 --- a/src/main/adoc/decision-log.adoc +++ b/src/main/adoc/decision-log.adoc @@ -25,7 +25,7 @@ Mixed US/UK spelling and occasional UTF-8 artefacts caused build failures in bin _Decision_:: * Use *British English* spelling for prose/code except fixed US technical spellings (`synchronized`, `Serializable`). -* Restrict all source and documentation files to *ASCII-7* code-points (0-127). +* Restrict all source and documentation files to *ISO-8859-1* code-points (0-255). _Alternatives considered_:: _Allow UTF-8 universally_ – rejected: breaks zero-copy buffers that rely on MSB=0. From 1f4c2bc7caf6a0271edac1881de76a1fda726f5c Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Wed, 29 Oct 2025 13:33:09 +0000 Subject: [PATCH 15/17] Refactor SpotBugs exclusions and remove deprecated @SuppressFBWarnings annotations --- src/main/java/net/openhft/posix/PosixAPI.java | 3 --- src/main/java/net/openhft/posix/ProcMaps.java | 3 --- src/main/java/net/openhft/posix/internal/jnr/JNRPosixAPI.java | 3 --- 3 files changed, 9 deletions(-) diff --git a/src/main/java/net/openhft/posix/PosixAPI.java b/src/main/java/net/openhft/posix/PosixAPI.java index 1a31be4..fb4a126 100644 --- a/src/main/java/net/openhft/posix/PosixAPI.java +++ b/src/main/java/net/openhft/posix/PosixAPI.java @@ -1,6 +1,4 @@ package net.openhft.posix; - -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import net.openhft.posix.internal.PosixAPIHolder; import net.openhft.posix.internal.UnsafeMemory; @@ -262,7 +260,6 @@ default int open(CharSequence path, OpenFlag flags, int perm) { * @return The disk usage in bytes. * @throws IOException If an I/O error occurs. */ - @SuppressFBWarnings(value = "COMMAND_INJECTION", justification = "POSIX-SEC-204: ProcessBuilder uses fixed argv without shell expansion") default long du(String filename) throws IOException { ProcessBuilder pb = new ProcessBuilder("du", filename); pb.redirectErrorStream(true); diff --git a/src/main/java/net/openhft/posix/ProcMaps.java b/src/main/java/net/openhft/posix/ProcMaps.java index 473865e..0b69d94 100644 --- a/src/main/java/net/openhft/posix/ProcMaps.java +++ b/src/main/java/net/openhft/posix/ProcMaps.java @@ -1,7 +1,5 @@ package net.openhft.posix; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; - import java.io.BufferedReader; import java.io.FileInputStream; import java.io.IOException; @@ -23,7 +21,6 @@ * * @see proc(5) */ -@SuppressFBWarnings(value = "PATH_TRAVERSAL_IN", justification = "POSIX-OPS-102: reads kernel-managed /proc//maps entries only") public final class ProcMaps { // A list to hold the memory mappings private final List mappingList = new ArrayList<>(); diff --git a/src/main/java/net/openhft/posix/internal/jnr/JNRPosixAPI.java b/src/main/java/net/openhft/posix/internal/jnr/JNRPosixAPI.java index 358da76..ff7bb09 100644 --- a/src/main/java/net/openhft/posix/internal/jnr/JNRPosixAPI.java +++ b/src/main/java/net/openhft/posix/internal/jnr/JNRPosixAPI.java @@ -1,6 +1,4 @@ package net.openhft.posix.internal.jnr; - -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import jnr.constants.platform.Errno; import jnr.ffi.Platform; import jnr.ffi.Pointer; @@ -25,7 +23,6 @@ * hard-coded numbers chosen for common architectures. If the kernel does not * recognise a number the call gracefully falls back to the available wrapper.

    */ -@SuppressFBWarnings(value = "THROWS_METHOD_THROWS_RUNTIMEEXCEPTION", justification = "POSIX-API-117: propagate errno via RuntimeException to honour existing interface") public final class JNRPosixAPI implements PosixAPI { private static final Logger LOGGER = LoggerFactory.getLogger(JNRPosixAPI.class); From 7ce19f3779c0ec3ad85e351388a64ab0187234ac Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Wed, 29 Oct 2025 15:47:33 +0000 Subject: [PATCH 16/17] Remove unused SpotBugs annotations dependency from pom.xml --- pom.xml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pom.xml b/pom.xml index 3178d23..701c092 100644 --- a/pom.xml +++ b/pom.xml @@ -45,12 +45,6 @@ org.slf4j slf4j-api - - com.github.spotbugs - spotbugs-annotations - 4.8.6 - provided - net.java.dev.jna From ae4f5eb8cfdc894c23d50e0782ac04353fabbfac Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Thu, 30 Oct 2025 11:00:25 +0000 Subject: [PATCH 17/17] Move Checkstyle config under src/main/config --- pom.xml | 2 +- src/main/config/checkstyle.xml | 210 +++++++++++++++++++++++++++++++++ 2 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 src/main/config/checkstyle.xml diff --git a/pom.xml b/pom.xml index 701c092..3ca485c 100644 --- a/pom.xml +++ b/pom.xml @@ -165,7 +165,7 @@ - net/openhft/quality/checkstyle/checkstyle.xml + src/main/config/checkstyle.xml true true warning diff --git a/src/main/config/checkstyle.xml b/src/main/config/checkstyle.xml new file mode 100644 index 0000000..844dd90 --- /dev/null +++ b/src/main/config/checkstyle.xml @@ -0,0 +1,210 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +