From 611790a6ce77d8687f95bf167209db8e92258bc6 Mon Sep 17 00:00:00 2001 From: mayankkunwar Date: Wed, 13 May 2026 16:22:15 +0100 Subject: [PATCH] ARTEMIS-5972: Replace JNI with Panama Foreign Function & Memory (FFM) API for Journal Native Layer --- artemis-ffm/etc/checkstyle-suppressions.xml | 33 + artemis-ffm/etc/checkstyle.xml | 136 +++ artemis-ffm/pom.xml | 238 ++++ .../artemis/nativo/jlibaio/LibaioContext.java | 508 ++++++++ .../artemis/nativo/jlibaio/LibaioFile.java | 146 +++ .../artemis/nativo/jlibaio/NativeLogger.java | 34 + .../artemis/nativo/jlibaio/SubmitInfo.java | 25 + .../artemis/nativo/jlibaio/ffm/AIORing.java | 102 ++ .../artemis/nativo/jlibaio/ffm/Constants.java | 63 + .../nativo/jlibaio/ffm/FFMHandles.java | 110 ++ .../nativo/jlibaio/ffm/FFMNativeHelper.java | 1063 +++++++++++++++++ .../artemis/nativo/jlibaio/ffm/IOCBInit.java | 128 ++ .../artemis/nativo/jlibaio/ffm/IOControl.java | 200 ++++ .../artemis/nativo/jlibaio/ffm/IOEvent.java | 50 + .../nativo/jlibaio/ffm/ReleaseCallback.java | 23 + .../nativo/jlibaio/ffm/SharedContext.java | 48 + .../artemis/nativo/jlibaio/ffm/Stat.java | 48 + .../artemis/nativo/jlibaio/package-info.java | 24 + .../nativo/jlibaio/util/CallbackCache.java | 76 ++ .../jlibaio/test/CallbackCachelTest.java | 101 ++ .../nativo/jlibaio/test/LibaioStressTest.java | 272 +++++ .../nativo/jlibaio/test/LibaioTest.java | 790 ++++++++++++ .../nativo/jlibaio/test/LoadedTest.java | 36 + .../jlibaio/test/OpenCloseContextTest.java | 186 +++ .../nativo/jlibaio/test/ReusableLatch.java | 135 +++ .../jlibaio/test/ffm/FFMNativeHelperTest.java | 442 +++++++ .../jlibaio/test/ffm/IOCBLayoutTest.java | 52 + .../jlibaio/test/ffm/IOControlTest.java | 302 +++++ .../jlibaio/test/jmh/AioCompareBenchmark.java | 193 +++ .../jlibaio/test/jmh/BenchmarkRunner.java | 28 + pom.xml | 10 + 31 files changed, 5602 insertions(+) create mode 100644 artemis-ffm/etc/checkstyle-suppressions.xml create mode 100644 artemis-ffm/etc/checkstyle.xml create mode 100644 artemis-ffm/pom.xml create mode 100644 artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/LibaioContext.java create mode 100644 artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/LibaioFile.java create mode 100644 artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/NativeLogger.java create mode 100644 artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/SubmitInfo.java create mode 100644 artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/ffm/AIORing.java create mode 100644 artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/ffm/Constants.java create mode 100644 artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/ffm/FFMHandles.java create mode 100644 artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/ffm/FFMNativeHelper.java create mode 100644 artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/ffm/IOCBInit.java create mode 100644 artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/ffm/IOControl.java create mode 100644 artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/ffm/IOEvent.java create mode 100644 artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/ffm/ReleaseCallback.java create mode 100644 artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/ffm/SharedContext.java create mode 100644 artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/ffm/Stat.java create mode 100644 artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/package-info.java create mode 100644 artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/util/CallbackCache.java create mode 100644 artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/CallbackCachelTest.java create mode 100644 artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/LibaioStressTest.java create mode 100644 artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/LibaioTest.java create mode 100644 artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/LoadedTest.java create mode 100644 artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/OpenCloseContextTest.java create mode 100644 artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/ReusableLatch.java create mode 100644 artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/ffm/FFMNativeHelperTest.java create mode 100644 artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/ffm/IOCBLayoutTest.java create mode 100644 artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/ffm/IOControlTest.java create mode 100644 artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/jmh/AioCompareBenchmark.java create mode 100644 artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/jmh/BenchmarkRunner.java diff --git a/artemis-ffm/etc/checkstyle-suppressions.xml b/artemis-ffm/etc/checkstyle-suppressions.xml new file mode 100644 index 00000000000..55aca3168f2 --- /dev/null +++ b/artemis-ffm/etc/checkstyle-suppressions.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + diff --git a/artemis-ffm/etc/checkstyle.xml b/artemis-ffm/etc/checkstyle.xml new file mode 100644 index 00000000000..0bc3e2f05c1 --- /dev/null +++ b/artemis-ffm/etc/checkstyle.xml @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/artemis-ffm/pom.xml b/artemis-ffm/pom.xml new file mode 100644 index 00000000000..0e918dc65c3 --- /dev/null +++ b/artemis-ffm/pom.xml @@ -0,0 +1,238 @@ + + + 4.0.0 + + org.apache.artemis + artemis-project + 2.54.0-SNAPSHOT + ../artemis-pom/pom.xml + + + artemis-ffm + + + 4.13.2 + 1.37 + + 5000 + + UTF-8 + UTF-8 + + 22 + 22 + 22 + + + -Dtest.stress.time=${test.stress.time} --enable-native-access=ALL-UNNAMED + + + + + + org.slf4j + slf4j-api + ${slf4j.version} + provided + + + + + junit + junit + ${junit.version} + test + + + + org.junit.vintage + junit-vintage-engine + 5.10.2 + test + + + org.junit.jupiter + junit-jupiter + 5.10.2 + test + + + org.openjdk.jmh + jmh-core + ${jmh.version} + test + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} + test + + + org.slf4j + slf4j-simple + ${slf4j.version} + test + + + + + + + linux + + + unix + Linux + + + + + non-linux + + + mac + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + + + + + + windows + + + windows + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + + + + + + no-libaio + + + /usr/lib64/libaio.so.1 + + + + true + false + true + + + + + org.apache.maven.plugins + maven-surefire-plugin + + ${skipTests} + + + + org.apache.maven.plugins + maven-compiler-plugin + + + default-compile + compile + compile + + + + ${native.skip} + + + + + + + + + + + ${basedir}/target/output/ + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + default-compile + + generate-sources + + compile + + + + -h + ./target/include + + + + + + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} + + + 22 + 22 + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + artemis.ffm + + + + + + + \ No newline at end of file diff --git a/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/LibaioContext.java b/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/LibaioContext.java new file mode 100644 index 00000000000..7163309c865 --- /dev/null +++ b/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/LibaioContext.java @@ -0,0 +1,508 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.artemis.nativo.jlibaio; + +import org.apache.artemis.nativo.jlibaio.ffm.FFMNativeHelper; +import org.apache.artemis.nativo.jlibaio.ffm.IOControl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.lang.foreign.MemorySegment; +import java.nio.ByteBuffer; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +/** + * This class is used as an aggregator for the {@link LibaioFile}. + *
+ * It holds native data, and it will share a libaio queue that can be used by multiple files. + *
+ * You need to use the poll methods to read the result of write and read submissions. + *
+ * You also need to use the special buffer created by {@link LibaioFile} as you need special alignments + * when dealing with O_DIRECT files. + *
+ * A Single controller can server multiple files. There's no need to create one controller per file. + *
+ * Interesting reading for this. + */ +public class LibaioContext implements Closeable { + /* Notice: After making changes to the native interface, mvn 'generate-sources' must occur at least once to generate the updated include file. + This is because the maven compiler plugin is the one generating org_apache_activemq_artemis_native_jlibaio_LibaioContext.h + So that file needs to be updated before Cmake comes along to compile the module. + This normally happens as needed in the regular mvn build, so if you use e.g 'mvn clean install -Ppodman' then no extra step is needed, + specific attention is only required if you e.g run the build scripts directly yourself. */ + + private static final Logger logger = LoggerFactory.getLogger(LibaioContext.class); + + private static final AtomicLong totalMaxIO = new AtomicLong(0); + + /** + * The Native layer will look at this version. + */ + private static final int EXPECTED_NATIVE_VERSION = 200; + + private static boolean loaded = true; + + private static final AtomicBoolean shuttingDown = new AtomicBoolean(false); + + private static final AtomicInteger contexts = new AtomicInteger(0); + + public static boolean isLoaded() { + return loaded; + } + + private static boolean loadLibrary(final String name) { + try { + logger.debug("Loading {}", name); + System.loadLibrary(name); + if (getNativeVersion() != EXPECTED_NATIVE_VERSION) { + NativeLogger.incompatibleNativeLibrary(); + return false; + } else { + return true; + } + } catch (Throwable e) { + logger.debug(name + " -> not possible to load native library", e); + return false; + } + + } + + static { + if (System.getProperty("org.apache.activemq.artemis.native.jlibaio.FORCE_SYSCALL") != null) { + LibaioContext.setForceSyscall(true); + } + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + shuttingDown.set(true); + checkShutdown(); + } + }); + } + + private static void checkShutdown() { + if (contexts.get() == 0 && shuttingDown.get()) { + shutdownHook(); + } + } + + private static void shutdownHook() { + FFMNativeHelper.shutdownHook(); + } + + public static void setForceSyscall(boolean value) { + FFMNativeHelper.setForceSyscall(value); + } + + /** + * The system may choose to set this if a failing condition happened inside the code. + */ + public static boolean isForceSyscall() { + return FFMNativeHelper.isForceSyscall(); + } + + /** + * This is used to validate leaks on tests. + * + * @return the number of allocated aio, to be used on test checks. + */ + public static long getTotalMaxIO() { + return totalMaxIO.get(); + } + + /** + * It will reset all the positions on the buffer to 0, using memset. + * + * @param buffer a native buffer. + * s + */ + public void memsetBuffer(ByteBuffer buffer) { + memsetBuffer(buffer, buffer.limit()); + } + + /** + * This is used on tests validating for leaks. + */ + public static void resetMaxAIO() { + totalMaxIO.set(0); + } + + /** + * the native ioContext including the structure created. + */ + private final IOControl ioContext; + + private final AtomicBoolean closed = new AtomicBoolean(false); + + final Semaphore ioSpace; + + final int queueSize; + + final boolean useFdatasync; + + final FFMNativeHelper ffmNativeHelper; + + /** + * The queue size here will use resources defined on the kernel parameter + * fs.aio-max-nr . + * + * @param queueSize the size to be initialize on libaio + * io_queue_init which can't be higher than /proc/sys/fs/aio-max-nr. + * @param useSemaphore should block on a semaphore avoiding using more submits than what's available. + * @param useFdatasync should use fdatasync before calling callbacks. + */ + public LibaioContext(int queueSize, boolean useSemaphore, boolean useFdatasync) { + try { + this.ffmNativeHelper = new FFMNativeHelper<>(this::releaseSemaphore); + contexts.incrementAndGet(); + this.ioContext = newContext(queueSize); + this.useFdatasync = useFdatasync; + } catch (Exception e) { + throw e; + } + this.queueSize = queueSize; + totalMaxIO.addAndGet(queueSize); + if (useSemaphore) { + this.ioSpace = new Semaphore(queueSize); + } else { + this.ioSpace = null; + } + } + + /** + * Documented at {@link LibaioFile#write(long, int, ByteBuffer, SubmitInfo)} + * + * @param fd the file descriptor + * @param position the write position + * @param size number of bytes to use + * @param bufferWrite the native buffer + * @param callback a callback + * @throws IOException in case of error + */ + public void submitWrite(int fd, + long position, + int size, + ByteBuffer bufferWrite, + Callback callback) throws IOException { + if (closed.get()) { + throw new IOException("Libaio Context is closed!"); + } + try { + if (ioSpace != null) { + ioSpace.acquire(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException(e.getMessage(), e); + } + submitWrite(fd, this.ioContext, position, size, bufferWrite, callback); + } + + public void submitRead(int fd, + long position, + int size, + ByteBuffer bufferWrite, + Callback callback) throws IOException { + if (closed.get()) { + throw new IOException("Libaio Context is closed!"); + } + try { + if (ioSpace != null) { + ioSpace.acquire(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException(e.getMessage(), e); + } + submitRead(fd, this.ioContext, position, size, bufferWrite, callback); + } + + /** + * This is used to close the libaio queues and cleanup the native data used. + *
+ * It is unsafe to close the controller while you have pending writes or files open as + * this could cause core dumps or VM crashes. + */ + @Override + public void close() { + if (!closed.getAndSet(true)) { + + if (ioSpace != null) { + try { + ioSpace.tryAcquire(queueSize, 10, TimeUnit.SECONDS); + } catch (Exception e) { + logger.warn(e.getMessage(), e); + } + } + totalMaxIO.addAndGet(-queueSize); + + if (ioContext != null) { + deleteContext(ioContext); + } + contexts.decrementAndGet(); + checkShutdown(); + } + } + + /** + * It will open a file. If you set the direct flag = false then you won't need to use the special buffer. + * Notice: This will create an empty file if the file doesn't already exist. + * + * @param file the file to be open. + * @param direct will set ODIRECT. + * @return It will return a LibaioFile instance. + * @throws IOException in case of error. + */ + public LibaioFile openFile(File file, boolean direct) throws IOException { + return openFile(file.getPath(), direct); + } + + /** + * It will open a file. If you set the direct flag = false then you won't need to use the special buffer. + * Notice: This will create an empty file if the file doesn't already exist. + * + * @param file the file to be open. + * @param direct should use O_DIRECT when opening the file. + * @return a new open file. + * @throws IOException in case of error. + */ + public LibaioFile openFile(String file, boolean direct) throws IOException { + checkNotNull(file, "path"); + checkNotNull(ioContext, "IOContext"); + + // note: the native layer will throw an IOException in case of errors + int res = LibaioContext.open(file, direct); + + return new LibaioFile<>(res, this); + } + + /** + * It will open a file disassociated with any sort of factory. + * This is useful when you won't use reading / writing through libaio like locking files. + * + * @param file a file name + * @param direct will use O_DIRECT + * @return a new file + * @throws IOException in case of error. + */ + public static LibaioFile openControlFile(String file, boolean direct) throws IOException { + checkNotNull(file, "path"); + + // note: the native layer will throw an IOException in case of errors + int res = LibaioContext.open(file, direct); + + return new LibaioFile<>(res, null); + } + + /** + * Checks that the given argument is not null. If it is, throws {@link NullPointerException}. + * Otherwise, returns the argument. + */ + private static T checkNotNull(T arg, String text) { + if (arg == null) { + throw new NullPointerException(text); + } + return arg; + } + + /** + * It will poll the libaio queue for results. It should block until min is reached + * Results are placed on the callback. + *
+ * This shouldn't be called concurrently. You should provide your own synchronization if you need more than one + * Thread polling for any reason. + *
+ * Notice that the native layer will invoke {@link SubmitInfo#onError(int, String)} in case of failures, + * but it won't call done method for you. + * + * @param callbacks area to receive the callbacks passed on submission.The size of this callback has to + * be greater than the parameter max. + * @param min the minimum number of elements to receive. It will block until this is achieved. + * @param max The maximum number of elements to receive. + * @return Number of callbacks returned. + * @see LibaioFile#write(long, int, ByteBuffer, SubmitInfo) + * @see LibaioFile#read(long, int, ByteBuffer, SubmitInfo) + */ + public int poll(Callback[] callbacks, int min, int max) { + int released = poll(ioContext, callbacks, min, max); + if (ioSpace != null) { + if (released > 0) { + ioSpace.release(released); + } + } + return released; + } + + /** + * It will start polling and will keep doing until the context is closed. + * This will call callbacks on {@link SubmitInfo#onError(int, String)} and + * {@link SubmitInfo#done()}. + * In case of error, both {@link SubmitInfo#onError(int, String)} and + * {@link SubmitInfo#done()} are called. + */ + public void poll() { + if (!closed.get()) { + blockedPoll(ioContext, useFdatasync); + } + } + + private void releaseSemaphore() { + if (ioSpace != null) { + ioSpace.release(); + } + } + + /** + * This is the queue for libaio, initialized with queueSize. + */ + private IOControl newContext(int queueSize) { + return this.ffmNativeHelper.newContext(queueSize); + } + + /** + * Internal method to be used when closing the controller. + */ + private void deleteContext(IOControl ioControl) { + this.ffmNativeHelper.deleteContext(ioControl); + } + + /** + * it will return a file descriptor. + * + * @param path the file name. + * @param direct translates as O_DIRECT On open + * @return a fd from open C call. + */ + public static int open(String path, boolean direct) throws IOException { + return FFMNativeHelper.open(path, direct); + } + + public static void close(int fd) throws IOException { + FFMNativeHelper.close(fd); + } + + /** + * Buffers for O_DIRECT need to use posix_memalign. + *
+ * Documented at {@link LibaioFile#newBuffer(int)}. + * + * @param size needs to be % alignment + * @param alignment the alignment used at the dispositive + * @return a new native buffer used with posix_memalign + */ + public static MemorySegment newAlignedBuffer(int size, int alignment) { + return FFMNativeHelper.newAlignedBuffer(size, alignment); + } + + /** + * This will call posix free to release the inner buffer allocated at {@link #newAlignedBuffer(int, int)}. + * + * @param buffer a native buffer allocated with {@link #newAlignedBuffer(int, int)}. + */ + public static void freeBuffer(MemorySegment buffer) { + FFMNativeHelper.freeBuffer(buffer); + } + + /** + * Documented at {@link LibaioFile#write(long, int, ByteBuffer, SubmitInfo)}. + */ + void submitWrite(int fd, + IOControl ioControl, + long position, + int size, + ByteBuffer bufferWrite, + Callback callback) throws IOException { + this.ffmNativeHelper.submitWrite(fd, ioControl, position, size, bufferWrite, callback); + } + + /** + * Documented at {@link LibaioFile#read(long, int, ByteBuffer, SubmitInfo)}. + */ + void submitRead(int fd, + IOControl ioControl, + long position, + int size, + ByteBuffer bufferWrite, + Callback callback) throws IOException { + this.ffmNativeHelper.submitRead(fd, ioControl, position, size, bufferWrite, callback); + } + + /** + * Note: this shouldn't be done concurrently. + * This method will block until the min condition is satisfied on the poll. + *

+ * The callbacks will include the original callback sent at submit (read or write). + */ + int poll(IOControl ioControl, Callback[] callbacks, int min, int max) { + return this.ffmNativeHelper.poll(ioControl, callbacks, min, max); + } + + /** + * This method will block as long as the context is open. + */ + void blockedPoll(IOControl ioControl, boolean useFdatasync) { + this.ffmNativeHelper.blockedPoll(ioControl, useFdatasync); + } + + static int getNativeVersion() { + return FFMNativeHelper.getNativeVersion(); + } + + public static boolean lock(int fd) { + return FFMNativeHelper.lock(fd); + } + + public static void memsetBuffer(ByteBuffer buffer, int size) { + FFMNativeHelper.memsetBuffer(buffer, size); + } + + static long getSize(int fd) throws IOException { + return FFMNativeHelper.getSize(fd); + } + + static int getBlockSizeFD(int fd) throws IOException { + return FFMNativeHelper.getBlockSizeFD(fd); + } + + public static int getBlockSize(File path) throws IOException { + return getBlockSize(path.getAbsolutePath()); + } + + public static int getBlockSize(String path) throws IOException { + return FFMNativeHelper.getBlockSize(path); + } + + static void fallocate(int fd, long size) throws IOException { + FFMNativeHelper.fallocate(fd, size); + } + + static void fill(int fd, int alignment, long size) throws IOException { + FFMNativeHelper.fill(fd, alignment, size); + } + + static void writeInternal(int fd, long position, long size, ByteBuffer bufferWrite) throws IOException { + FFMNativeHelper.writeInternal(fd, position, size, bufferWrite); + } +} diff --git a/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/LibaioFile.java b/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/LibaioFile.java new file mode 100644 index 00000000000..bf7a30cc428 --- /dev/null +++ b/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/LibaioFile.java @@ -0,0 +1,146 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.artemis.nativo.jlibaio; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.lang.foreign.MemorySegment; +import java.nio.ByteBuffer; +import java.util.Objects; + +/** + * This is an extension to use libaio. + */ +public final class LibaioFile implements AutoCloseable { + + private static final Logger logger = LoggerFactory.getLogger(LibaioFile.class); + + protected boolean open; + /** + * This represents a structure allocated on the native + * this is a io_context_t + */ + final LibaioContext ctx; + + private int fd; + + LibaioFile(int fd, LibaioContext ctx) { + this.ctx = ctx; + this.fd = fd; + } + + public int getBlockSize() throws IOException { + return LibaioContext.getBlockSizeFD(fd); + } + + public boolean lock() { + return LibaioContext.lock(fd); + } + + @Override + public void close() throws IOException { + open = false; + LibaioContext.close(fd); + } + + /** + * @return The size of the file. + */ + public long getSize() throws IOException { + return LibaioContext.getSize(fd); + } + + /** + * It will submit a write to the queue. The callback sent here will be received on the + * {@link LibaioContext#poll(SubmitInfo[], int, int)} + * In case of the libaio queue is full (e.g. returning E_AGAIN) this method will return false. + *
+ * Notice: this won't hold a global reference on buffer, callback should hold a reference towards bufferWrite. + * And don't free the buffer until the callback was called as this could crash the VM. + * + * @param position The position on the file to write. Notice this has to be a multiple of 512. + * @param size The size of the buffer to use while writing. + * @param buffer if you are using O_DIRECT the buffer here needs to be allocated by {@link #newBuffer(int)}. + * @param callback A callback to be returned on the poll method. + * @throws IOException in case of error + */ + public void write(long position, int size, ByteBuffer buffer, Callback callback) throws IOException { + Objects.requireNonNull(callback, "Callback cannot be null"); + ctx.submitWrite(fd, position, size, buffer, callback); + } + + /** + * It will submit a read to the queue. The callback sent here will be received on the + * {@link LibaioContext#poll(SubmitInfo[], int, int)}. + * In case of the libaio queue is full (e.g. returning E_AGAIN) this method will return false. + *
+ * Notice: this won't hold a global reference on buffer, callback should hold a reference towards bufferWrite. + * And don't free the buffer until the callback was called as this could crash the VM. + * * + * + * @param position The position on the file to read. Notice this has to be a multiple of 512. + * @param size The size of the buffer to use while reading. + * @param buffer if you are using O_DIRECT the buffer here needs to be allocated by {@link #newBuffer(int)}. + * @param callback A callback to be returned on the poll method. + * @throws IOException in case of error + * @see LibaioContext#poll(SubmitInfo[], int, int) + */ + public void read(long position, int size, ByteBuffer buffer, Callback callback) throws IOException { + Objects.requireNonNull(callback, "Callback cannot be null"); + ctx.submitRead(fd, position, size, buffer, callback); + } + + /** + * It will allocate a buffer to be used on libaio operations. + * Buffers here are allocated with posix_memalign. + *
+ * You need to explicitly free the buffer created from here using the + * {@link LibaioContext#freeBuffer(MemorySegment)}. + * + * @param size the size of the buffer. + * @return the buffer allocated. + */ + public MemorySegment newBuffer(int size) { + return LibaioContext.newAlignedBuffer(size, 4 * 1024); + } + + /** + * It will preallocate the file with a given size. + * + * @param size number of bytes to be filled on the file + */ + public void fill(int alignment, long size) throws IOException { + try { + LibaioContext.fill(fd, alignment, size); + } catch (OutOfMemoryError e) { + logger.warn("Did not have enough memory to allocate " + size + " bytes in memory while filling the file, using simple fallocate"); + LibaioContext.fallocate(fd, size); + } + } + + /** + * It will use fallocate to initialize a file. + * + * @param size number of bytes to be filled on the file + */ + public void fallocate(long size) throws IOException { + LibaioContext.fallocate(fd, size); + } + +} diff --git a/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/NativeLogger.java b/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/NativeLogger.java new file mode 100644 index 00000000000..d45585fc4a2 --- /dev/null +++ b/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/NativeLogger.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.artemis.nativo.jlibaio; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class NativeLogger { + + private static final Logger logger = LoggerFactory.getLogger(NativeLogger.class); + + public static final String PROJECT_PREFIX = "jlibaio"; + + private static final int DIFFERENT_VERSION_ID = 163001; + private static final String DIFFERENT_VERSION = PROJECT_PREFIX + DIFFERENT_VERSION_ID + " You have a native library with a different version than expected"; + + public static final void incompatibleNativeLibrary() { + logger.warn(DIFFERENT_VERSION); + } +} diff --git a/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/SubmitInfo.java b/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/SubmitInfo.java new file mode 100644 index 00000000000..c41aeaf1af9 --- /dev/null +++ b/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/SubmitInfo.java @@ -0,0 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.artemis.nativo.jlibaio; + +public interface SubmitInfo { + + void onError(int errno, String message); + + void done(); +} diff --git a/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/ffm/AIORing.java b/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/ffm/AIORing.java new file mode 100644 index 00000000000..fcf54a08a31 --- /dev/null +++ b/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/ffm/AIORing.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.artemis.nativo.jlibaio.ffm; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.foreign.MemoryLayout; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.StructLayout; +import java.lang.foreign.ValueLayout; +import java.lang.invoke.VarHandle; + +import static org.apache.artemis.nativo.jlibaio.ffm.Constants.AIO_RING_INCOMPAT_FEATURES; +import static org.apache.artemis.nativo.jlibaio.ffm.Constants.AIO_RING_MAGIC; +import static org.apache.artemis.nativo.jlibaio.ffm.IOEvent.IO_EVENT_LAYOUT; + +public class AIORing { + + private static final Logger logger = LoggerFactory.getLogger(AIORing.class); + + /** + * There is no defined aio_ring anywhere in an include, + * This is an implementation detail, that is a binary contract. + * it is safe to use the feature though. + */ + static final StructLayout AIO_RING_LAYOUT = MemoryLayout.structLayout( + // Fixed header (32 bytes) + ValueLayout.JAVA_INT.withName("id"), /* kernel internal index number */ + ValueLayout.JAVA_INT.withName("nr"), /* number of io_events */ + ValueLayout.JAVA_INT.withName("head"), ValueLayout.JAVA_INT.withName("tail"), ValueLayout.JAVA_INT.withName("magic"), ValueLayout.JAVA_INT.withName("compat_features"), ValueLayout.JAVA_INT.withName("incompat_features"), ValueLayout.JAVA_INT.withName("header_length") /* size of aio_ring */).withName("aio_ring"); + + public static final long AIO_RING_HEADER_SIZE = AIO_RING_LAYOUT.byteSize(); + + public static final VarHandle AIO_RING_NR_VH = AIO_RING_LAYOUT.varHandle(MemoryLayout.PathElement.groupElement("nr")); + public static final VarHandle AIO_RING_HEAD_VH = AIO_RING_LAYOUT.varHandle(MemoryLayout.PathElement.groupElement("head")); + public static final VarHandle AIO_RING_TAIL_VH = AIO_RING_LAYOUT.varHandle(MemoryLayout.PathElement.groupElement("tail")); + public static final VarHandle AIO_RING_MAGIC_VH = AIO_RING_LAYOUT.varHandle(MemoryLayout.PathElement.groupElement("magic")); + public static final VarHandle AIO_RING_INCOMPAT_FEATURES_VH = AIO_RING_LAYOUT.varHandle(MemoryLayout.PathElement.groupElement("incompat_features")); + + // Check if the implementation supports AIO_RING by checking this number directly. + public static boolean hasUsableRing(MemorySegment ring) { + if (ring == null || ring.address() == 0L || ring.byteSize() < AIO_RING_HEADER_SIZE) { + return false; + } + + MemorySegment header = ring.asSlice(0, AIO_RING_HEADER_SIZE); + int magic = (int) AIO_RING_MAGIC_VH.getAcquire(header, 0L); + int incompat = (int) AIO_RING_INCOMPAT_FEATURES_VH.getAcquire(header, 0L); + int nr = (int) AIO_RING_NR_VH.getAcquire(header, 0L); + if (logger.isTraceEnabled()) { + logger.trace("nr={}, magic={}, incompat={}", nr, magic, incompat); + } + + return magic == AIO_RING_MAGIC && incompat == AIO_RING_INCOMPAT_FEATURES && nr > 0; + } + + // Newer versions of the kernel (newer here being a relative word, a couple years already at the time + // I am writing this), will have io_context_t as an opaque type, and the real type being the aio_ring. + public static MemorySegment toAioRing(MemorySegment aioCtx) { + if (aioCtx == null || aioCtx.address() == 0L) { + return MemorySegment.NULL; + } + + MemorySegment header = aioCtx.reinterpret(AIO_RING_HEADER_SIZE); + + if (!hasUsableRing(header)) { + return MemorySegment.NULL; + } + + int nr = (int) AIO_RING_NR_VH.getAcquire(header, 0L); + long eventBytesize = IO_EVENT_LAYOUT.byteSize(); + long fullSize; + + try { + fullSize = Math.addExact(AIO_RING_HEADER_SIZE, Math.multiplyExact((long) nr, eventBytesize)); + } catch (ArithmeticException e) { + logger.warn("toAioRing: overflow computing ring size (nr={}, eventBytes={})", nr, eventBytesize); + return MemorySegment.NULL; + } + + if (fullSize <= AIO_RING_HEADER_SIZE) { + return MemorySegment.NULL; + } + + return aioCtx.reinterpret(fullSize); + } +} diff --git a/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/ffm/Constants.java b/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/ffm/Constants.java new file mode 100644 index 00000000000..cf719c67494 --- /dev/null +++ b/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/ffm/Constants.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.artemis.nativo.jlibaio.ffm; + +public final class Constants { + + private Constants() { + } + + static final long ONE_MEGA = 1048576L; + + //These should be used to check if the user-space io_getevents is supported: + //Linux ABI for the ring buffer: https://elixir.bootlin.com/linux/v4.20.13/source/fs/aio.c#L54 + //aio_read_events_ring: https://elixir.bootlin.com/linux/v4.20.13/source/fs/aio.c#L1148 + + // NOTE: if the kernel ever updates the structure, the RING-MAGIC will change and the code will switch back to normal IO calls + static final int AIO_RING_MAGIC = 0xa10a10a1; + static final int AIO_RING_INCOMPAT_FEATURES = 0; + + // set this to false if you want to stop using ring reaping + static final boolean RING_REAPER = true; + + static final int PERMISSION_MODE = 0666; + static final int O_RDWR = 0x0002; + static final int O_CREAT = 0x0040; + static final int O_DIRECT; + + static final int LOCK_EX = 2; // Exclusive lock + static final int LOCK_NB = 4; // Non-blocking lock + + static { + O_DIRECT = detectODirectFlag(); + } + + /* + * Detecting OS Architecture and setting O_DIRECT + * + * */ + private static int detectODirectFlag() { + String arch = System.getProperty("os.arch"); + if ("aarch64".equals(arch) || "arm64".equals(arch) || "arm".equals(arch)) { + return 0x10000; + } else if ("ppc64le".equals(arch) || "ppc64".equals(arch) || "ppc".equals(arch)) { + return 0x8000; + } + // amd64, x86_64 + return 0x4000; + } +} diff --git a/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/ffm/FFMHandles.java b/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/ffm/FFMHandles.java new file mode 100644 index 00000000000..14182746544 --- /dev/null +++ b/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/ffm/FFMHandles.java @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.artemis.nativo.jlibaio.ffm; + +import java.lang.foreign.Arena; +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.Linker; +import java.lang.foreign.MemoryLayout; +import java.lang.foreign.StructLayout; +import java.lang.foreign.SymbolLookup; +import java.lang.foreign.ValueLayout; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.VarHandle; +import java.util.concurrent.locks.ReentrantLock; + +public class FFMHandles { + + static final Linker LINKER = Linker.nativeLinker(); + static final SymbolLookup STDLIB = setStdLib(); + public static final SymbolLookup LIBAIO = setLibaio(); + + static final ReentrantLock oneMegaMutex = new ReentrantLock(); + + static final StructLayout CAPTURE_STATE_LAYOUT = Linker.Option.captureStateLayout(); + static final VarHandle ERRNO_VH = CAPTURE_STATE_LAYOUT.varHandle(MemoryLayout.PathElement.groupElement("errno")); + + private static final Linker.Option captureCallState = Linker.Option.captureCallState("errno"); + + static final MethodHandle WRITE_HANDLE = LINKER.downcallHandle(STDLIB.find("write").orElseThrow(() -> new UnsatisfiedLinkError("write not found in STDLIB")), FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.JAVA_LONG), captureCallState); + + static final MethodHandle OPEN_HANDLE = LINKER.downcallHandle(STDLIB.find("open").orElseThrow(() -> new UnsatisfiedLinkError("open not found in STDLIB")), FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, // pathName + ValueLayout.JAVA_INT, // flags + ValueLayout.JAVA_INT), // mode + captureCallState); + + static final MethodHandle CLOSE_HANDLE = LINKER.downcallHandle(STDLIB.find("close").orElseThrow(() -> new UnsatisfiedLinkError("close not found in STDLIB")), FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT), captureCallState); + + static final MethodHandle FALLOCATE_HANDLE = LINKER.downcallHandle(STDLIB.find("fallocate").orElseThrow(() -> new UnsatisfiedLinkError("fallocate not found in STDLIB")), FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG), captureCallState); + + static final MethodHandle FSYNC_HANDLE = LINKER.downcallHandle(STDLIB.find("fsync").orElseThrow(() -> new UnsatisfiedLinkError("fsync not found in STDLIB")), FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT), captureCallState); + + static final MethodHandle LSEEK_HANDLE = LINKER.downcallHandle(STDLIB.find("lseek").orElseThrow(() -> new UnsatisfiedLinkError("lseek not found in STDLIB")), FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_INT, ValueLayout.JAVA_LONG, ValueLayout.JAVA_INT), captureCallState); + + static final MethodHandle FSTAT_HANDLE = LINKER.downcallHandle(STDLIB.find("fstat").orElseThrow(() -> new UnsatisfiedLinkError("fstat not found in STDLIB")), FunctionDescriptor.of(ValueLayout.JAVA_INT, // return + ValueLayout.JAVA_INT, // fd + ValueLayout.ADDRESS), // struct stat + captureCallState); + + // for x86_64 - stat + static final MethodHandle STAT_HANDLE = LINKER.downcallHandle(STDLIB.find("stat").orElseThrow(() -> new UnsatisfiedLinkError("stat not found in STDLIB")), FunctionDescriptor.of(ValueLayout.JAVA_INT, // return + ValueLayout.ADDRESS, // pathname + ValueLayout.ADDRESS), // struct stat + captureCallState); + + static final MethodHandle IO_GETEVENTS_HANDLE = LINKER.downcallHandle(LIBAIO.find("io_getevents").orElseThrow(() -> new UnsatisfiedLinkError("io_getevents not found in LIBAIO")), FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.ADDRESS, ValueLayout.ADDRESS), captureCallState); + + static final MethodHandle IO_SUBMIT_HANDLE = LINKER.downcallHandle(LIBAIO.find("io_submit").orElseThrow(() -> new UnsatisfiedLinkError("io_submit not found in LIBAIO")), FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)).asFixedArity(); + + static final MethodHandle FREE_BUF_HANDLE = LINKER.downcallHandle(STDLIB.find("free").orElseThrow(() -> new UnsatisfiedLinkError("free not found in STDLIB")), FunctionDescriptor.ofVoid(ValueLayout.ADDRESS)); + + static final MethodHandle FLOCK_HANDLE = LINKER.downcallHandle(STDLIB.find("flock").orElseThrow(() -> new UnsatisfiedLinkError("flock not found in STDLIB")), FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT), captureCallState); + + static final MethodHandle IO_QUEUE_INIT_HANDLE = LINKER.downcallHandle(LIBAIO.find("io_queue_init").orElseThrow(() -> new UnsatisfiedLinkError("io_queue_init not found in LIBAIO")), FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.ADDRESS), captureCallState); + + static final MethodHandle IO_QUEUE_RELEASE_HANDLE = LINKER.downcallHandle(LIBAIO.find("io_queue_release").orElseThrow(() -> new UnsatisfiedLinkError("io_queue_release not found in LIBAIO")), FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS), captureCallState); + + static final MethodHandle FDATASYNC_HANDLE = LINKER.downcallHandle(STDLIB.find("fdatasync").orElseThrow(() -> new UnsatisfiedLinkError("fdatasync not found in STDLIB")), FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT), captureCallState); + + static final MethodHandle MEMSET_HANDLE = LINKER.downcallHandle(STDLIB.find("memset").orElseThrow(() -> new UnsatisfiedLinkError("memset not found in STDLIB")), FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_INT, ValueLayout.JAVA_LONG)); + + static final MethodHandle POSIX_MEMALIGN_HANDLE = LINKER.downcallHandle(STDLIB.find("posix_memalign").orElseThrow(() -> new UnsatisfiedLinkError("posix_memalign not found in STDLIB")), FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG), Linker.Option.captureCallState("errno")); + + private static SymbolLookup setStdLib() { + String[] libcPaths = {"/lib64/libc.so.6", "/usr/lib64/libc.so.6", "/lib/x86_64-linux-gnu/libc.so.6", "libc.so.6"}; + for (String path : libcPaths) { + SymbolLookup loopup = SymbolLookup.libraryLookup(path, Arena.global()); + if (loopup != null) { + return loopup; + } + } + throw new RuntimeException("libc.so.6 not found"); + } + + private static SymbolLookup setLibaio() { + String[] paths = {System.getProperty("libaio.path"), "/usr/lib64/libaio.so.1", "/usr/lib/x86_64-linux-gnu/libaio.so.1", "/lib64/libaio.so.1", "/usr/lib/libaio.so.1", "libaio.so.1"}; + for (String path : paths) { + if (path != null && !path.isEmpty()) { + SymbolLookup lookup = SymbolLookup.libraryLookup(path, Arena.global()); + if (lookup != null) { + return lookup; + } + } + } + throw new RuntimeException("libaio.so.1 not found"); + } +} diff --git a/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/ffm/FFMNativeHelper.java b/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/ffm/FFMNativeHelper.java new file mode 100644 index 00000000000..e30d6752f97 --- /dev/null +++ b/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/ffm/FFMNativeHelper.java @@ -0,0 +1,1063 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.artemis.nativo.jlibaio.ffm; + +import org.apache.artemis.nativo.jlibaio.SubmitInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import static org.apache.artemis.nativo.jlibaio.ffm.AIORing.AIO_RING_HEADER_SIZE; +import static org.apache.artemis.nativo.jlibaio.ffm.AIORing.AIO_RING_HEAD_VH; +import static org.apache.artemis.nativo.jlibaio.ffm.AIORing.AIO_RING_NR_VH; +import static org.apache.artemis.nativo.jlibaio.ffm.AIORing.AIO_RING_TAIL_VH; +import static org.apache.artemis.nativo.jlibaio.ffm.AIORing.hasUsableRing; +import static org.apache.artemis.nativo.jlibaio.ffm.AIORing.toAioRing; +import static org.apache.artemis.nativo.jlibaio.ffm.Constants.LOCK_EX; +import static org.apache.artemis.nativo.jlibaio.ffm.Constants.LOCK_NB; +import static org.apache.artemis.nativo.jlibaio.ffm.Constants.ONE_MEGA; +import static org.apache.artemis.nativo.jlibaio.ffm.Constants.O_CREAT; +import static org.apache.artemis.nativo.jlibaio.ffm.Constants.O_DIRECT; +import static org.apache.artemis.nativo.jlibaio.ffm.Constants.O_RDWR; +import static org.apache.artemis.nativo.jlibaio.ffm.Constants.PERMISSION_MODE; +import static org.apache.artemis.nativo.jlibaio.ffm.Constants.RING_REAPER; +import static org.apache.artemis.nativo.jlibaio.ffm.FFMHandles.CAPTURE_STATE_LAYOUT; +import static org.apache.artemis.nativo.jlibaio.ffm.FFMHandles.CLOSE_HANDLE; +import static org.apache.artemis.nativo.jlibaio.ffm.FFMHandles.ERRNO_VH; +import static org.apache.artemis.nativo.jlibaio.ffm.FFMHandles.FALLOCATE_HANDLE; +import static org.apache.artemis.nativo.jlibaio.ffm.FFMHandles.FDATASYNC_HANDLE; +import static org.apache.artemis.nativo.jlibaio.ffm.FFMHandles.FLOCK_HANDLE; +import static org.apache.artemis.nativo.jlibaio.ffm.FFMHandles.FREE_BUF_HANDLE; +import static org.apache.artemis.nativo.jlibaio.ffm.FFMHandles.FSTAT_HANDLE; +import static org.apache.artemis.nativo.jlibaio.ffm.FFMHandles.FSYNC_HANDLE; +import static org.apache.artemis.nativo.jlibaio.ffm.FFMHandles.IO_GETEVENTS_HANDLE; +import static org.apache.artemis.nativo.jlibaio.ffm.FFMHandles.IO_QUEUE_INIT_HANDLE; +import static org.apache.artemis.nativo.jlibaio.ffm.FFMHandles.IO_QUEUE_RELEASE_HANDLE; +import static org.apache.artemis.nativo.jlibaio.ffm.FFMHandles.IO_SUBMIT_HANDLE; +import static org.apache.artemis.nativo.jlibaio.ffm.FFMHandles.LSEEK_HANDLE; +import static org.apache.artemis.nativo.jlibaio.ffm.FFMHandles.MEMSET_HANDLE; +import static org.apache.artemis.nativo.jlibaio.ffm.FFMHandles.OPEN_HANDLE; +import static org.apache.artemis.nativo.jlibaio.ffm.FFMHandles.POSIX_MEMALIGN_HANDLE; +import static org.apache.artemis.nativo.jlibaio.ffm.FFMHandles.STAT_HANDLE; +import static org.apache.artemis.nativo.jlibaio.ffm.FFMHandles.WRITE_HANDLE; +import static org.apache.artemis.nativo.jlibaio.ffm.FFMHandles.oneMegaMutex; +import static org.apache.artemis.nativo.jlibaio.ffm.IOCBInit.IOCB_LAYOUT_SIZE; +import static org.apache.artemis.nativo.jlibaio.ffm.IOEvent.IO_EVENT_LAYOUT; +import static org.apache.artemis.nativo.jlibaio.ffm.Stat.STAT_LAYOUT; + +public class FFMNativeHelper { + + private static final Logger logger = LoggerFactory.getLogger(FFMNativeHelper.class); + + private static volatile MemorySegment oneMegaBuffer; + + private static final AtomicBoolean forceSysCall = new AtomicBoolean(false); + + private static final ThreadLocal SHARED_CONTEXT = ThreadLocal.withInitial(SharedContext::new); + + private static final AtomicReference DUMB_FD = new AtomicReference<>(-1); + + private static volatile String DUMB_PATH; + + private static final int DUMB_WRITE_HANDLER; + + static { + DUMB_WRITE_HANDLER = initDumbFd(); + } + + private static int initDumbFd() { + try { + Integer fd = DUMB_FD.get(); + if (fd != null && fd >= 0) { + logger.trace("Dumb FD already initialized: {}", fd); + return fd; + } + Path tempDir = Path.of(System.getProperty("java.io.tmpdir")); + Path tempFile; + try { + tempFile = Files.createTempFile(tempDir, "artemisDumb", ".tmp"); + DUMB_PATH = tempFile.toString(); + } catch (Exception e) { + throw new RuntimeException("Failed to create temp file for shutdown signaling", e); + } + fd = open(DUMB_PATH, false); + if (fd < 0) { + Files.deleteIfExists(tempFile); + throw new RuntimeException("Failed to open dumb file: " + tempFile); + } + + DUMB_FD.set(fd); + logger.debug("Dumb FD created: {}, path = {}", fd, DUMB_PATH); + return fd; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static void closeDumbFd() { + try { + Integer fd = DUMB_FD.getAndSet(-1); + if (fd != null && fd >= 0) { + try { + close(fd); + if (DUMB_PATH != null) { + Path path = Path.of(DUMB_PATH); + Files.deleteIfExists(path); + } + logger.debug("Dumb FD closed and file removed: fd={}, path={}", fd, DUMB_PATH); + } catch (IOException e) { + logger.warn("Failed to close/remove dumb FD {}: {}", fd, e.getMessage()); + } + } + } finally { + DUMB_PATH = null; + } + } + + private final ReleaseCallback releaseCallback; + + public FFMNativeHelper(ReleaseCallback releaseCallback) { + this.releaseCallback = releaseCallback; + } + + //It implements a user space batch read io events implementation that attempts to read io avoiding any sys calls + // This implementation will look at the internal structure (aio_ring) and move along the memory result + private int ringioGetEvents(MemorySegment aioCtxAddr, + MemorySegment events, + int min, + int max, + MemorySegment timeout) throws Throwable { + if (aioCtxAddr == null || aioCtxAddr.address() == 0) { + if (logger.isTraceEnabled()) { + logger.trace("ringioGetEvents: aioCtxAddr is null -> syscall"); + } + return ioGetEvents(aioCtxAddr, events, min, max, timeout); + } + + if (min < 0 || max <= 0 || min > max) { + logger.warn("ringioGetEvents: invalid parameters: min={}, max={}", min, max); + return ioGetEvents(aioCtxAddr, events, min, max, timeout); + } + + MemorySegment ring = toAioRing(aioCtxAddr); + if (ring.address() == 0) { + if (logger.isTraceEnabled()) { + logger.trace("toAioRing failed -> syscall"); + } + return ioGetEvents(aioCtxAddr, events, min, max, timeout); + } + + //checks if it could be completed in user space, saving a sys call + if (!(RING_REAPER && !isForceSyscall() && hasUsableRing(ring))) { + if (logger.isTraceEnabled()) { + logger.trace("kernel not supporting ring buffer"); + } + return ioGetEvents(aioCtxAddr, events, min, max, timeout); + } + + int ringNr = (int) AIO_RING_NR_VH.getAcquire(ring, 0L); + if (ringNr <= 0) { + if (logger.isTraceEnabled()) { + logger.trace("ringioGetEvents: invalid ring size {} -> syscall", ringNr); + } + return ioGetEvents(aioCtxAddr, events, min, max, timeout); + } + + // We're assuming to be the exclusive writer to head, so we just need a compiler barrier + // instead of compiler barrier, using getAcquired + int head = (int) AIO_RING_HEAD_VH.getAcquire(ring, 0L); + int tail = (int) AIO_RING_TAIL_VH.getAcquire(ring, 0L); + + int available = tail - head; + if (available < 0) { + available += ringNr; + } + + if (logger.isTraceEnabled()) { + logger.trace("tail={}, head={} nr={} available={}", tail, head, ringNr, available); + } + + boolean timeoutZero = false; + if (timeout != null && timeout.address() != 0) { + timeoutZero = timeout.get(ValueLayout.JAVA_LONG, 0L) == 0 && timeout.get(ValueLayout.JAVA_LONG, 8L) == 0; + } + + if (available < min && !timeoutZero) { + if (logger.isTraceEnabled()) { + logger.trace("ringioGetEvents: not enough available events -> syscall"); + } + return ioGetEvents(aioCtxAddr, events, min, max, timeout); + } + + if (available == 0) { + return 0; + } + + if (available >= max) { + // This is to trap a possible bug from the kernel: + // https://bugzilla.redhat.com/show_bug.cgi?id=1845326 + // https://issues.apache.org/jira/browse/ARTEMIS-2800 + // + // On the race available would eventually be >= max, while ring->tail was invalid + // we could work around by waiting ring-tail to change: + // while (ring->tail == tail) mem_barrier(); + // + // however eventually we could have available==max in a legal situation what could lead to infinite loop here + if (logger.isTraceEnabled()) { + logger.trace("ringioGetEvents: ring full ({}>= {}) → syscall", available, max); + } + return ioGetEvents(aioCtxAddr, events, min, max, timeout); + + // also: I could have called io_getevents to the one at the end of this method + // but I really hate goto, so I would rather have a duplicate code here + // and I did not want to create another memory flag to stop the rest of the code + } + + //the kernel has written ring->tail from an interrupt: + //we need to load acquire the completed events here + + // available < max ( this is always true ) + // old code -> int availableNr = available < max ? available : max; + //if isn't needed to wrap we can avoid % operations that are quite expansive + int needMod = ((head + available) >= ringNr) ? 1 : 0; + + long eventSize = IO_EVENT_LAYOUT.byteSize(); + long requiredBytes; + try { + requiredBytes = Math.multiplyExact((long) max, eventSize); + } catch (ArithmeticException e) { + logger.warn("ringioGetEvents: overflow computing required event bytes max={}, eventSize={}", max, eventSize); + return ioGetEvents(aioCtxAddr, events, min, max, timeout); + } + + MemorySegment usableEvents = events.reinterpret(requiredBytes); + + int eventIdx = head; + int contiguous = Math.min(available, ringNr - head); + + // first contiguous chunk + for (int i = 0; i < contiguous; i++) { + long eventOffset = AIO_RING_HEADER_SIZE + (long) (eventIdx + i) * eventSize; + MemorySegment srcEvent = ring.asSlice(eventOffset, eventSize); + MemorySegment dstEvent = usableEvents.asSlice((long) i * eventSize, eventSize); + dstEvent.copyFrom(srcEvent); + } + + // wrap around chunk, if any + if (contiguous < available) { + for (int i = contiguous; i < available; i++) { + long eventOffset = AIO_RING_HEADER_SIZE + (long) (i - contiguous) * eventSize; + MemorySegment srcEvent = ring.asSlice(eventOffset, eventSize); + MemorySegment dstEvent = usableEvents.asSlice((long) i * eventSize, eventSize); + dstEvent.copyFrom(srcEvent); + } + } + //it allow the kernel to build its own view of the ring buffer size + //and push new events if there are any + int newHead = (head + available) % ringNr; + AIO_RING_HEAD_VH.setRelease(ring, 0L, newHead); + + if (logger.isTraceEnabled()) { + logger.trace("consumed non sys-call = {}", available); + } + return available; + } + + private int ioGetEvents(MemorySegment aioCtx, + MemorySegment events, + long min, + long max, + MemorySegment timeout) throws Throwable { + MemorySegment captureState = SHARED_CONTEXT.get().getStateCapture(); + // Direct syscall wrapper + int result = (int) IO_GETEVENTS_HANDLE.invoke(captureState, aioCtx, min, max, events, (timeout == null ? MemorySegment.NULL : timeout)); + + if (result < 0) { + int errno = (int) ERRNO_VH.get(captureState, 0L); + logger.warn("ioGetEvents: failed to call IO_GETEVENTS_HANDLE. result={}, errno={}", result, errno); + } + return result; + } + + private static void freeOneMegaBuffer() { + oneMegaMutex.lock(); + try { + if (oneMegaBuffer != null) { + freeBuffer(oneMegaBuffer); + oneMegaBuffer = null; + logger.debug("One mega buffer freed"); + } + } finally { + oneMegaMutex.unlock(); + } + } + + public static void shutdownHook() { + logger.debug("FFMNativeHelper shutdown hook executing"); + closeDumbFd(); + freeOneMegaBuffer(); + } + + public static void setForceSyscall(boolean value) { + forceSysCall.set(value); + logger.info("forceSysCall={}", value); + } + + public static boolean isForceSyscall() { + return forceSysCall.get() || !RING_REAPER; + } + + public IOControl newContext(int queueSize) { + logger.debug("Initializing context with QueueSize={}", queueSize); + + IOControl ioControl = new IOControl<>(); + try { + MemorySegment ioContext = ioQueueInit(queueSize); + ioControl.setIoContext(ioContext); + + MemorySegment events = Arena.global().allocate(IO_EVENT_LAYOUT, queueSize); + if (events.address() == 0) { + ioQueueRelease(ioContext); + throw new OutOfMemoryError("Arena allocation failed: events array(queueSize = " + queueSize + ")"); + } + ioControl.setEvents(events); + + MemorySegment[] iocbPool = new MemorySegment[queueSize]; + for (int i = 0; i < queueSize; i++) { + MemorySegment iocb = Arena.global().allocate(IOCBInit.IOCB_LAYOUT); + if (iocb.address() == 0) { + for (int j = 0; j < i; j++) { + if (iocbPool[j] != null && iocbPool[j].address() != 0) { + freeBuffer(iocbPool[j]); + } + } + destroyIOCBs(events, queueSize); + ioQueueRelease(ioContext); + throw new OutOfMemoryError(String.format("Arena memory allocation failed: iocb[%d/%d]", i, queueSize)); + } + IOCBInit.setAioData(iocb, i); + iocbPool[i] = iocb; + } + ioControl.setIocbPool(iocbPool); + ioControl.setQueueSize(queueSize); + + logger.debug("Context created successfully: queueSize={}, ioContext=0x{}", queueSize, Long.toHexString(ioContext.address())); + return ioControl; + } catch (Throwable t) { + logger.error("newContext failed: queueSize={}, error={}", queueSize, t.getMessage(), t); + throw new RuntimeException(t); + } + } + + private void ioQueueRelease(MemorySegment ioContext) { + if (ioContext == null || ioContext.address() == 0) { + return; + } + try { + MemorySegment captureState = SHARED_CONTEXT.get().getStateCapture(); + int result = (int) IO_QUEUE_RELEASE_HANDLE.invoke(captureState, ioContext); + if (result < 0) { + logger.warn("io_queue_release(0x{}) failed: errno={}", Long.toHexString(ioContext.address()), ERRNO_VH.get(captureState, 0L)); + } else { + logger.trace("io_queue_release(0x{}) successful", Long.toHexString(ioContext.address())); + } + } catch (Throwable e) { + logger.warn("ioQueueRelease failed: error:{}", e.getMessage(), e); + } + } + + private void destroyIOCBs(MemorySegment array, int size) throws Throwable { + destroyIOCBsBounded(array, size); + } + + private void destroyIOCBsBounded(MemorySegment iocbArray, int upperBound) throws Throwable { + for (int i = 0; i < upperBound; i++) { + MemorySegment iocb = iocbArray.getAtIndex(ValueLayout.ADDRESS, i); + if (iocb.address() != 0) { + freeBuffer(iocb); + } + } + freeBuffer(iocbArray); + } + + private MemorySegment ioQueueInit(int queueSize) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment ctx = arena.allocate(ValueLayout.ADDRESS); + MemorySegment captureState = arena.allocate(CAPTURE_STATE_LAYOUT); + int result = (int) IO_QUEUE_INIT_HANDLE.invokeExact(captureState, queueSize, ctx); + if (result < 0) { + throw new IOException("io_queue_init failed: " + ERRNO_VH.get(captureState, 0L)); + } + long rawAddress = ctx.get(ValueLayout.JAVA_LONG, 0L); + logger.trace("ioQueueInit({}) → 0x{} (result={})", queueSize, Long.toHexString(rawAddress), result); + return MemorySegment.ofAddress(rawAddress).reinterpret(1, Arena.global(), null); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + public void deleteContext(IOControl ioControl) { + if (ioControl == null) { + logger.debug("deleteContext: null ioControl"); + return; + } + if (!ioControl.isValid()) { + logger.warn("deleteContext: invalid ioControl"); + return; + } + logger.debug("deleteContext: queueSize={}, ioContext=0x{}", ioControl.queueSize(), Long.toHexString(ioControl.ioContext().address())); + try { + MemorySegment dumbIocb = ioControl.getIOCB(); + if (dumbIocb == null || dumbIocb.address() == 0) { + throw new IOException("Not enough space in libaio queue during shutdown"); + } + ioPrepPOp(dumbIocb, DUMB_WRITE_HANDLER, MemorySegment.NULL, 0L, 0L, 1); + int iocbId = (int) IOCBInit.getAioData(dumbIocb); + ioControl.getIocbState().set(iocbId, -1); + + if (!submit(ioControl, dumbIocb)) { + logger.warn("deleteContext: submit failed: Continuing cleanup"); + return; + } else { + logger.debug("deleteContext: dumb write submitted (fd={})", DUMB_WRITE_HANDLER); + } + + // to make sure the poll has finished + ioControl.withPollLock(() -> { + }); + + // To return any pending IOCBs + int drained = 0; + while (true) { + try { + int result = ringioGetEvents(ioControl.ioContext(), ioControl.events(), 0, 1, null); + if (result <= 0) { + logger.trace("deleteContext: drain complete (result={})", result); + break; + } + logger.debug("deleteContext: drained {} pending IOCBs", result); + MemorySegment events = ioControl.events(); + events = events.reinterpret((long) result * IO_EVENT_LAYOUT.byteSize()); + for (int i = 0; i < result; i++) { + MemorySegment event = events.asSlice(i * IO_EVENT_LAYOUT.byteSize(), IO_EVENT_LAYOUT.byteSize()); + MemorySegment iocbp = event.get(ValueLayout.ADDRESS, 8L); + if (iocbp != null && iocbp.address() != 0) { + ioControl.putIOCB(iocbp); + } + } + drained += result; + } catch (Throwable t) { + logger.warn("deleteContext: drain unexpected error: {}", t.getMessage()); + break; + } + } + logger.trace("deleteContext: drained {} IOCBs under lock", drained); + + ioQueueRelease(ioControl.ioContext()); + + MemorySegment[] iocbPool = ioControl.iocbPool(); + if (iocbPool != null) { + for (MemorySegment iocb : iocbPool) { + if (iocb != null && iocb.address() != 0) { + freeBuffer(iocb); + } + } + } + + freeBuffer(ioControl.events()); + logger.debug("deleteContext completed successfully"); + } catch (IOException e) { + logger.warn("deleteContext: {}", e.getMessage()); + } catch (Throwable e) { + logger.error("deleteContext: unexpected error", e); + } + } + + public static int open(String filePath, boolean direct) throws IOException { + int flags = O_RDWR | O_CREAT; + if (direct) { + flags |= O_DIRECT; + logger.debug("Opening with O_DIRECT= {}", Integer.toHexString(O_DIRECT)); + } + try (Arena arena = Arena.ofConfined()) { + // manually ensuring null termination by adding "\0" + MemorySegment path = arena.allocateFrom(filePath + "\0"); + MemorySegment captureState = arena.allocate(CAPTURE_STATE_LAYOUT); + + int fd = (int) OPEN_HANDLE.invoke(captureState, path, flags, (int) PERMISSION_MODE); + + if (fd < 0) { + int errorCode = (int) ERRNO_VH.get(captureState, 0L); + logger.error("open failed: path={}, flags={}, direct={}, errno={}", filePath, Integer.toHexString(flags), direct, errorCode); + throw new IOException("Open failed for filePath = " + filePath + " with fd errno = " + errorCode); + } + logger.debug("Opened {} with fd = {}", direct ? "O_DIRECT" : "normal", fd); + return fd; + } catch (Throwable t) { + throw new IOException("Failed to open " + filePath, t); + } + } + + public static void close(int fd) throws IOException { + try (Arena arena = Arena.ofConfined()) { + MemorySegment captureState = arena.allocate(CAPTURE_STATE_LAYOUT); + + int res = (int) CLOSE_HANDLE.invoke(captureState, fd); + + if (res < 0) { + int errorCode = (int) ERRNO_VH.get(captureState, 0L); + throw new IOException("Error during close for fd = " + fd + ", error code = " + errorCode); + } + logger.debug("File with fd = {} is successfully closed", fd); + } catch (Throwable t) { + throw new IOException(t); + } + } + + public static MemorySegment newAlignedBuffer(int size, int alignment) { + if (size % alignment != 0) { + throw new IllegalArgumentException("size " + size + " must be aligned to " + alignment); + } + try (Arena arena = Arena.ofConfined()) { + MemorySegment prtOut = arena.allocate(ValueLayout.ADDRESS); + MemorySegment captureState = arena.allocate(CAPTURE_STATE_LAYOUT); + + int res = (int) POSIX_MEMALIGN_HANDLE.invoke(captureState, prtOut, (long) alignment, (long) size); + if (res != 0) { + int errno = (int) ERRNO_VH.get(captureState, 0L); + throw new RuntimeException("posix_memalign failed: result= " + res + " errno=" + errno + "(size= " + size + ", align= " + alignment + ")"); + } + // get allocated pointer + MemorySegment memorySegment = prtOut.get(ValueLayout.ADDRESS, 0L).reinterpret(size); + if (memorySegment.address() == 0) { + throw new RuntimeException("posix_memalign returned NULL!"); + } + //zero initialization + MEMSET_HANDLE.invoke(memorySegment, 0, (long) size); + logger.debug("posix_memalign(addrs={}, size={}, align={})", Long.toHexString(memorySegment.address()), size, alignment); + return memorySegment; + } catch (Throwable t) { + throw new RuntimeException("newAlignedBuffer failed", t); + } + } + + public static void freeBuffer(MemorySegment memorySegment) { + if (memorySegment == null || memorySegment.address() == 0) { + if (logger.isDebugEnabled()) { + logger.debug("freeBuffer: memorySegment is null"); + } + } + try { + if (logger.isTraceEnabled()) { + logger.trace("freeing buffer at address: 0x{} with capacity={}", Long.toHexString(memorySegment.address()), memorySegment.asByteBuffer().capacity()); + } + FREE_BUF_HANDLE.invoke(memorySegment); + } catch (Throwable t) { + throw new RuntimeException("freeBuffer: Native free failed for address 0x" + Long.toHexString(memorySegment.address()), t); + } + } + + private boolean submit(IOControl ioControl, MemorySegment iocb) throws IOException { + Objects.requireNonNull(ioControl.ioContext(), "Attempted to submit I/O to a null context"); + SharedContext ctx = SHARED_CONTEXT.get(); + int result = -1; + try { + ctx.getIocbArray().setAtIndex(ValueLayout.JAVA_LONG, 0, iocb.address()); + + if (logger.isTraceEnabled()) { + logger.trace("submit: ctx=0x{}, iocb=0x{}, iocbArray=0x{}", Long.toHexString(ioControl.ioContext().address()), Long.toHexString(iocb.address()), Long.toHexString(ctx.getIocbArray().address())); + } + + result = (int) IO_SUBMIT_HANDLE.invokeExact(ioControl.ioContext(), 1L, ctx.getIocbArray()); + + if (result < 0) { + throw new IOException("Error while submitting IO: result = " + result); + } + return true; + } catch (Throwable t) { + throw new IOException(t); + } finally { + if (result < 0) { + // return to the pool + ioControl.putIOCB(iocb); + } + } + } + + public void submitWrite(int fd, + IOControl ioControl, + long position, + int size, + ByteBuffer bufferWrite, + Callback callback) throws IOException { + + MemorySegment iocb = ioControl.getIOCB(); + if (iocb == null || iocb.address() == 0) { + throw new IOException("IOCB pool exhausted (used=" + ioControl.used() + "/queueSize=" + ioControl.queueSize() + ")"); + } + int callbackId = (int) IOCBInit.getAioData(iocb); + if (logger.isTraceEnabled()) { + logger.trace("submitWrite called! callbackId: {}", callbackId); + } + boolean submitted = false; + try { + if (!ioControl.getIocbState().compareAndSet(callbackId, 0, 1)) { + throw new IOException("submitWrite failed: callbackId=" + callbackId + " already in use"); + } + ioControl.addCallback(callbackId, callback); + bufferWrite.clear(); + ioPrepPOp(iocb, fd, MemorySegment.ofBuffer(bufferWrite), size, position, 1); + + submit(ioControl, iocb); + submitted = true; + } catch (Throwable e) { + throw new IOException("submitWrite failed", e); + } finally { + if (!submitted) { + ioControl.takeCallback(callbackId); + } + } + } + + /* + * Unable to load io_prep_pwrite and io_prep_pread from libaio because it is defined as a static inline function + * in the header file + * Because it is an inline function, the code is compiled directly into any C program that + * includes the header. It does not exist as a named symbol inside the copiled libaio.so shared lib file. + * + * 0: IO_CMD_PREAD + * 1: IO_CMD_PWRITE + * 2: IO_CMD_FSYNC + * 3: IO_CMD_FDSYNC + * 7: IO_CMD_NOOP + * 8: IO_CMD_PREADV (Vectorized read) + * + * */ + private void ioPrepPOp(MemorySegment iocb, int fd, MemorySegment buffer, long nbytes, long offset, int op) { + if (iocb == null) { + if (logger.isTraceEnabled()) { + logger.trace("ioPrepPOp: iocb is null"); + } + return; + } + IOCBInit.setAioFildes(iocb, fd); + IOCBInit.setAioLioOpcode(iocb, (short) op); + IOCBInit.setAioReqprio(iocb, (short) 0); + IOCBInit.setAioBuf(iocb, buffer.address()); + IOCBInit.setAioNbytes(iocb, nbytes); + IOCBInit.setAioOffset(iocb, offset); + } + + public void submitRead(int fd, + IOControl ioControl, + long position, + int size, + ByteBuffer bufferWrite, + Callback callback) throws IOException { + + MemorySegment iocb = ioControl.getIOCB(); + if (iocb == null || iocb.address() == 0) { + throw new IOException("IOCB pool exhausted"); + } + + if (logger.isTraceEnabled()) { + logger.trace("submitRead called!"); + } + long callbackId = IOCBInit.getAioData(iocb); + boolean submitted = false; + try { + if (!ioControl.getIocbState().compareAndSet((int) callbackId, 0, 1)) { + throw new IOException("submitRead failed: callbackId=" + callbackId + " already in use"); + } + ioControl.addCallback((int) callbackId, callback); + bufferWrite.clear(); + ioPrepPOp(iocb, fd, MemorySegment.ofBuffer(bufferWrite), size, position, 0); + + submit(ioControl, iocb); + submitted = true; + } catch (Throwable e) { + throw new IOException("submitRead failed", e); + } finally { + if (!submitted) { + ioControl.takeCallback((int) callbackId); + } + } + } + + public int poll(IOControl ioControl, Callback[] callbacks, int min, int max) { + if (ioControl == null || !ioControl.isValid()) { + logger.warn("poll: invalid context"); + return 0; + } + + try { + int result = ringioGetEvents(ioControl.ioContext(), ioControl.events(), min, max, null); + logger.trace("poll harvested {} events (min={}, max={})", result, min, max); + if (result <= 0) { + return result; + } + + MemorySegment events = ioControl.events(); + if (!events.scope().isAlive()) { + logger.error("Poll:: CRITICAL: Events segment is closed before polling!"); + return 0; + } + + events = events.reinterpret((long) result * IO_EVENT_LAYOUT.byteSize()); + for (int i = 0; i < result; i++) { + MemorySegment event = events.asSlice(i * IO_EVENT_LAYOUT.byteSize(), IO_EVENT_LAYOUT.byteSize()); + MemorySegment iocbp = event.get(ValueLayout.ADDRESS, 8L).reinterpret(64); + int eventResult = (int) event.get(ValueLayout.JAVA_LONG, 16L); + if (logger.isTraceEnabled()) { + logger.trace("poll[{}]: res={}, iocbp=0x{}, AioData: {}", i, eventResult, Long.toHexString(iocbp.address()), IOCBInit.getAioData(iocbp)); + } + + if (eventResult < 0) { + logger.warn("poll[{}]: I/O error: {}", i, eventResult); + } + + int callbackIdRaw = (int) IOCBInit.getAioData(iocbp); + int iocbState = ioControl.getIocbState().get(callbackIdRaw); + if (iocbState == 0 || iocbState == -1) { + logger.warn("poll[{}]: invalid callback=0x{}", i, Long.toHexString(callbackIdRaw)); + ioControl.putIOCB(iocbp); + continue; + } + + Callback callback = ioControl.takeCallback(callbackIdRaw); + if (callback != null) { + callbacks[i] = callback; + if (eventResult < 0) { + callback.onError(eventResult, "I/O error"); + } else { + callback.done(); + } + if (releaseCallback != null) { + releaseCallback.release(); + } + } else { + logger.warn("poll[{}]: callback not found for id=0x{}", i, Long.toHexString(callbackIdRaw)); + } + ioControl.getIocbState().set(callbackIdRaw, 0); + ioControl.putIOCB(iocbp); + } + return result; + } catch (Throwable e) { + logger.error("poll failed", e); + return -1; + } + } + + public void blockedPoll(IOControl ioControl, boolean useFdatasync) { + logger.debug("blockedPoll starting(useFdatasync={})", useFdatasync); + if (ioControl == null || !ioControl.isValid()) { + logger.warn("blockedPoll: invalid context"); + return; + } + + ioControl.withPollLock(() -> { + try (Arena arena = Arena.ofConfined()) { + boolean running = true; + int lastFile = -1; + + while (running) { + if (!ioControl.isValid()) { + logger.debug("blockedPoll: context destroyed - self-exit"); + break; + } + int result = ringioGetEvents(ioControl.ioContext(), ioControl.events(), 1, ioControl.queueSize(), null); + if (result == -4) { + logger.trace("blockedPoll: EINTR - ignoring (jmap?)"); + continue; + } + + if (result < 0) { + logger.error("blockedPoll: ringio_get_events failed: {}", result); + throw new IOException("blockedPoll: ringio_get_events failed:" + result); + } + + logger.trace("blockedPoll returned: {} events", result); + lastFile = -1; + + MemorySegment harvestedEvents = ioControl.events().reinterpret((long) result * IO_EVENT_LAYOUT.byteSize()); + + for (int i = 0; i < result; i++) { + + MemorySegment event = harvestedEvents.asSlice(i * IO_EVENT_LAYOUT.byteSize(), IO_EVENT_LAYOUT.byteSize()); + MemorySegment iocbp = IOEvent.getObj(event).reinterpret(IOCB_LAYOUT_SIZE); + + int fd = IOCBInit.getAioFildes(iocbp); + if (fd == DUMB_WRITE_HANDLER) { + logger.trace("blockedPoll: shutdown signal detected (dumb fd={})", fd); + ioControl.putIOCB(iocbp); + running = false; + break; + } + + if (useFdatasync && lastFile != fd) { + lastFile = fd; + fdatasync(arena, fd); + } + + int eventResult = (int) event.get(ValueLayout.JAVA_LONG, 16L); + + int callbackIdRaw = (int) IOCBInit.getAioData(iocbp); + if (logger.isTraceEnabled()) { + logger.trace("blockedPoll: callbackIdRaw: {}", callbackIdRaw); + } + + // this IOCB state is to detect invalid elements on the buffer. + if (ioControl.getIocbState().compareAndSet(callbackIdRaw, 1, 0)) { + ioControl.putIOCB(iocbp); + Callback callback = ioControl.takeCallback(callbackIdRaw); + if (callback != null) { + if (eventResult < 0) { + logger.error("blockedPoll[{}]: I/O error fd={}, {}", i, fd, eventResult); + callback.onError(eventResult, "I/O error in blockedPoll"); + } else { + callback.done(); + if (logger.isTraceEnabled()) { + logger.trace("callback executed!"); + } + } + if (releaseCallback != null) { + releaseCallback.release(); + } + } + } else { + if (!forceSysCall.get()) { + logger.warn("blockedPoll: Warning from ActiveMQ Artemis Native Layer: Your system is hitting duplicate / invalid records from libaio, which is a bug on the Linux Kernel you are using.You should set property org.apache.activemq.artemis.native.jlibaio.FORCE_SYSCALL=1 or upgrade to a kernel version that contains a fix"); + } + setForceSyscall(true); + } + } + } + } catch (Throwable e) { + logger.error("blockedPoll error", e); + } + }); + logger.debug("blockedPoll completed"); + } + + private static void fdatasync(Arena arena, int fd) throws Throwable { + MemorySegment captureState = arena.allocate(CAPTURE_STATE_LAYOUT); + int res = (int) FDATASYNC_HANDLE.invoke(captureState, fd); + if (res < 0) { + throw new IOException("fdatasync(fd = " + fd + ") failed, errno: " + ERRNO_VH.get(captureState, 0L)); + } + } + + public static int getNativeVersion() { + return 200; + } + + public static boolean lock(int fd) { + if (fd < 0) { + return false; + } + try (Arena arena = Arena.ofConfined()) { + MemorySegment captureState = arena.allocate(CAPTURE_STATE_LAYOUT); + int result = (int) FLOCK_HANDLE.invokeExact(captureState, fd, LOCK_EX | LOCK_NB); + return result == 0; + } catch (Throwable t) { + logger.warn("lock(fd={}) failed", fd); + return false; + } + } + + public static void memsetBuffer(ByteBuffer buffer, int size) { + if (!buffer.isDirect()) { + throw new IllegalArgumentException("libaio requires NativeBuffer (Direct ByteBuffer)"); + } + if (size <= 0 || size > buffer.capacity()) { + throw new IllegalArgumentException("Invalid size: " + size + " (capacity = " + buffer.capacity() + ")"); + } + + try { + ByteBuffer dup = buffer.duplicate(); + dup.clear(); + MemorySegment seg = MemorySegment.ofBuffer(dup); + long addr = seg.address(); + logger.trace("memset(buffer={}, size={})", buffer, size); + MemorySegment nativeSeg = MemorySegment.ofAddress(addr).reinterpret(buffer.capacity()); + // memset(buffer, 0, size) + MemorySegment ignore = (MemorySegment) MEMSET_HANDLE.invokeExact(nativeSeg, 0, (long) size); + logger.trace("memset completed!"); + } catch (Throwable t) { + throw new RuntimeException("memset failed", t); + } + } + + public static long getSize(int fd) throws IOException { + try (Arena arena = Arena.ofConfined()) { + MemorySegment statbuf = arena.allocate(STAT_LAYOUT); + MemorySegment captureState = arena.allocate(CAPTURE_STATE_LAYOUT); + + int res = (int) FSTAT_HANDLE.invokeExact(captureState, fd, statbuf); + if (res < 0) { + int errno = (int) ERRNO_VH.get(captureState, 0L); + throw new IOException("fstat failed for fd=" + fd + ": errno=" + errno); + } + + long size = Stat.getSize(statbuf); + logger.debug("getSize(fd = {}): {} bytes", fd, size); + return size; + } catch (Throwable t) { + throw new IOException("getSize failed for fd = " + fd, t); + } + } + + public static int getBlockSizeFD(int fd) throws IOException { + try (Arena arena = Arena.ofConfined()) { + MemorySegment statbuf = arena.allocate(STAT_LAYOUT); + MemorySegment captureState = arena.allocate(CAPTURE_STATE_LAYOUT); + int res = (int) FSTAT_HANDLE.invokeExact(captureState, fd, statbuf); + if (res < 0) { + int errno = (int) ERRNO_VH.get(captureState, 0L); + throw new IOException("fstat failed for fd=" + fd + ": errno=" + errno); + } + + int blksize = Stat.getBlksize(statbuf); + if (blksize <= 0 || blksize > 65536) { + logger.warn("Invalid st_blksize={} for fd={}, using 4096", blksize, fd); + return 4096; + } + logger.trace("getBlockSizeFD(fd = {}) = {} bytes", fd, blksize); + return blksize; + } catch (Throwable t) { + throw new IOException("getBlockSizeFD failed for fd=" + fd, t); + } + } + + public static int getBlockSize(String path) throws IOException { + try (Arena arena = Arena.ofConfined()) { + MemorySegment pathSeg = arena.allocateFrom(path); + MemorySegment statbuf = arena.allocate(STAT_LAYOUT); + MemorySegment captureState = arena.allocate(CAPTURE_STATE_LAYOUT); + int res = (int) STAT_HANDLE.invokeExact(captureState, pathSeg, statbuf); + if (res < 0) { + int errno = (int) ERRNO_VH.get(captureState, 0L); + throw new IOException("statx failed path=" + path + ": errno = " + errno); + } + int blksize = Stat.getBlksize(statbuf); + if (blksize <= 0 || blksize > 65536) { + logger.warn("Invalid st_blksize={} for path={}, using 4096", blksize, path); + return 4096; + } + logger.trace("getBlockSize(path = {}) = {} bytes", path, blksize); + return blksize; + } catch (Throwable t) { + logger.warn("getBlockSize failed '{}', fallback 4096", path, t); + return 4096; + } + } + + public static void fallocate(int fd, long size) throws IOException { + try { + MemorySegment captureState = SHARED_CONTEXT.get().getStateCapture(); + // fallocate(fd, mode=0, offset=0, len=size) + int res = (int) FALLOCATE_HANDLE.invoke(captureState, fd, 0, 0L, size); + if (res < 0) { + int errno = (int) ERRNO_VH.get(captureState, 0L); + throw new IOException("fallocate failed fd=" + fd + " size=" + size + ": errno= " + errno); + } + // fsync(fd) - ensure allocation hits the disk + res = (int) FSYNC_HANDLE.invoke(captureState, fd); + if (res < 0) { + int errno = (int) ERRNO_VH.get(captureState, 0L); + logger.warn("fsync after allocation failed fd={}: errno={}", fd, errno); + } + //lseek(fd, 0, SEEK_SET) - reset position + long pos = (long) LSEEK_HANDLE.invoke(captureState, fd, 0L, 0); + if (pos < 0) { + int errno = (int) ERRNO_VH.get(captureState, 0L); + logger.warn("lseek reset failed fd={}: errno={}", fd, errno); + } + logger.debug("fallocate(fd={}, size={}) + fsync + lseek(reset)", fd, size); + } catch (Throwable t) { + throw new IOException("fallocate failed fd=" + fd + " size=" + size, t); + } + } + + private static MemorySegment verifyBuffer(int alignment) { + oneMegaMutex.lock(); + try { + if (oneMegaBuffer == null) { + logger.debug("Allocating 1MB shared buffer (align={})", alignment); + oneMegaBuffer = newAlignedBuffer((int) ONE_MEGA, alignment); + } + return oneMegaBuffer; + } finally { + oneMegaMutex.unlock(); + } + } + + public static void fill(int fd, int alignment, long size) throws IOException { + logger.debug("fill(fd={}, alignment={}, size={})", fd, alignment, size); + + long blocks = size / ONE_MEGA; + long rest = size % ONE_MEGA; + + //verify/create 1MB buffer + verifyBuffer(alignment); + + try { + MemorySegment captureState = SHARED_CONTEXT.get().getStateCapture(); + // lseek (fd, 0, SEEK_SET) + LSEEK_HANDLE.invoke(captureState, fd, 0L, 0); + //Write full blocks + for (long i = 0; i < blocks; i++) { + MemorySegment bufferAddrs = oneMegaBuffer; + long written = (long) WRITE_HANDLE.invoke(captureState, fd, bufferAddrs, ONE_MEGA); + if (written < 0) { + int errno = (int) ERRNO_VH.get(captureState, 0L); + throw new IOException("write failed block " + i + ": errno= " + errno); + } + } + + // Remainder + if (rest > 0) { + MemorySegment bufferAddrs = oneMegaBuffer; + long written = (long) WRITE_HANDLE.invoke(captureState, fd, bufferAddrs, rest); + if (written < 0) { + int errno = (int) ERRNO_VH.get(captureState, 0L); + throw new IOException("write rest failed: errno= " + errno); + } + } + + //Reset position + LSEEK_HANDLE.invoke(captureState, fd, 0L, 0); + } catch (Throwable t) { + throw new IOException("fill failed fd=" + fd + " size=" + size, t); + } + logger.debug("fill completed: {} bytes written.", size); + } + + public static void writeInternal(int fd, long position, long size, ByteBuffer bufferWrite) throws IOException { + // No Impl + } +} diff --git a/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/ffm/IOCBInit.java b/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/ffm/IOCBInit.java new file mode 100644 index 00000000000..2f368e22986 --- /dev/null +++ b/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/ffm/IOCBInit.java @@ -0,0 +1,128 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.artemis.nativo.jlibaio.ffm; + +import java.lang.foreign.MemoryLayout; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; + +public class IOCBInit { + + public static final int IOCB_LAYOUT_SIZE = 64; + public static final MemoryLayout IOCB_LAYOUT = MemoryLayout.structLayout(ValueLayout.JAVA_LONG.withName("aio_data"), ValueLayout.JAVA_INT.withName("aio_key"), ValueLayout.JAVA_INT.withName("aio_rw_flags"), ValueLayout.JAVA_SHORT.withName("aio_lio_opcode"), ValueLayout.JAVA_SHORT.withName("aio_reqprio"), ValueLayout.JAVA_INT.withName("aio_fildes"), ValueLayout.JAVA_LONG.withName("aio_buf"), ValueLayout.JAVA_LONG.withName("aio_nbytes"), ValueLayout.JAVA_LONG.withName("aio_offset"), ValueLayout.JAVA_LONG.withName("aio_reserved2"), ValueLayout.JAVA_INT.withName("aio_flags"), ValueLayout.JAVA_INT.withName("aio_resfd")).withByteAlignment(8).withName("iocb"); + + public static final long AIO_DATA = 0; + public static final long AIO_KEY = 8; + public static final long AIO_RW_FLAGS = 12; + public static final long AIO_LIO_OPCODE = 16; + public static final long AIO_REQPRIO = 18; + public static final long AIO_FILDES = 20; + public static final long AIO_BUF = 24; + public static final long AIO_NBYTES = 32; + public static final long AIO_OFFSET = 40; + public static final long AIO_RESERVED2 = 48; + public static final long AIO_FLAGS = 56; + public static final long AIO_RESFD = 60; + + public static long getAioData(MemorySegment iocb) { + return iocb.get(ValueLayout.JAVA_LONG, AIO_DATA); + } + + public static void setAioData(MemorySegment iocb, long value) { + iocb.set(ValueLayout.JAVA_LONG, AIO_DATA, value); + } + + public static int getAioKey(MemorySegment iocb) { + return iocb.get(ValueLayout.JAVA_INT, AIO_KEY); + } + + public static void setAioKey(MemorySegment iocb, int value) { + iocb.set(ValueLayout.JAVA_INT, AIO_KEY, value); + } + + public static int getAioRwFlags(MemorySegment iocb) { + return iocb.get(ValueLayout.JAVA_INT, AIO_RW_FLAGS); + } + + public static void setAioRwFlags(MemorySegment iocb, int value) { + iocb.set(ValueLayout.JAVA_INT, AIO_RW_FLAGS, value); + } + + public static short getAioLioOpcode(MemorySegment iocb) { + return iocb.get(ValueLayout.JAVA_SHORT, AIO_LIO_OPCODE); + } + + public static void setAioLioOpcode(MemorySegment iocb, short value) { + iocb.set(ValueLayout.JAVA_SHORT, AIO_LIO_OPCODE, value); + } + + public static short getAioReqprio(MemorySegment iocb) { + return iocb.get(ValueLayout.JAVA_SHORT, AIO_REQPRIO); + } + + public static void setAioReqprio(MemorySegment iocb, short value) { + iocb.set(ValueLayout.JAVA_SHORT, AIO_REQPRIO, value); + } + + public static int getAioFildes(MemorySegment iocb) { + return iocb.get(ValueLayout.JAVA_INT, AIO_FILDES); + } + + public static void setAioFildes(MemorySegment iocb, int value) { + iocb.set(ValueLayout.JAVA_INT, AIO_FILDES, value); + } + + public static long getAioBuf(MemorySegment iocb) { + return iocb.get(ValueLayout.JAVA_LONG, AIO_BUF); + } + + public static void setAioBuf(MemorySegment iocb, long value) { + iocb.set(ValueLayout.JAVA_LONG, AIO_BUF, value); + } + + public static long getAioNbytes(MemorySegment iocb) { + return iocb.get(ValueLayout.JAVA_LONG, AIO_NBYTES); + } + + public static void setAioNbytes(MemorySegment iocb, long value) { + iocb.set(ValueLayout.JAVA_LONG, AIO_NBYTES, value); + } + + public static long getAioOffset(MemorySegment iocb) { + return iocb.get(ValueLayout.JAVA_LONG, AIO_OFFSET); + } + + public static void setAioOffset(MemorySegment iocb, long value) { + iocb.set(ValueLayout.JAVA_LONG, AIO_OFFSET, value); + } + + public static int getAioFlags(MemorySegment iocb) { + return iocb.get(ValueLayout.JAVA_INT, AIO_FLAGS); + } + + public static void setAioFlags(MemorySegment iocb, int value) { + iocb.set(ValueLayout.JAVA_INT, AIO_FLAGS, value); + } + + public static int getAioResfd(MemorySegment iocb) { + return iocb.get(ValueLayout.JAVA_INT, AIO_RESFD); + } + + public static void setAioResfd(MemorySegment iocb, int value) { + iocb.set(ValueLayout.JAVA_INT, AIO_RESFD, value); + } +} diff --git a/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/ffm/IOControl.java b/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/ffm/IOControl.java new file mode 100644 index 00000000000..d2089a07604 --- /dev/null +++ b/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/ffm/IOControl.java @@ -0,0 +1,200 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.artemis.nativo.jlibaio.ffm; + +import org.apache.artemis.nativo.jlibaio.SubmitInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.foreign.MemorySegment; +import java.util.concurrent.atomic.AtomicIntegerArray; +import java.util.concurrent.atomic.AtomicReferenceArray; + +public class IOControl { + + private static final Logger logger = LoggerFactory.getLogger(IOControl.class); + + private final Object iocbLock = new Object(); + private final Object pollLock = new Object(); + + private MemorySegment ioContext; + private MemorySegment events; + private int queueSize; + private int iocbPut; + private int iocbGet; + private int used; + private MemorySegment[] iocbPool; + private AtomicReferenceArray callbackRegistry; + + // -1: delete, 0: free, 1: used + private AtomicIntegerArray iocbState; + + public MemorySegment ioContext() { + return this.ioContext; + } + + public void setIoContext(MemorySegment ioContext) { + this.ioContext = ioContext; + } + + public MemorySegment events() { + return this.events; + } + + public void setEvents(MemorySegment events) { + this.events = events; + } + + public int queueSize() { + return queueSize; + } + + public void setQueueSize(int size) { + this.queueSize = size; + callbackRegistry = new AtomicReferenceArray<>(size); + iocbState = new AtomicIntegerArray(size); + } + + public int iocbPut() { + return this.iocbPut; + } + + public int iocbGet() { + return this.iocbGet; + } + + public int used() { + return this.used; + } + + public MemorySegment[] iocbPool() { + return this.iocbPool; + } + + public void setIocbPool(MemorySegment[] iocbPool) { + this.iocbPool = iocbPool; + } + + public void addCallback(int idx, Callback callback) { + if (callbackRegistry.get(idx) != null) { + throw new IllegalStateException("callback already registered"); + } + callbackRegistry.set(idx, callback); + } + + public Callback takeCallback(int idx) { + return callbackRegistry.getAndSet(idx, null); + } + + public AtomicIntegerArray getIocbState() { + return this.iocbState; + } + + public void withIocbLock(Runnable action) { + synchronized (iocbLock) { + action.run(); + } + } + + public void withPollLock(Runnable action) { + synchronized (pollLock) { + action.run(); + } + } + + public MemorySegment getIOCB() { + synchronized (iocbLock) { + final int qSize = this.queueSize; + if (qSize <= 0 || used >= qSize || iocbPool == null) { + return null; + } + + final int idx = iocbGet; + if (idx < 0 || idx >= qSize) { + return null; + } + + final MemorySegment seg = iocbPool[idx]; + if (seg == null || seg.address() == 0L) { + logger.error("getIOCB: null IOCB at index {}", idx); + return null; + } + + used++; + iocbGet = (idx + 1); + if (iocbGet >= qSize) { + iocbGet = 0; + } + if (logger.isTraceEnabled()) { + logger.trace("getIOCB: getIdx={} used={}", idx, used); + } + return seg; + } + } + + public void putIOCB(MemorySegment iocb) { + if (iocb == null || iocb.address() == 0L) { + logger.warn("putIOCB: null IOCB ignored"); + return; + } + synchronized (iocbLock) { + final int qSize = this.queueSize; + if (qSize <= 0 || used <= 0 || iocbPool == null) { + return; + } + + int idx = this.iocbPut; + if (idx < 0 || idx >= qSize) { + logger.error("putIOCB: invalid putIdx={} queueSize={}", idx, qSize); + return; + } + + iocbPool[idx] = iocb; + used--; + iocbPut = (idx + 1); + if (iocbPut >= qSize) { + iocbPut = 0; + } + if (logger.isTraceEnabled()) { + logger.trace("putIOCB: putIdx={} used={}", idx, used); + } + } + } + + public boolean isValid() { + if (ioContext == null || ioContext.address() == 0) { + return false; + } + if (events == null || events.address() == 0) { + return false; + } + + if (queueSize <= 0) { + return false; + } + + if (used < 0 || used > queueSize) { + return false; + } + + if (iocbPool == null || iocbPool.length != queueSize) { + return false; + } + + return iocbPut >= 0 && iocbPut < queueSize && iocbGet >= 0 && iocbGet < queueSize; + } +} diff --git a/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/ffm/IOEvent.java b/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/ffm/IOEvent.java new file mode 100644 index 00000000000..92b890f20c2 --- /dev/null +++ b/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/ffm/IOEvent.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.artemis.nativo.jlibaio.ffm; + +import java.lang.foreign.MemoryLayout; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.StructLayout; +import java.lang.foreign.ValueLayout; + +public class IOEvent { + + public static final int IO_EVENT_LAYOUT_SIZE = 32; + + static final StructLayout IO_EVENT_LAYOUT = MemoryLayout.structLayout(ValueLayout.JAVA_LONG.withName("data"), ValueLayout.ADDRESS.withName("obj"), ValueLayout.JAVA_LONG.withName("res"), ValueLayout.JAVA_LONG.withName("res2")).withName("io_event"); + + public static final long DATA = 0; + public static final long OBJ = 8; + public static final long RES = 16; + public static final long RES2 = 24; + + public static long getData(MemorySegment ioEvent) { + return ioEvent.get(ValueLayout.JAVA_LONG, DATA); + } + + public static void setData(MemorySegment ioEvent, long value) { + ioEvent.set(ValueLayout.JAVA_LONG, DATA, value); + } + + public static MemorySegment getObj(MemorySegment ioEvent) { + return ioEvent.get(ValueLayout.ADDRESS, OBJ); + } + + public static void setObj(MemorySegment ioEvent, MemorySegment value) { + ioEvent.set(ValueLayout.ADDRESS, OBJ, value); + } +} diff --git a/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/ffm/ReleaseCallback.java b/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/ffm/ReleaseCallback.java new file mode 100644 index 00000000000..398fb4665a5 --- /dev/null +++ b/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/ffm/ReleaseCallback.java @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.artemis.nativo.jlibaio.ffm; + +@FunctionalInterface +public interface ReleaseCallback { + + void release(); +} diff --git a/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/ffm/SharedContext.java b/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/ffm/SharedContext.java new file mode 100644 index 00000000000..28f007d42f1 --- /dev/null +++ b/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/ffm/SharedContext.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.artemis.nativo.jlibaio.ffm; + +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; + +import static org.apache.artemis.nativo.jlibaio.ffm.FFMHandles.CAPTURE_STATE_LAYOUT; + +public final class SharedContext { + + private final Arena arena; + private final MemorySegment stateCapture; + private final MemorySegment iocbArray; + + public SharedContext() { + this.arena = Arena.ofShared(); + this.stateCapture = arena.allocate(CAPTURE_STATE_LAYOUT); + this.iocbArray = arena.allocate(ValueLayout.ADDRESS, 1); + } + + public Arena getArena() { + return arena; + } + + public MemorySegment getStateCapture() { + return stateCapture; + } + + public MemorySegment getIocbArray() { + return iocbArray; + } +} diff --git a/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/ffm/Stat.java b/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/ffm/Stat.java new file mode 100644 index 00000000000..4084615f4f3 --- /dev/null +++ b/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/ffm/Stat.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.artemis.nativo.jlibaio.ffm; + +import java.lang.foreign.MemoryLayout; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.StructLayout; +import java.lang.foreign.ValueLayout; +import java.lang.invoke.VarHandle; + +public final class Stat { + + // this will work only for 64-bit linux + static final StructLayout STAT_LAYOUT = MemoryLayout.structLayout(MemoryLayout.paddingLayout(48), ValueLayout.JAVA_LONG.withName("st_size"), // File size (bytes) + ValueLayout.JAVA_INT.withName("st_blksize"), // Block size for filesystem I/O + ValueLayout.JAVA_INT.withName("__pad2"), ValueLayout.JAVA_LONG.withName("st_blocks"), // Number of 512B blocks allocated + MemoryLayout.paddingLayout(192)).withName("stat").withByteAlignment(8L); + + static final VarHandle ST_SIZE_VH = STAT_LAYOUT.varHandle(MemoryLayout.PathElement.groupElement("st_size")); + static final VarHandle ST_BLKSIZE_VH = STAT_LAYOUT.varHandle(MemoryLayout.PathElement.groupElement("st_blksize")); + static final VarHandle ST_BLOCKS_VH = STAT_LAYOUT.varHandle(MemoryLayout.PathElement.groupElement("st_blocks")); + + public static long getSize(MemorySegment stat) { + return (long) ST_SIZE_VH.get(stat, 0L); + } + + public static int getBlksize(MemorySegment stat) { + return (int) ST_BLKSIZE_VH.get(stat, 0L); + } + + public static int getBlocks(MemorySegment stat) { + return (int) ST_BLOCKS_VH.get(stat, 0L); + } +} diff --git a/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/package-info.java b/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/package-info.java new file mode 100644 index 00000000000..530db661fc6 --- /dev/null +++ b/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/package-info.java @@ -0,0 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * This packages handles Linux libaio at a low level. + *
+ * Buffers needs to be specially allocated by {@link org.apache.artemis.nativo.jlibaio.LibaioContext#newAlignedBuffer(int, int)} + * as they need to be aligned to 512 or 4096 when using Direct files. + */ +package org.apache.artemis.nativo.jlibaio; diff --git a/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/util/CallbackCache.java b/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/util/CallbackCache.java new file mode 100644 index 00000000000..59e036a81f6 --- /dev/null +++ b/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/util/CallbackCache.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.artemis.nativo.jlibaio.util; + +import org.apache.artemis.nativo.jlibaio.SubmitInfo; + +/** + * this is an utility class where you can reuse Callback objects for your LibaioContext usage. + */ +public class CallbackCache { + + private final SubmitInfo[] pool; + + private int put = 0; + private int get = 0; + private int available = 0; + private final int size; + + private final Object lock = new Object(); + + public CallbackCache(int size) { + this.pool = new SubmitInfo[size]; + this.size = size; + } + + public Callback get() { + synchronized (lock) { + if (available <= 0) { + return null; + } else { + Callback retValue = (Callback) pool[get]; + pool[get] = null; + if (retValue == null) { + throw new NullPointerException("You should initialize the pool before using it"); + } + available--; + get++; + if (get >= size) { + get = 0; + } + return retValue; + } + } + } + + public CallbackCache put(Callback callback) { + if (callback == null) { + return null; + } + synchronized (lock) { + if (available < size) { + available++; + pool[put++] = callback; + if (put >= size) { + put = 0; + } + } + } + return this; + } +} diff --git a/artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/CallbackCachelTest.java b/artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/CallbackCachelTest.java new file mode 100644 index 00000000000..c13173c2de0 --- /dev/null +++ b/artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/CallbackCachelTest.java @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.artemis.nativo.jlibaio.test; + +import org.apache.artemis.nativo.jlibaio.SubmitInfo; +import org.apache.artemis.nativo.jlibaio.util.CallbackCache; +import org.junit.Assert; +import org.junit.Test; + +import java.util.HashSet; + +public class CallbackCachelTest { + + @Test + public void testPartiallyInitialized() { + CallbackCache pool = new CallbackCache(100); + + for (int i = 0; i < 50; i++) { + pool.put(new MyPool(i)); + } + + MyPool value = pool.get(); + + Assert.assertNotNull(value); + + pool.put(value); + + // add and remove immediately + for (int i = 0; i < 777; i++) { + pool.put(pool.get()); + } + + HashSet hashValues = new HashSet<>(); + + MyPool getValue; + while ((getValue = pool.get()) != null) { + hashValues.add(getValue); + } + + Assert.assertEquals(50, hashValues.size()); + } + + static class MyPool implements SubmitInfo { + + public final int i; + + MyPool(int i) { + this.i = i; + } + + public int getI() { + return i; + } + + @Override + public void onError(int errno, String message) { + } + + @Override + public void done() { + + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + MyPool myPool = (MyPool) o; + + if (i != myPool.i) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + return i; + } + } +} diff --git a/artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/LibaioStressTest.java b/artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/LibaioStressTest.java new file mode 100644 index 00000000000..f1ca22ea602 --- /dev/null +++ b/artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/LibaioStressTest.java @@ -0,0 +1,272 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.artemis.nativo.jlibaio.test; + +import org.apache.artemis.nativo.jlibaio.LibaioContext; +import org.apache.artemis.nativo.jlibaio.LibaioFile; +import org.apache.artemis.nativo.jlibaio.SubmitInfo; +import org.apache.artemis.nativo.jlibaio.util.CallbackCache; +import org.junit.After; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.lang.foreign.MemorySegment; +import java.nio.ByteBuffer; +import java.util.concurrent.TimeUnit; + +/** + * This test is using a different package from {@link LibaioFile} + * as I need to validate public methods on the API + */ +public class LibaioStressTest { + + private static final Logger logger = LoggerFactory.getLogger(LibaioStressTest.class); + + private static final int STRESS_TIME = Integer.parseInt(System.getProperty("test.stress.time", "5000")); + + static { + logger.debug("LibaioStressTest:: -Dtest.stress.time=" + STRESS_TIME); + } + + @BeforeClass + public static void testAssume() { + Assume.assumeTrue(LibaioContext.isLoaded()); + } + + /** + * This is just an arbitrary number for a number of elements you need to pass to the libaio init method + * Some of the tests are using half of this number, so if anyone decide to change this please use an even number. + */ + private static final int LIBAIO_QUEUE_SIZE = 4096; + + private int errors = 0; + + private boolean running = true; + + @Rule + public TemporaryFolder temporaryFolder; + + public LibaioContext control; + + @Before + public void setUpFactory() { + control = new LibaioContext<>(LIBAIO_QUEUE_SIZE, true, false); + } + + @After + public void deleteFactory() { + control.close(); + validateLibaio(); + } + + public void validateLibaio() { + Assert.assertEquals(0, LibaioContext.getTotalMaxIO()); + } + + public LibaioStressTest() { + /* + * I didn't use /tmp for three reasons + * - Most systems now will use tmpfs which is not compatible with O_DIRECT + * - This would fill up /tmp in case of failures. + * - target is cleaned up every time you do a mvn clean, so it's safer + */ + File parent = new File("./target"); + parent.mkdirs(); + temporaryFolder = new TemporaryFolder(parent); + } + + @Test + public void testOpen() throws Exception { + LibaioFile fileDescriptor = control.openFile(temporaryFolder.newFile("test.bin"), true); + fileDescriptor.close(); + } + + CallbackCache callbackCache = new CallbackCache<>(LIBAIO_QUEUE_SIZE); + + class MyClass implements SubmitInfo { + + ReusableLatch reusableLatch; + + @Override + public void onError(int errno, String message) { + + } + + @Override + public void done() { + try { + reusableLatch.countDown(); + reusableLatch = null; + callbackCache.put(this); + } catch (Throwable e) { + e.printStackTrace(); + System.exit(-1); + } + } + } + + @Test + public void testForceSyscall() { + Assert.assertFalse(LibaioContext.isForceSyscall()); + LibaioContext.setForceSyscall(true); + Assert.assertTrue(LibaioContext.isForceSyscall()); + LibaioContext.setForceSyscall(false); + } + + @Test + public void testStressWritesNoSleeps() throws Exception { + testStressWrites(false); + } + + @Test + public void testStressWrites() throws Exception { + testStressWrites(true); + } + + private void testStressWrites(boolean sleeps) throws Exception { + Assume.assumeFalse(LibaioContext.isForceSyscall()); + + Thread t = new Thread() { + @Override + public void run() { + control.poll(); + } + }; + + t.start(); + + Thread t2 = new Thread(() -> { + while (running) { + try { + Thread.sleep(1000); + } catch (Exception e) { + } + // this is just to make things more interesting from the POV of testing + System.gc(); + } + }); + + t2.start(); + + Thread test1 = startThread("test1.bin", sleeps); + Thread test2 = startThread("test2.bin", sleeps); + Thread.sleep(STRESS_TIME); // Configured timeout on the test + running = false; + test2.join(); + test1.join(); + t2.join(); + + Assert.assertFalse(LibaioContext.isForceSyscall()); + return; + } + + private Thread startThread(String name, boolean sleeps) { + Thread t_test = new Thread(() -> { + try { + doFile(name, sleeps); + } catch (IOException e) { + e.printStackTrace(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }); + t_test.start(); + + return t_test; + } + + private void doFile(String fileName, boolean sleeps) throws IOException, InterruptedException { + ReusableLatch latchWrites = new ReusableLatch(0); + + File file = temporaryFolder.newFile(fileName); + LibaioFile fileDescriptor = control.openFile(file, true); + + // ByteBuffer buffer = ByteBuffer.allocateDirect(4096); + MemorySegment memorySegment = LibaioContext.newAlignedBuffer(4096, 4096); + ByteBuffer buffer = memorySegment.asByteBuffer(); + + int maxSize = 4096 * LIBAIO_QUEUE_SIZE; + fileDescriptor.fill(4096, maxSize); + for (int i = 0; i < 4096; i++) { + buffer.put((byte) 'a'); + } + + buffer.rewind(); + + int pos = 0; + + long count = 0; + + long nextBreak = System.currentTimeMillis() + 3000; + + while (running) { + count++; + + if (System.currentTimeMillis() > nextBreak) { + if (!latchWrites.await(10, TimeUnit.SECONDS)) { + System.err.println("Latch did not complete for some reason"); + errors++; + return; + } + fileDescriptor.close(); + + fileDescriptor = control.openFile(file, true); + pos = 0; + // we close / open a file every 5 seconds + nextBreak = System.currentTimeMillis() + 5000; + } + + if (count % (sleeps ? 1_000 : 100_000) == 0) { + logger.debug("Writen " + count + " buffers at " + fileName); + } + MyClass myClass = callbackCache.get(); + + if (myClass == null) { + myClass = new MyClass(); + } + + myClass.reusableLatch = latchWrites; + myClass.reusableLatch.countUp(); + + if (sleeps) { + if (count % 100 == 0) { + Thread.sleep(100); + } + } + fileDescriptor.write(pos, 4096, buffer, myClass); + pos += 4096; + + if (pos >= maxSize) { + pos = 0; + } + + } + + fileDescriptor.close(); + } + +} diff --git a/artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/LibaioTest.java b/artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/LibaioTest.java new file mode 100644 index 00000000000..9f54400911f --- /dev/null +++ b/artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/LibaioTest.java @@ -0,0 +1,790 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.artemis.nativo.jlibaio.test; + +import org.apache.artemis.nativo.jlibaio.LibaioContext; +import org.apache.artemis.nativo.jlibaio.LibaioFile; +import org.apache.artemis.nativo.jlibaio.SubmitInfo; +import org.junit.After; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.lang.foreign.MemorySegment; +import java.lang.ref.Cleaner; +import java.lang.ref.WeakReference; +import java.nio.ByteBuffer; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * This test is using a different package from {@link LibaioFile} + * as I need to validate public methods on the API + */ +public class LibaioTest { + + private static final Logger logger = LoggerFactory.getLogger(LibaioTest.class); + + @BeforeClass + public static void testAIO() { + Assume.assumeTrue(LibaioContext.isLoaded()); + + File parent = new File("./target"); + File file = new File(parent, "testFile"); + + try { + parent.mkdirs(); + + boolean failed = false; + try (LibaioContext control = new LibaioContext<>(1, true, true); LibaioFile fileDescriptor = control.openFile(file, true)) { + fileDescriptor.fallocate(4 * 1024); + } catch (Exception e) { + e.printStackTrace(); + failed = true; + } + + Assume.assumeFalse("There is not enough support to libaio", failed); + } finally { + file.delete(); + } + } + + /** + * This is just an arbitrary number for a number of elements you need to pass to the libaio init method + * Some of the tests are using half of this number, so if anyone decide to change this please use an even number. + */ + private static final int LIBAIO_QUEUE_SIZE = 50; + + @Rule + public TemporaryFolder temporaryFolder; + + public LibaioContext control; + + @Before + public void setUpFactory() { + control = new LibaioContext<>(LIBAIO_QUEUE_SIZE, true, true); + } + + @After + public void deleteFactory() { + control.close(); + validateLibaio(); + } + + public void validateLibaio() { + Assert.assertEquals(0, LibaioContext.getTotalMaxIO()); + } + + public LibaioTest() { + /* + * I didn't use /tmp for three reasons + * - Most systems now will use tmpfs which is not compatible with O_DIRECT + * - This would fill up /tmp in case of failures. + * - target is cleaned up every time you do a mvn clean, so it's safer + */ + File parent = new File("./target"); + parent.mkdirs(); + temporaryFolder = new TemporaryFolder(parent); + } + + @Test + public void testOpen() throws Exception { + LibaioFile fileDescriptor = control.openFile(temporaryFolder.newFile("test.bin"), true); + fileDescriptor.close(); + } + + @Test + public void testInitAndFallocate10M() throws Exception { + testInit(10 * 1024 * 1024); + } + + @Test + public void testInitAndFallocate10M100K() throws Exception { + testInit(10 * 1024 * 1024 + 100 * 1024); + } + + private void testInit(int size) throws IOException { + LibaioFile fileDescriptor = control.openFile(temporaryFolder.newFile("test.bin"), true); + fileDescriptor.fallocate(size); + + MemorySegment buffer = fileDescriptor.newBuffer(size); + fileDescriptor.read(0, size, buffer.asByteBuffer(), new TestInfo()); + + TestInfo[] callbacks = new TestInfo[1]; + control.poll(callbacks, 1, 1); + + fileDescriptor.close(); + + buffer.asByteBuffer().position(0); + + LibaioFile fileDescriptor2 = control.openFile(temporaryFolder.newFile("test2.bin"), true); + fileDescriptor2.fill(fileDescriptor.getBlockSize(), size); + fileDescriptor2.read(0, size, buffer.asByteBuffer(), new TestInfo()); + + control.poll(callbacks, 1, 1); + for (int i = 0; i < size; i++) { + Assert.assertEquals(0, buffer.asByteBuffer().get()); + } + + LibaioContext.freeBuffer(buffer); + } + + @Test + public void testInitAndFallocate10K() throws Exception { + testInit(10 * 4096); + } + + @Test + public void testInitAndFallocate20K() throws Exception { + testInit(20 * 4096); + } + + @Test + public void testSubmitWriteOnTwoFiles() throws Exception { + + File file1 = temporaryFolder.newFile("test.bin"); + File file2 = temporaryFolder.newFile("test2.bin"); + + fillupFile(file1, LIBAIO_QUEUE_SIZE / 2); + fillupFile(file2, LIBAIO_QUEUE_SIZE / 2); + + LibaioFile[] fileDescriptor = new LibaioFile[]{control.openFile(file1, true), control.openFile(file2, true)}; + + Assert.assertEquals((LIBAIO_QUEUE_SIZE / 2) * 4096, fileDescriptor[0].getSize()); + Assert.assertEquals((LIBAIO_QUEUE_SIZE / 2) * 4096, fileDescriptor[1].getSize()); + Assert.assertEquals(fileDescriptor[0].getBlockSize(), fileDescriptor[1].getBlockSize()); + Assert.assertEquals(LibaioContext.getBlockSize(temporaryFolder.getRoot()), LibaioContext.getBlockSize(file1)); + Assert.assertEquals(LibaioContext.getBlockSize(file1), LibaioContext.getBlockSize(file2)); + logger.debug("blockSize = " + fileDescriptor[0].getBlockSize()); + logger.debug("blockSize /tmp= " + LibaioContext.getBlockSize("/tmp")); + + MemorySegment buffer = LibaioContext.newAlignedBuffer(4096, 4096); + + try { + for (int i = 0; i < 4096; i++) { + buffer.asByteBuffer().put((byte) 'a'); + } + + TestInfo callback = new TestInfo(); + TestInfo[] callbacks = new TestInfo[LIBAIO_QUEUE_SIZE]; + + for (int i = 0; i < LIBAIO_QUEUE_SIZE / 2; i++) { + for (LibaioFile file : fileDescriptor) { + file.write(i * 4096, 4096, buffer.asByteBuffer(), callback); + } + } + + Assert.assertEquals(LIBAIO_QUEUE_SIZE, control.poll(callbacks, LIBAIO_QUEUE_SIZE, LIBAIO_QUEUE_SIZE)); + + for (Object returnedCallback : callbacks) { + Assert.assertSame(returnedCallback, callback); + } + + for (LibaioFile file : fileDescriptor) { + MemorySegment bigbuffer = LibaioContext.newAlignedBuffer(4096 * 25, 4096); + file.read(0, 4096 * 25, bigbuffer.asByteBuffer(), callback); + Assert.assertEquals(1, control.poll(callbacks, 1, LIBAIO_QUEUE_SIZE)); + + for (Object returnedCallback : callbacks) { + Assert.assertSame(returnedCallback, callback); + } + + for (int i = 0; i < 4096 * 25; i++) { + Assert.assertEquals((byte) 'a', bigbuffer.asByteBuffer().get()); + } + + LibaioContext.freeBuffer(bigbuffer); + + file.close(); + } + } finally { + LibaioContext.freeBuffer(buffer); + } + } + + @Test + public void testSubmitWriteAndRead() throws Exception { + TestInfo callback = new TestInfo(); + + TestInfo[] callbacks = new TestInfo[LIBAIO_QUEUE_SIZE]; + + LibaioFile fileDescriptor = control.openFile(temporaryFolder.newFile("test.bin"), true); + + // ByteBuffer buffer = ByteBuffer.allocateDirect(4096); + MemorySegment buffer = LibaioContext.newAlignedBuffer(4096, 4096); + + try { + for (int i = 0; i < 4096; i++) { + buffer.asByteBuffer().put((byte) 'a'); + } + + buffer.asByteBuffer().rewind(); + + fileDescriptor.write(0, 4096, buffer.asByteBuffer(), callback); + + int retValue = control.poll(callbacks, 1, LIBAIO_QUEUE_SIZE); + Assert.assertEquals(1, retValue); + + Assert.assertSame(callback, callbacks[0]); + + LibaioContext.freeBuffer(buffer); + + buffer = LibaioContext.newAlignedBuffer(4096, 4096); + + for (int i = 0; i < 4096; i++) { + buffer.asByteBuffer().put((byte) 'B'); + } + + fileDescriptor.write(0, 4096, buffer.asByteBuffer(), new TestInfo()); + + Assert.assertEquals(1, control.poll(callbacks, 1, LIBAIO_QUEUE_SIZE)); + + buffer.asByteBuffer().rewind(); + + fileDescriptor.read(0, 4096, buffer.asByteBuffer(), new TestInfo()); + + Assert.assertEquals(1, control.poll(callbacks, 1, LIBAIO_QUEUE_SIZE)); + + for (int i = 0; i < 4096; i++) { + Assert.assertEquals('B', buffer.asByteBuffer().get()); + } + } finally { + LibaioContext.freeBuffer(buffer); + fileDescriptor.close(); + } + } + + @Test + /* + * This file is making use of libaio without O_DIRECT + * We won't need special buffers on this case. + */ public void testSubmitWriteAndReadRegularBuffers() throws Exception { + TestInfo callback = new TestInfo(); + + TestInfo[] callbacks = new TestInfo[LIBAIO_QUEUE_SIZE]; + + File file = temporaryFolder.newFile("test.bin"); + + fillupFile(file, LIBAIO_QUEUE_SIZE); + + LibaioFile fileDescriptor = control.openFile(file, false); + + final int BUFFER_SIZE = 50; + + ByteBuffer buffer = ByteBuffer.allocateDirect(BUFFER_SIZE); + + try { + for (int i = 0; i < BUFFER_SIZE; i++) { + buffer.put((byte) 'a'); + } + + buffer.rewind(); + + fileDescriptor.write(0, BUFFER_SIZE, buffer, callback); + + int retValue = control.poll(callbacks, 1, LIBAIO_QUEUE_SIZE); + logger.debug("Return from poll::" + retValue); + Assert.assertEquals(1, retValue); + + Assert.assertSame(callback, callbacks[0]); + + buffer.rewind(); + + for (int i = 0; i < BUFFER_SIZE; i++) { + buffer.put((byte) 'B'); + } + + fileDescriptor.write(0, BUFFER_SIZE, buffer, new TestInfo()); + + Assert.assertEquals(1, control.poll(callbacks, 1, LIBAIO_QUEUE_SIZE)); + + buffer.rewind(); + + fileDescriptor.read(0, 50, buffer, new TestInfo()); + + Assert.assertEquals(1, control.poll(callbacks, 1, LIBAIO_QUEUE_SIZE)); + + for (int i = 0; i < BUFFER_SIZE; i++) { + Assert.assertEquals('B', buffer.get()); + } + } finally { + fileDescriptor.close(); + } + } + + @Test + public void testSubmitRead() throws Exception { + + TestInfo callback = new TestInfo(); + + TestInfo[] callbacks = new TestInfo[LIBAIO_QUEUE_SIZE]; + + File file = temporaryFolder.newFile("test.bin"); + + fillupFile(file, LIBAIO_QUEUE_SIZE); + + LibaioFile fileDescriptor = control.openFile(file, true); + + MemorySegment buffer = LibaioContext.newAlignedBuffer(4096, 4096); + + final int BUFFER_SIZE = 4096; + try { + for (int i = 0; i < BUFFER_SIZE; i++) { + buffer.asByteBuffer().put((byte) '@'); + } + + fileDescriptor.write(0, BUFFER_SIZE, buffer.asByteBuffer(), callback); + Assert.assertEquals(1, control.poll(callbacks, 1, LIBAIO_QUEUE_SIZE)); + Assert.assertSame(callback, callbacks[0]); + + buffer.asByteBuffer().rewind(); + + fileDescriptor.read(0, BUFFER_SIZE, buffer.asByteBuffer(), callback); + + Assert.assertEquals(1, control.poll(callbacks, 1, LIBAIO_QUEUE_SIZE)); + + Assert.assertSame(callback, callbacks[0]); + + for (int i = 0; i < BUFFER_SIZE; i++) { + Assert.assertEquals('@', buffer.asByteBuffer().get()); + } + } finally { + LibaioContext.freeBuffer(buffer); + fileDescriptor.close(); + } + } + + @Test + @Ignore + public void testInvalidWrite() throws Exception { + + TestInfo callback = new TestInfo(); + + TestInfo[] callbacks = new TestInfo[LIBAIO_QUEUE_SIZE]; + + File file = temporaryFolder.newFile("test.bin"); + + fillupFile(file, LIBAIO_QUEUE_SIZE); + + LibaioFile fileDescriptor = control.openFile(file, true); + + try { + ByteBuffer buffer = ByteBuffer.allocateDirect(300); + for (int i = 0; i < 300; i++) { + buffer.put((byte) 'z'); + } + + fileDescriptor.write(0, 300, buffer, callback); + + Assert.assertEquals(1, control.poll(callbacks, 1, LIBAIO_QUEUE_SIZE)); + + Assert.assertTrue(callbacks[0].isError()); + + // Error condition + Assert.assertSame(callbacks[0], callback); + + logger.debug("Error:" + callbacks[0]); + + MemorySegment memorySegment = fileDescriptor.newBuffer(4096); + buffer = memorySegment.asByteBuffer(); + for (int i = 0; i < 4096; i++) { + buffer.put((byte) 'z'); + } + + callback = new TestInfo(); + + fileDescriptor.write(0, 4096, buffer, callback); + + Assert.assertEquals(1, control.poll(callbacks, 1, 1)); + + Assert.assertSame(callback, callbacks[0]); + + fileDescriptor.write(5, 4096, buffer, callback); + + Assert.assertEquals(1, control.poll(callbacks, 1, 1)); + + Assert.assertTrue(callbacks[0].isError()); + + callbacks = null; + callback = null; + + TestInfo.checkLeaks(); + } finally { + fileDescriptor.close(); + } + } + + @Test + public void testLeaks() throws Exception { + File file = temporaryFolder.newFile("test.bin"); + + fillupFile(file, LIBAIO_QUEUE_SIZE * 2); + + TestInfo[] callbacks = new TestInfo[LIBAIO_QUEUE_SIZE]; + + LibaioFile fileDescriptor = control.openFile(file, true); + + MemorySegment bufferWrite = LibaioContext.newAlignedBuffer(4096, 4096); + + try { + for (int i = 0; i < 4096; i++) { + bufferWrite.asByteBuffer().put((byte) 'B'); + } + + for (int j = 0; j < LIBAIO_QUEUE_SIZE * 2; j++) { + for (int i = 0; i < LIBAIO_QUEUE_SIZE; i++) { + TestInfo countClass = new TestInfo(); + fileDescriptor.write(i * 4096, 4096, bufferWrite.asByteBuffer(), countClass); + } + + Assert.assertEquals(LIBAIO_QUEUE_SIZE, control.poll(callbacks, LIBAIO_QUEUE_SIZE, LIBAIO_QUEUE_SIZE)); + + for (int i = 0; i < LIBAIO_QUEUE_SIZE; i++) { + Assert.assertNotNull(callbacks[i]); + callbacks[i] = null; + } + } + + TestInfo.checkLeaks(); + } finally { + LibaioContext.freeBuffer(bufferWrite); + } + } + + @Test + public void testLock() throws Exception { + File file = temporaryFolder.newFile("test.bin"); + + LibaioFile fileDescriptor = control.openFile(file, true); + fileDescriptor.lock(); + + fileDescriptor.close(); + } + + @Test + public void testAlloc() throws Exception { + File file = temporaryFolder.newFile("test.bin"); + + LibaioFile fileDescriptor = control.openFile(file, true); + fileDescriptor.fill(fileDescriptor.getBlockSize(), 10 * 1024 * 1024); + + fileDescriptor.close(); + } + + @Test + public void testReleaseNullBuffer() throws Exception { + boolean failed = false; + try { + LibaioContext.freeBuffer(null); + } catch (Exception expected) { + failed = true; + } + + Assert.assertTrue("Exception happened!", failed); + + } + + @Test + public void testMemset() throws Exception { + + MemorySegment memorySegment = LibaioContext.newAlignedBuffer(4096 * 8, 4096); + ByteBuffer buffer = memorySegment.asByteBuffer(); + + for (int i = 0; i < buffer.capacity(); i++) { + buffer.put((byte) 'z'); + } + + buffer.position(0); + + for (int i = 0; i < buffer.capacity(); i++) { + Assert.assertEquals((byte) 'z', buffer.get()); + } + + control.memsetBuffer(buffer); + + buffer.position(0); + + for (int i = 0; i < buffer.capacity(); i++) { + Assert.assertEquals((byte) 0, buffer.get()); + } + + LibaioContext.freeBuffer(memorySegment); + + } + + @Test + @Ignore + public void testIOExceptionConditions() throws Exception { + boolean exceptionThrown = false; + + control.close(); + control = new LibaioContext<>(LIBAIO_QUEUE_SIZE, false, true); + try { + // There is no space for a queue this huge, the native layer should throw the exception + LibaioContext newController = new LibaioContext(Integer.MAX_VALUE, false, true); + } catch (RuntimeException e) { + exceptionThrown = true; + } + + Assert.assertTrue(exceptionThrown); + exceptionThrown = false; + + try { + // this should throw an exception, we shouldn't be able to open a directory! + control.openFile(temporaryFolder.getRoot(), true); + } catch (IOException expected) { + exceptionThrown = true; + } + + Assert.assertTrue(exceptionThrown); + + exceptionThrown = false; + + LibaioFile fileDescriptor = control.openFile(temporaryFolder.newFile(), true); + fileDescriptor.close(); + try { + fileDescriptor.close(); + } catch (IOException expected) { + exceptionThrown = true; + } + + Assert.assertTrue(exceptionThrown); + + fileDescriptor = control.openFile(temporaryFolder.newFile(), true); + + MemorySegment memorySegment = fileDescriptor.newBuffer(4096); + ByteBuffer buffer = memorySegment.asByteBuffer(); + + try { + for (int i = 0; i < 4096; i++) { + buffer.put((byte) 'a'); + } + + for (int i = 0; i < LIBAIO_QUEUE_SIZE; i++) { + fileDescriptor.write(i * 4096, 4096, buffer, new TestInfo()); + } + + boolean ex = false; + try { + fileDescriptor.write(0, 4096, buffer, new TestInfo()); + } catch (Exception e) { + ex = true; + } + + Assert.assertTrue(ex); + + TestInfo[] callbacks = new TestInfo[LIBAIO_QUEUE_SIZE]; + Assert.assertEquals(LIBAIO_QUEUE_SIZE, control.poll(callbacks, LIBAIO_QUEUE_SIZE, LIBAIO_QUEUE_SIZE)); + + // it should be possible to write now after queue space being released + fileDescriptor.write(0, 4096, buffer, new TestInfo()); + Assert.assertEquals(1, control.poll(callbacks, 1, 100)); + + TestInfo errorCallback = new TestInfo(); + // odd positions will have failures through O_DIRECT + fileDescriptor.read(3, 4096, buffer, errorCallback); + Assert.assertEquals(1, control.poll(callbacks, 1, 50)); + Assert.assertTrue(callbacks[0].isError()); + Assert.assertSame(errorCallback, (callbacks[0])); + + // to help GC and the checkLeaks + callbacks = null; + errorCallback = null; + + TestInfo.checkLeaks(); + + exceptionThrown = false; + try { + LibaioContext.newAlignedBuffer(300, 4096); + } catch (RuntimeException e) { + exceptionThrown = true; + } + + Assert.assertTrue(exceptionThrown); + + exceptionThrown = false; + try { + LibaioContext.newAlignedBuffer(-4096, 4096); + } catch (RuntimeException e) { + exceptionThrown = true; + } + + Assert.assertTrue(exceptionThrown); + } finally { + LibaioContext.freeBuffer(memorySegment); + } + } + + @Test + public void testBlockedCallback() throws Exception { + final LibaioContext blockedContext = new LibaioContext(LIBAIO_QUEUE_SIZE, true, true); + Thread t = new Thread() { + @Override + public void run() { + blockedContext.poll(); + } + }; + + t.start(); + + int NUMBER_OF_BLOCKS = LIBAIO_QUEUE_SIZE * 10; + + final CountDownLatch latch = new CountDownLatch(NUMBER_OF_BLOCKS); + + File file = temporaryFolder.newFile("sub-file.txt"); + LibaioFile aioFile = blockedContext.openFile(file, true); + aioFile.fill(aioFile.getBlockSize(), NUMBER_OF_BLOCKS * 4096); + + final AtomicInteger errors = new AtomicInteger(0); + + class MyCallback implements SubmitInfo { + + @Override + public void onError(int errno, String message) { + errors.incrementAndGet(); + } + + @Override + public void done() { + latch.countDown(); + } + } + + MyCallback callback = new MyCallback(); + + MemorySegment memorySegment = LibaioContext.newAlignedBuffer(4096, 4096); + ByteBuffer buffer = memorySegment.asByteBuffer(); + + for (int i = 0; i < 4096; i++) { + buffer.put((byte) 'a'); + } + + long start = System.currentTimeMillis(); + + for (int i = 0; i < NUMBER_OF_BLOCKS; i++) { + aioFile.write(i * 4096, 4096, buffer, callback); + } + + long end = System.currentTimeMillis(); + + latch.await(); + + logger.debug("time = " + (end - start) + " writes/second=" + NUMBER_OF_BLOCKS * 1000L / (end - start)); + + blockedContext.close(); + t.join(); + } + + private void fillupFile(File file, int blocks) throws IOException { + FileOutputStream fileOutputStream = new FileOutputStream(file); + byte[] bufferWrite = new byte[4096]; + for (int i = 0; i < 4096; i++) { + bufferWrite[i] = (byte) 0; + } + + for (int i = 0; i < blocks; i++) { + fileOutputStream.write(bufferWrite); + } + + fileOutputStream.close(); + } + + static class TestInfo implements SubmitInfo { + + static final Cleaner cleaner; + + static { + Cleaner tempCleaner; + try { + tempCleaner = Cleaner.create(); + } catch (Throwable e) { + e.printStackTrace(); + tempCleaner = null; + } + cleaner = tempCleaner; + } + + static AtomicInteger count = new AtomicInteger(); + + public static void checkLeaks() throws InterruptedException { + for (int i = 0; count.get() != 0 && i < 50; i++) { + WeakReference reference = new WeakReference(new Object()); + while (reference.get() != null) { + System.gc(); + Thread.sleep(100); + } + } + Assert.assertEquals(0, count.get()); + } + + boolean error = false; + String errorMessage; + int errno; + + TestInfo() { + count.incrementAndGet(); + cleaner.register(this, count::decrementAndGet); + + } + + @Override + public void onError(int errno, String message) { + this.errno = errno; + this.errorMessage = message; + this.error = true; + } + + @Override + public void done() { + } + + public int getErrno() { + return errno; + } + + public void setErrno(int errno) { + this.errno = errno; + } + + public boolean isError() { + return error; + } + + public void setError(boolean error) { + this.error = error; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + } +} diff --git a/artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/LoadedTest.java b/artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/LoadedTest.java new file mode 100644 index 00000000000..c531fd9a858 --- /dev/null +++ b/artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/LoadedTest.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.artemis.nativo.jlibaio.test; + +import org.apache.artemis.nativo.jlibaio.LibaioContext; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Test; + +public class LoadedTest { + + private static final String OS = System.getProperty("os.name").toLowerCase(); + private static final boolean IS_LINUX = OS.startsWith("linux"); + + @Test + public void testValidateIsLoaded() { + Assume.assumeTrue(IS_LINUX); + Assert.assertTrue(LibaioContext.isLoaded()); + } + +} diff --git a/artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/OpenCloseContextTest.java b/artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/OpenCloseContextTest.java new file mode 100644 index 00000000000..88a28a36233 --- /dev/null +++ b/artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/OpenCloseContextTest.java @@ -0,0 +1,186 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.artemis.nativo.jlibaio.test; + +import org.apache.artemis.nativo.jlibaio.LibaioContext; +import org.apache.artemis.nativo.jlibaio.LibaioFile; +import org.apache.artemis.nativo.jlibaio.SubmitInfo; +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.lang.foreign.MemorySegment; +import java.nio.ByteBuffer; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class OpenCloseContextTest { + + Logger logger = LoggerFactory.getLogger(OpenCloseContextTest.class); + + @BeforeClass + public static void testAIO() { + Assume.assumeTrue(LibaioContext.isLoaded()); + } + + @Rule + public TemporaryFolder folder; + + public OpenCloseContextTest() { + folder = new TemporaryFolder(new File("./target")); + } + + @Test + public void testRepeatOpenCloseContext() throws Exception { + MemorySegment memorySegment = LibaioContext.newAlignedBuffer(512, 512); + ByteBuffer buffer = memorySegment.asByteBuffer(); + for (int i = 0; i < 512; i++) { + buffer.put((byte) 'x'); + } + + for (int i = 0; i < 10; i++) { + logger.debug("#test " + i); + final LibaioContext control = new LibaioContext<>(5, true, true); + Thread t = new Thread() { + @Override + public void run() { + control.poll(); + } + }; + t.start(); + LibaioFile file = control.openFile(folder.newFile(), true); + file.fill(file.getBlockSize(), 4 * 1024); + final CountDownLatch insideMethod = new CountDownLatch(1); + final CountDownLatch awaitInside = new CountDownLatch(1); + file.write(0, 512, buffer, new SubmitInfo() { + @Override + public void onError(int errno, String message) { + + } + + @Override + public void done() { + insideMethod.countDown(); + try { + awaitInside.await(); + } catch (Throwable e) { + e.printStackTrace(); + } + logger.debug("done"); + } + }); + + insideMethod.await(); + + file.write(512, 512, buffer, new SubmitInfo() { + @Override + public void onError(int errno, String message) { + } + + @Override + public void done() { + } + }); + + awaitInside.countDown(); + control.close(); + + t.join(); + } + + } + + @Test + public void testRepeatOpenCloseContext2() throws Exception { + MemorySegment memorySegment = LibaioContext.newAlignedBuffer(512, 512); + ByteBuffer buffer = memorySegment.asByteBuffer(); + for (int i = 0; i < 512; i++) { + buffer.put((byte) 'x'); + } + + for (int i = 0; i < 10; i++) { + logger.debug("#test " + i); + final LibaioContext control = new LibaioContext<>(5, true, true); + Thread t = new Thread() { + @Override + public void run() { + control.poll(); + } + }; + t.start(); + LibaioFile file = control.openFile(folder.newFile(), true); + file.fill(file.getBlockSize(), 4 * 1024); + final CountDownLatch insideMethod = new CountDownLatch(1); + final CountDownLatch awaitInside = new CountDownLatch(1); + file.write(0, 512, buffer, new SubmitInfo() { + @Override + public void onError(int errno, String message) { + + } + + @Override + public void done() { + insideMethod.countDown(); + try { + awaitInside.await(100, TimeUnit.MILLISECONDS); + } catch (Throwable e) { + e.printStackTrace(); + } + logger.debug("done"); + } + }); + + insideMethod.await(); + + file.write(512, 512, buffer, new SubmitInfo() { + @Override + public void onError(int errno, String message) { + } + + @Override + public void done() { + } + }); + + awaitInside.countDown(); + + control.close(); + + t.join(); + } + + } + + @Test + public void testCloseAndStart() throws Exception { + final LibaioContext control2 = new LibaioContext<>(5, true, true); + + final LibaioContext control = new LibaioContext<>(5, true, true); + control.close(); + control.poll(); + + control2.close(); + control2.poll(); + } + +} diff --git a/artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/ReusableLatch.java b/artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/ReusableLatch.java new file mode 100644 index 00000000000..9d88e6189b1 --- /dev/null +++ b/artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/ReusableLatch.java @@ -0,0 +1,135 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.artemis.nativo.jlibaio.test; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.AbstractQueuedSynchronizer; + +/** + *

This class will use the framework provided to by AbstractQueuedSynchronizer.

+ *

AbstractQueuedSynchronizer is the framework for any sort of concurrent synchronization, such as Semaphores, events, etc, based on AtomicIntegers.

+ * + *

This class works just like CountDownLatch, with the difference you can also increase the counter

+ * + *

It could be used for sync points when one process is feeding the latch while another will wait when everything is done. (e.g. waiting IO completions to finish)

+ * + *

On ActiveMQ Artemis we have the requirement of increment and decrement a counter until the user fires a ready event (commit). At that point we just act as a regular countDown.

+ * + *

Note: This latch is reusable. Once it reaches zero, you can call up again, and reuse it on further waits.

+ * + *

For example: prepareTransaction will wait for the current completions, and further adds will be called on the latch. Later on when commit is called you can reuse the same latch.

+ */ +public class ReusableLatch { + + /** + * Look at the doc and examples provided by AbstractQueuedSynchronizer for more information + * + * @see AbstractQueuedSynchronizer + */ + @SuppressWarnings("serial") + private static class CountSync extends AbstractQueuedSynchronizer { + + private CountSync(int count) { + setState(count); + } + + public int getCount() { + return getState(); + } + + public void setCount(final int count) { + setState(count); + } + + @Override + public int tryAcquireShared(final int numberOfAqcquires) { + return getState() == 0 ? 1 : -1; + } + + public void add() { + for (; ; ) { + int actualState = getState(); + int newState = actualState + 1; + if (compareAndSetState(actualState, newState)) { + return; + } + } + } + + @Override + public boolean tryReleaseShared(final int numberOfReleases) { + for (; ; ) { + int actualState = getState(); + if (actualState == 0) { + return true; + } + + int newState = actualState - numberOfReleases; + + if (newState < 0) { + newState = 0; + } + + if (compareAndSetState(actualState, newState)) { + return newState == 0; + } + } + } + } + + private final CountSync control; + + public ReusableLatch() { + this(0); + } + + public ReusableLatch(final int count) { + control = new CountSync(count); + } + + public int getCount() { + return control.getCount(); + } + + public void setCount(final int count) { + control.setCount(count); + } + + public void countUp() { + control.add(); + } + + public void countDown() { + control.releaseShared(1); + } + + public void countDown(final int count) { + control.releaseShared(count); + } + + public void await() throws InterruptedException { + control.acquireSharedInterruptibly(1); + } + + public boolean await(final long milliseconds) throws InterruptedException { + return control.tryAcquireSharedNanos(1, TimeUnit.MILLISECONDS.toNanos(milliseconds)); + } + + public boolean await(final long timeWait, TimeUnit timeUnit) throws InterruptedException { + return control.tryAcquireSharedNanos(1, timeUnit.toNanos(timeWait)); + } +} diff --git a/artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/ffm/FFMNativeHelperTest.java b/artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/ffm/FFMNativeHelperTest.java new file mode 100644 index 00000000000..a5fce2e46d0 --- /dev/null +++ b/artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/ffm/FFMNativeHelperTest.java @@ -0,0 +1,442 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.artemis.nativo.jlibaio.test.ffm; + +import org.apache.artemis.nativo.jlibaio.SubmitInfo; +import org.apache.artemis.nativo.jlibaio.ffm.FFMNativeHelper; +import org.apache.artemis.nativo.jlibaio.ffm.IOControl; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.SymbolLookup; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Random; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import static org.apache.artemis.nativo.jlibaio.ffm.FFMHandles.LIBAIO; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class FFMNativeHelperTest { + + private static final Logger logger = LoggerFactory.getLogger(FFMNativeHelperTest.class); + + @Test + @EnabledOnOs(OS.LINUX) + public void libLoadInittest() { + logger.trace("@Test:: libLoadInittest"); + String libName = System.getProperty("libaio.path", "libaio.so.1"); + SymbolLookup libaio = SymbolLookup.libraryLookup(libName, Arena.global()); + assertTrue(libaio.find("io_setup").isPresent()); + } + + @Test + @EnabledOnOs(OS.LINUX) + public void libLoadtest() { + logger.trace("@Test:: libLoadtest"); + assertTrue(LIBAIO.find("io_setup").isPresent()); + } + + @Test + @EnabledOnOs(OS.LINUX) + public void testOpenCloseLifecycle() throws IOException, InterruptedException { + logger.trace("@Test:: testOpenCloseLifecycle"); + Path testFile = Path.of("libaio-test.bin"); + logger.info("Testing file: {}", testFile.toAbsolutePath()); + try { + int fd = FFMNativeHelper.open(testFile.toString(), false); + long allocate = 16 * 1024 * 1024L; + FFMNativeHelper.fallocate(fd, allocate); + long size = FFMNativeHelper.getSize(fd); + assertEquals(allocate, size, "file size mismatch"); + + fd = FFMNativeHelper.open(testFile.toString(), true); + assertTrue(fd >= 0, "Failed to open with O_DIRECT"); + logger.info("Opened fd={} WITH O_DIRECT", fd); + + FFMNativeHelper.close(fd); + } finally { + // Cleanup + Files.deleteIfExists(testFile); + } + } + + @Test + @EnabledOnOs(OS.LINUX) + public void getBlockSizeFDTest() throws IOException { + logger.trace("@Test:: getBlockSizeFDTest"); + Path testFile = Path.of("libaio-test.bin"); + logger.info("Testing file: {}", testFile.toAbsolutePath()); + try { + int fd = FFMNativeHelper.open(testFile.toString(), false); + long blockSize = FFMNativeHelper.getBlockSizeFD(fd); + assertTrue(blockSize > 512 && blockSize < 65536, "Invalid blockSize = " + blockSize); + FFMNativeHelper.close(fd); + } finally { + // Cleanup + Files.deleteIfExists(testFile); + } + } + + @Test + @EnabledOnOs(OS.LINUX) + public void getBlockSizeTest() throws IOException { + logger.trace("@Test:: getBlockSizeTest"); + Path testFile = Path.of("libaio-test.bin"); + Files.write(testFile, new byte[4096]); + logger.info("Testing file: {}", testFile.toAbsolutePath()); + try { + int fd = FFMNativeHelper.open(testFile.toString(), false); + int fdBlockSize = FFMNativeHelper.getBlockSizeFD(fd); + FFMNativeHelper.close(fd); + + int pathBlockSize = FFMNativeHelper.getBlockSize(testFile.toString()); + assertEquals(fdBlockSize, pathBlockSize, "FD vs Path block size mismatch"); + } finally { + // Cleanup + Files.deleteIfExists(testFile); + } + } + + @Test + @EnabledOnOs(OS.LINUX) + public void memsetBufferTest() throws IOException { + logger.trace("@Test:: memsetBufferTest"); + int blockSize = 4096; + ByteBuffer buffer = ByteBuffer.allocateDirect(blockSize * 2); + byte[] garbage = new byte[blockSize]; + new Random().nextBytes(garbage); + buffer.put(garbage); + + FFMNativeHelper.memsetBuffer(buffer, blockSize); + for (int i = 0; i < blockSize; i++) { + assertEquals(0, buffer.get(i), "Byte " + i + " is not Zeroed"); + } + } + + @Test + @EnabledOnOs(OS.LINUX) + public void newAlignedBufferTest() throws IOException { + logger.trace("@Test:: newAlignedBufferTest"); + int alignment = 4096; + int size = alignment * 4; + + ByteBuffer buffer = FFMNativeHelper.newAlignedBuffer(size, alignment).asByteBuffer(); + assertTrue(buffer.isDirect()); + assertEquals(size, buffer.capacity()); + + MemorySegment addr = MemorySegment.ofBuffer(buffer); + long address = addr.address(); + + assertEquals(0, address % alignment, "Buffer not aligned to " + alignment); + } + + @Test + @EnabledOnOs(OS.LINUX) + public void testNewContextDeleteContextLifecycle() throws IOException { + logger.trace("@Test:: testNewContextDeleteContextLifecycle"); + FFMNativeHelper helper = new FFMNativeHelper<>(null); + IOControl context = null; + context = helper.newContext(10); + assertNotNull(context, "Context should not be null"); + logger.info("Created context = {}", context); + + helper.deleteContext(context); + logger.info("Context deleted successfully"); + } + + @Test + @EnabledOnOs(OS.LINUX) + public void testSubmitWriteReadFullCycle() throws IOException, InterruptedException { + logger.trace("@Test:: testSubmitWriteReadFullCycle"); + Path testFile = Path.of("aio-cycle-test.bin"); + FFMNativeHelper helper = new FFMNativeHelper<>(null); + IOControl context = null; + int fd = -1; + ByteBuffer writeBuffer = null, readBuffer = null; + try { + Files.deleteIfExists(testFile); + fd = FFMNativeHelper.open(testFile.toString(), true); + FFMNativeHelper.fallocate(fd, 4096); + + context = helper.newContext(4); + + byte[] testData = new byte[4096]; + new Random(12345).nextBytes(testData); + + writeBuffer = FFMNativeHelper.newAlignedBuffer(4096, 4096).asByteBuffer(); + writeBuffer.put(testData).flip(); + + readBuffer = FFMNativeHelper.newAlignedBuffer(4096, 4096).asByteBuffer(); + + //Write + TestSubmitInfo writeCb = new TestSubmitInfo(); + helper.submitWrite(fd, context, 0, 4096, writeBuffer, writeCb); + + int events = helper.poll(context, new TestSubmitInfo[1], 1, 1); + assertEquals(1, events); + assertTrue(writeCb.isDone()); + assertFalse(writeCb.hasError()); + + //Read + TestSubmitInfo readCb = new TestSubmitInfo(); + helper.submitRead(fd, context, 0, 4096, readBuffer, readCb); + + events = helper.poll(context, new TestSubmitInfo[1], 1, 1); + assertEquals(1, events); + assertTrue(readCb.isDone()); + assertFalse(readCb.hasError()); + + //verify data + readBuffer.position(0); + byte[] readData = new byte[4096]; + readBuffer.get(readData); + assertArrayEquals(testData, readData); + } finally { + if (context != null) { + helper.deleteContext(context); + } + if (fd >= 0) { + FFMNativeHelper.close(fd); + } + Files.deleteIfExists(testFile); + } + } + + @Test + @EnabledOnOs(OS.LINUX) + public void testPollMultipleEvents() throws IOException, InterruptedException { + logger.trace("@Test:: testPollMultipleEvents"); + Path testFile = Path.of("multi-poll-test.bin"); + FFMNativeHelper helper = new FFMNativeHelper<>(null); + IOControl context = null; + int fd = -1; + TestSubmitInfo[] callBacks = new TestSubmitInfo[4]; + MemorySegment[] nativeBuffers = new MemorySegment[4]; + ByteBuffer[] buffers = new ByteBuffer[4]; + + try { + Files.deleteIfExists(testFile); + fd = FFMNativeHelper.open(testFile.toString(), true); + FFMNativeHelper.fallocate(fd, 8192); + + context = helper.newContext(8); + + for (int i = 0; i < 4; i++) { + callBacks[i] = new TestSubmitInfo(); + nativeBuffers[i] = FFMNativeHelper.newAlignedBuffer(4096, 4096); + buffers[i] = nativeBuffers[i].asByteBuffer(); + byte[] data = new byte[2048]; + new Random(12345 + i).nextBytes(data); + buffers[i].put(data).flip(); + helper.submitWrite(fd, context, i * 2048, 2048, buffers[i], callBacks[i]); + } + + int events = helper.poll(context, callBacks, 2, 4); + assertTrue(events >= 2 && events <= 4, "Expected 2-4 events, got = " + events); + + for (TestSubmitInfo cb : callBacks) { + assertTrue(cb.isDone()); + assertFalse(cb.hasError()); + } + + } finally { + if (context != null) { + helper.deleteContext(context); + } + if (fd >= 0) { + FFMNativeHelper.close(fd); + } + Files.deleteIfExists(testFile); + for (MemorySegment buf : nativeBuffers) { + if (buf != null) { + FFMNativeHelper.freeBuffer(buf); + } + } + } + } + + @Test + @EnabledOnOs(OS.LINUX) + public void blockedPollTest() throws IOException, InterruptedException { + logger.trace("@Test:: blockedPollTest"); + Path testFile = Path.of("blocked-poll-test.bin"); + FFMNativeHelper helper = new FFMNativeHelper<>(null); + IOControl context = null; + int fd = -1; + MemorySegment nativeBuffer = null; + ByteBuffer buffer = null; + + try { + Files.deleteIfExists(testFile); + fd = FFMNativeHelper.open(testFile.toString(), true); + FFMNativeHelper.fallocate(fd, 4096); + + context = helper.newContext(2); + + TestSubmitInfo callBack = new TestSubmitInfo(); + nativeBuffer = FFMNativeHelper.newAlignedBuffer(4096, 4096); + buffer = nativeBuffer.asByteBuffer(); + buffer.put((byte) 42).flip(); + + helper.submitWrite(fd, context, 0, 4096, buffer, callBack); + final IOControl contextRef = context; + Thread pollThread = new Thread(() -> { + try { + helper.blockedPoll(contextRef, false); + } catch (Throwable e) { + logger.error("BlockedPoll failed", e); + } + }); + + pollThread.start(); + Thread.sleep(100); + pollThread.join(1000); + + assertTrue(callBack.isDone()); + } finally { + if (context != null) { + helper.deleteContext(context); + } + if (fd >= 0) { + FFMNativeHelper.close(fd); + } + Files.deleteIfExists(testFile); + if (buffer != null) { + FFMNativeHelper.freeBuffer(nativeBuffer); + } + } + } + + @Test + @EnabledOnOs(OS.LINUX) + public void fillMethodTest() throws IOException { + logger.trace("@Test:: fillMethodTest"); + Path testFile = Path.of("fill-test.bin"); + int fd = -1; + + try { + Files.deleteIfExists(testFile); + fd = FFMNativeHelper.open(testFile.toString(), false); + long size = 3 * 1024 * 1024L; + + FFMNativeHelper.fill(fd, 4096, size); + long actualSize = FFMNativeHelper.getSize(fd); + assertEquals(size, actualSize); + } finally { + if (fd >= 0) { + FFMNativeHelper.close(fd); + } + Files.deleteIfExists(testFile); + } + } + + @Test + @EnabledOnOs(OS.LINUX) + public void lockUnlockTest() throws IOException { + logger.trace("@Test:: lockUnlockTest"); + Path testFile = Path.of("lock-test.bin"); + int fd = -1; + + try { + Files.deleteIfExists(testFile); + fd = FFMNativeHelper.open(testFile.toString(), false); + + assertTrue(FFMNativeHelper.lock(fd)); + int fd2 = FFMNativeHelper.open(testFile.toString(), false); + try { + assertFalse(FFMNativeHelper.lock(fd2)); + } finally { + FFMNativeHelper.close(fd2); + } + } finally { + if (fd >= 0) { + FFMNativeHelper.close(fd); + Files.deleteIfExists(testFile); + } + } + } + + @Test + @EnabledOnOs(OS.LINUX) + public void iocbPoolExhaustionTest() throws IOException { + logger.trace("@Test:: iocbPoolExhaustionTest"); + FFMNativeHelper helper = new FFMNativeHelper<>(null); + IOControl context = helper.newContext(1); + int fd = FFMNativeHelper.open("pool-test.bin", false); + MemorySegment nativeBuffer = FFMNativeHelper.newAlignedBuffer(4096, 4096); + ByteBuffer buffer = nativeBuffer.asByteBuffer(); + try { + TestSubmitInfo cb1 = new TestSubmitInfo(); + helper.submitWrite(fd, context, 0, 4096, buffer, cb1); + + TestSubmitInfo cb2 = new TestSubmitInfo(); + assertThrows(IOException.class, () -> helper.submitWrite(fd, context, 4096, 4096, buffer, cb2)); + + helper.poll(context, new TestSubmitInfo[1], 1, 1); + + TestSubmitInfo cb3 = new TestSubmitInfo(); + helper.submitWrite(fd, context, 8192, 4096, buffer, cb3); + } finally { + FFMNativeHelper.freeBuffer(nativeBuffer); + helper.deleteContext(context); + FFMNativeHelper.close(fd); + Files.deleteIfExists(Path.of("pool-test.bin")); + } + } + + private static class TestSubmitInfo implements SubmitInfo { + + private final AtomicBoolean done = new AtomicBoolean(false); + private final AtomicBoolean error = new AtomicBoolean(false); + private final AtomicReference errorCode = new AtomicReference<>(0); + private final AtomicReference errorMessage = new AtomicReference<>(""); + + @Override + public void onError(int errno, String message) { + error.set(true); + this.errorCode.set(errno); + this.errorMessage.set(message); + } + + @Override + public void done() { + done.set(true); + } + + public boolean isDone() { + return done.get(); + } + + public boolean hasError() { + return error.get(); + } + } +} diff --git a/artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/ffm/IOCBLayoutTest.java b/artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/ffm/IOCBLayoutTest.java new file mode 100644 index 00000000000..71731650357 --- /dev/null +++ b/artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/ffm/IOCBLayoutTest.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.artemis.nativo.jlibaio.test.ffm; + +import org.apache.artemis.nativo.jlibaio.ffm.IOCBInit; +import org.junit.jupiter.api.Test; + +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; + +import static org.apache.artemis.nativo.jlibaio.ffm.IOCBInit.IOCB_LAYOUT; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class IOCBLayoutTest { + + @Test + public void iocbLayoutSizeTest() { + assertEquals(64, (int) IOCB_LAYOUT.byteSize(), "Expected 64-byte iocb"); + } + + @Test + public void iocbLayoutValueTest() { + try (Arena arena = Arena.ofConfined()) { + MemorySegment iocb = arena.allocate(IOCB_LAYOUT); + IOCBInit.setAioKey(iocb, 123); + IOCBInit.setAioFildes(iocb, 42); + IOCBInit.setAioBuf(iocb, 0x7f1234567890L); + IOCBInit.setAioNbytes(iocb, 4096); + IOCBInit.setAioFlags(iocb, 0); + + assertEquals(123, IOCBInit.getAioKey(iocb)); + assertEquals(42, IOCBInit.getAioFildes(iocb)); + assertEquals(0x7f1234567890L, IOCBInit.getAioBuf(iocb)); + assertEquals(4096, IOCBInit.getAioNbytes(iocb)); + assertEquals(0, IOCBInit.getAioFlags(iocb)); + } + } +} diff --git a/artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/ffm/IOControlTest.java b/artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/ffm/IOControlTest.java new file mode 100644 index 00000000000..2bc78fa757a --- /dev/null +++ b/artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/ffm/IOControlTest.java @@ -0,0 +1,302 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.artemis.nativo.jlibaio.test.ffm; + +import org.apache.artemis.nativo.jlibaio.ffm.IOCBInit; +import org.apache.artemis.nativo.jlibaio.ffm.IOControl; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@DisplayName("IOControl lifecycle and concurrency tests") +public class IOControlTest { + + private Arena arena; + private IOControl ioControl; + + @BeforeEach + void setUp() { + arena = Arena.ofConfined(); + + ioControl = new IOControl(); + ioControl.setIoContext(arena.allocate(8)); + ioControl.setEvents(arena.allocate(8)); + ioControl.setQueueSize(8); + + MemorySegment[] pool = new MemorySegment[8]; + for (int i = 0; i < pool.length; i++) { + pool[i] = arena.allocate(IOCBInit.IOCB_LAYOUT); + } + ioControl.setIocbPool(pool); + } + + @AfterEach + void tearDown() { + if (arena != null) { + arena.close(); + } + } + + @Test + void isValidShouldBeTrueForProperlyInitializedControl() { + assertTrue(ioControl.isValid()); + } + + @Test + void isValidShouldFailForNullContext() { + ioControl.setIoContext(MemorySegment.NULL); + assertFalse(ioControl.isValid()); + } + + @Test + void isValidShouldFailForNullEvents() { + ioControl.setEvents(MemorySegment.NULL); + assertFalse(ioControl.isValid()); + } + + @Test + void isValidShouldFailForZeroQueueSize() { + ioControl.setQueueSize(0); + assertFalse(ioControl.isValid()); + } + + @Test + void getIOCBShouldReturnDifferentSegmentsUntilQueueIsExhausted() { + Set addresses = new HashSet<>(); + + for (int i = 0; i < 8; i++) { + MemorySegment iocb = ioControl.getIOCB(); + assertNotNull(iocb); + assertTrue(iocb.address() != 0L); + addresses.add(iocb.address()); + } + + assertEquals(8, addresses.size()); + assertEquals(8, ioControl.used()); + assertEquals(0, ioControl.iocbGet()); + + assertNull(ioControl.getIOCB()); + } + + @Test + void putIOCBShouldReturnIOCBToPoolAndDecrementUsed() { + MemorySegment first = ioControl.getIOCB(); + MemorySegment second = ioControl.getIOCB(); + + assertNotNull(first); + assertNotNull(second); + assertEquals(2, ioControl.used()); + + ioControl.putIOCB(first); + assertEquals(1, ioControl.used()); + + ioControl.putIOCB(second); + assertEquals(0, ioControl.used()); + } + + @Test + void getIOCBShouldWrapAround() { + for (int i = 0; i < 8; i++) { + assertNotNull(ioControl.getIOCB()); + } + + for (int i = 0; i < 8; i++) { + ioControl.putIOCB(ioControl.iocbPool()[i]); + } + + assertEquals(0, ioControl.used()); + assertEquals(0, ioControl.iocbGet()); + assertEquals(0, ioControl.iocbPut()); + + MemorySegment again = ioControl.getIOCB(); + assertNotNull(again); + assertEquals(1, ioControl.used()); + assertEquals(1, ioControl.iocbGet()); + } + + @Test + void putIOCBShouldIgnoreNullAndInvalidSegments() { + assertDoesNotThrow(() -> ioControl.putIOCB(null)); + assertDoesNotThrow(() -> ioControl.putIOCB(MemorySegment.NULL)); + assertEquals(0, ioControl.used()); + } + + @Test + void getIOCBShouldReturnNullWhenPoolIsEmpty() { + for (int i = 0; i < 8; i++) { + assertNotNull(ioControl.getIOCB()); + } + + assertNull(ioControl.getIOCB()); + assertEquals(8, ioControl.used()); + } + + @Test + void concurrentGetAndPutShouldPreserveInvariant() throws Exception { + final int threads = 8; + final int iterationsPerThread = 5_000; + + ExecutorService executor = Executors.newFixedThreadPool(threads); + CountDownLatch start = new CountDownLatch(1); + List> tasks = new ArrayList<>(); + + for (int t = 0; t < threads; t++) { + tasks.add(() -> { + start.await(); + + for (int i = 0; i < iterationsPerThread; i++) { + MemorySegment iocb = ioControl.getIOCB(); + if (iocb != null) { + ioControl.putIOCB(iocb); + } + } + return null; + }); + } + + try { + List> futures = new ArrayList<>(); + for (Callable task : tasks) { + futures.add(executor.submit(task)); + } + + start.countDown(); + + for (Future future : futures) { + future.get(30, TimeUnit.SECONDS); + } + + assertTrue(ioControl.isValid()); + assertEquals(0, ioControl.used()); + assertEquals(0, ioControl.iocbGet()); + assertEquals(0, ioControl.iocbPut()); + + MemorySegment[] pool = ioControl.iocbPool(); + assertNotNull(pool); + assertEquals(8, pool.length); + + Set addresses = new HashSet<>(); + for (MemorySegment seg : pool) { + assertNotNull(seg); + assertTrue(seg.address() != 0L); + addresses.add(seg.address()); + } + assertEquals(8, addresses.size()); + } finally { + executor.shutdownNow(); + assertTrue(executor.awaitTermination(10, TimeUnit.SECONDS)); + } + } + + @Test + void concurrentGetShouldNeverReturnSameIOCBTwiceWithoutPut() throws Exception { + final int threads = 8; + ExecutorService executor = Executors.newFixedThreadPool(threads); + CountDownLatch start = new CountDownLatch(1); + + try { + List> futures = new ArrayList<>(); + for (int i = 0; i < threads; i++) { + futures.add(executor.submit(() -> { + start.await(); + return ioControl.getIOCB(); + })); + } + + start.countDown(); + + Set addresses = new HashSet<>(); + int nonNullCount = 0; + + for (Future future : futures) { + MemorySegment seg = future.get(10, TimeUnit.SECONDS); + if (seg != null) { + nonNullCount++; + addresses.add(seg.address()); + } + } + + assertEquals(nonNullCount, addresses.size()); + assertTrue(ioControl.used() <= ioControl.queueSize()); + assertTrue(ioControl.isValid()); + } finally { + executor.shutdownNow(); + assertTrue(executor.awaitTermination(10, TimeUnit.SECONDS)); + } + } + + @Test + void concurrentPutShouldBeSafeAfterPreallocation() throws Exception { + MemorySegment[] taken = new MemorySegment[8]; + for (int i = 0; i < 8; i++) { + taken[i] = ioControl.getIOCB(); + assertNotNull(taken[i]); + } + assertEquals(8, ioControl.used()); + + final int threads = 8; + ExecutorService executor = Executors.newFixedThreadPool(threads); + CountDownLatch start = new CountDownLatch(1); + + try { + List> futures = new ArrayList<>(); + for (int i = 0; i < threads; i++) { + final MemorySegment seg = taken[i]; + futures.add(executor.submit(() -> { + start.await(); + ioControl.putIOCB(seg); + return null; + })); + } + + start.countDown(); + + for (Future future : futures) { + future.get(10, TimeUnit.SECONDS); + } + + assertEquals(0, ioControl.used()); + assertEquals(0, ioControl.iocbGet()); + assertEquals(0, ioControl.iocbPut()); + assertTrue(ioControl.isValid()); + } finally { + executor.shutdownNow(); + assertTrue(executor.awaitTermination(10, TimeUnit.SECONDS)); + } + } +} diff --git a/artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/jmh/AioCompareBenchmark.java b/artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/jmh/AioCompareBenchmark.java new file mode 100644 index 00000000000..329dc3886ba --- /dev/null +++ b/artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/jmh/AioCompareBenchmark.java @@ -0,0 +1,193 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.artemis.nativo.jlibaio.test.jmh; + +import org.apache.artemis.nativo.jlibaio.LibaioContext; +import org.apache.artemis.nativo.jlibaio.LibaioFile; +import org.apache.artemis.nativo.jlibaio.SubmitInfo; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; + +import java.io.File; +import java.lang.foreign.MemorySegment; +import java.nio.ByteBuffer; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +@State(Scope.Benchmark) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Fork(value = 2) +@Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 10, time = 200, timeUnit = TimeUnit.MILLISECONDS) +public class AioCompareBenchmark { + + private static final int FILE_SIZE = 10000 * 4096; + private static final int BLOCK_SIZE = 4096; + + @Param({"2048"}) + private int LIBAIO_QUEUE_SIZE; + + @Param({"10000"}) + private int recordCount; + + private File file; + private LibaioContext control; + private LibaioFile libaioFile; + + private MemorySegment headerSegment; + private ByteBuffer headerBuffer; + + private MemorySegment recordSegment; + private ByteBuffer recordBuffer; + + private final AtomicReference currentLatch = new AtomicReference<>(); + + private Thread pollThread; + private volatile boolean polling = true; + + private final SubmitInfo callback = new SubmitInfo() { + @Override + public void onError(int errno, String message) { + //ignore + } + + @Override + public void done() { + CountDownLatch latch = currentLatch.get(); + if (latch != null) { + latch.countDown(); + } + } + }; + + private long fileId = 1L; + + @Setup(Level.Trial) + public void setuo() throws Exception { + file = File.createTempFile("aio-bench-", ".dat"); + + control = new LibaioContext<>(LIBAIO_QUEUE_SIZE, true, true); + libaioFile = control.openFile(file, true); + + //one-time file initialization + libaioFile.fallocate(FILE_SIZE); + + headerSegment = LibaioContext.newAlignedBuffer(BLOCK_SIZE, BLOCK_SIZE); + headerBuffer = headerSegment.asByteBuffer(); + + recordSegment = LibaioContext.newAlignedBuffer(BLOCK_SIZE, BLOCK_SIZE); + recordBuffer = recordSegment.asByteBuffer(); + + initRecord(headerBuffer); // filling the record clock with 1 + initRecord(recordBuffer); // filling the record clock with 1 + + fillHeader(fileId); + updateRecord(recordBuffer, fileId, 0L); + + polling = true; + pollThread = new Thread(() -> { + while (polling && !Thread.currentThread().isInterrupted()) { + try { + control.poll(); + } catch (Throwable e) { + if (polling) { + throw new RuntimeException(e); + } + break; + } + } + }, "aio-jmh-poll-thread"); + pollThread.setDaemon(true); + pollThread.start(); + } + + @TearDown(Level.Trial) + public void tearDown() throws Exception { + polling = false; + if (pollThread != null) { + pollThread.interrupt(); + pollThread.join(TimeUnit.SECONDS.toMillis(10)); + } + + if (libaioFile != null) { + libaioFile.close(); + } + if (control != null) { + control.close(); + } + if (headerSegment != null && headerSegment.address() != 0) { + LibaioContext.freeBuffer(headerSegment); + } + if (recordSegment != null && recordSegment.address() != 0) { + LibaioContext.freeBuffer(recordSegment); + } + if (file != null) { + file.delete(); + } + } + + @Benchmark + public void writeHeaderAndRecord() throws Exception { + CountDownLatch latch = new CountDownLatch(recordCount * 100); + currentLatch.set(latch); + + try { + // fillHeader(fileId); + // libaioFile.write(0L, BLOCK_SIZE, headerBuffer, callback); + + for (int j = 0; j < 100; j++) { + for (int i = 0; i < recordCount; i++) { + updateRecord(recordBuffer, fileId, i); + long offset = BLOCK_SIZE + ((long) i * BLOCK_SIZE); + libaioFile.write(offset, BLOCK_SIZE, recordBuffer, callback); + } + } + + latch.await(); + } finally { + currentLatch.compareAndSet(latch, null); + } + } + + private void fillHeader(long fileId) { + headerBuffer.putLong(0, fileId); + } + + private void updateRecord(ByteBuffer buffer, long fileId, long recordId) { + buffer.putLong(0, fileId); + buffer.putLong(8, recordId); + } + + private void initRecord(ByteBuffer record) { + while (record.position() < BLOCK_SIZE) { + record.put((byte) 1); + } + } +} diff --git a/artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/jmh/BenchmarkRunner.java b/artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/jmh/BenchmarkRunner.java new file mode 100644 index 00000000000..29b66d6a245 --- /dev/null +++ b/artemis-ffm/src/test/java/org/apache/artemis/nativo/jlibaio/test/jmh/BenchmarkRunner.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.artemis.nativo.jlibaio.test.jmh; + +import org.openjdk.jmh.Main; + +import java.io.IOException; + +public class BenchmarkRunner { + + public static void main(String[] args) throws IOException { + Main.main(args); + } +} diff --git a/pom.xml b/pom.xml index 64a973ca016..51bf01a8ccc 100644 --- a/pom.xml +++ b/pom.xml @@ -74,6 +74,7 @@ artemis-lockmanager artemis-image artemis-image/examples + artemis-ffm Apache Artemis @@ -750,6 +751,15 @@ + + jdk22-plus + + [22,) + + + artemis-ffm + +