diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 54dd28139ae..524693b4fad 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -55,6 +55,11 @@ jobs:
java-version: ${{ matrix.java }}
distribution: 'temurin'
+ - name: Install System libaio Packages
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y libaio1 libaio-dev
+
# use 'install' so smoke-tests will work
# By setting anything to org.apache.activemq.artemis.core.io.aio.AIOSequentialFileFactory.DISABLED we are disabling libaio loading on the testsuite
- name: Fast Tests
diff --git a/artemis-bom/pom.xml b/artemis-bom/pom.xml
index 9902e2fdee0..3e568e7f0df 100644
--- a/artemis-bom/pom.xml
+++ b/artemis-bom/pom.xml
@@ -166,6 +166,11 @@
artemis-jms-server${project.version}
+
+ org.apache.artemis
+ artemis-ffm
+ ${project.version}
+ org.apache.artemisartemis-journal
diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/Create.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/Create.java
index f84e18647d4..ae7c6a9c34d 100644
--- a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/Create.java
+++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/Create.java
@@ -238,6 +238,9 @@ public class Create extends InstallAbstract {
@Option(names = "--aio", description = "Set the journal as asyncio.")
private boolean aio;
+ @Option(names = "--aio2", description = "Set the journal as asyncio 2 (Panama FFM).")
+ private boolean aio2;
+
@Option(names = "--nio", description = "Set the journal as nio.")
private boolean nio;
@@ -1010,7 +1013,7 @@ private void setupJournalType() {
aio = false;
}
}
- int countJournalTypes = countBoolean(aio, nio, mapped);
+ int countJournalTypes = countBoolean(aio2, aio, nio, mapped);
if (countJournalTypes > 1) {
throw new RuntimeException("You can only select one journal type (--nio | --aio | --mapped).");
}
@@ -1023,7 +1026,9 @@ private void setupJournalType() {
}
}
- if (aio) {
+ if (aio2) {
+ journalType = JournalType.ASYNCIO_2;
+ } else if (aio) {
journalType = JournalType.ASYNCIO;
} else if (nio) {
journalType = JournalType.NIO;
diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/util/SyncCalculation.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/util/SyncCalculation.java
index 42a4732a96f..f09f47ff603 100644
--- a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/util/SyncCalculation.java
+++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/util/SyncCalculation.java
@@ -28,6 +28,7 @@
import org.apache.activemq.artemis.core.io.SequentialFile;
import org.apache.activemq.artemis.core.io.SequentialFileFactory;
import org.apache.activemq.artemis.core.io.aio.AIOSequentialFileFactory;
+import org.apache.activemq.artemis.core.io.aio2.AIO2Helper;
import org.apache.activemq.artemis.core.io.mapped.MappedSequentialFileFactory;
import org.apache.activemq.artemis.core.io.nio.NIOSequentialFileFactory;
import org.apache.activemq.artemis.core.server.ActiveMQMessageBundle;
@@ -247,7 +248,12 @@ private static SequentialFileFactory newFactory(File datafolder, boolean datasyn
case ASYNCIO:
factory = new AIOSequentialFileFactory(datafolder, maxAIO).setDatasync(datasync);
factory.start();
- ((AIOSequentialFileFactory) factory).disableBufferReuse();
+ factory.disableBufferReuse();
+ return factory;
+ case ASYNCIO_2:
+ factory = AIO2Helper.getAIO2SequentialFileFactory(datafolder, maxAIO).setDatasync(datasync);
+ factory.start();
+ factory.disableBufferReuse();
return factory;
case MAPPED:
factory = new MappedSequentialFileFactory(datafolder, fileSize, false, 0, 0, null)
diff --git a/artemis-features/src/main/resources/features.xml b/artemis-features/src/main/resources/features.xml
index c00193f8e64..18c59be3787 100644
--- a/artemis-features/src/main/resources/features.xml
+++ b/artemis-features/src/main/resources/features.xml
@@ -85,6 +85,7 @@
mvn:org.snakeyaml/snakeyaml-engine/${snakeyaml-engine.version}mvn:org.apache.activemq/activemq-artemis-native/${activemq-artemis-native-version}
+ mvn:org.apache.artemis/artemis-ffm/${pom.version}mvn:org.apache.artemis/artemis-lockmanager-api/${pom.version}mvn:org.apache.artemis/artemis-server-osgi/${pom.version}
diff --git a/artemis-ffm/pom.xml b/artemis-ffm/pom.xml
new file mode 100644
index 00000000000..c7a9738e292
--- /dev/null
+++ b/artemis-ffm/pom.xml
@@ -0,0 +1,184 @@
+
+
+ 4.0.0
+
+ org.apache.artemis
+ artemis-pom
+ 2.55.0-SNAPSHOT
+ ../artemis-pom/pom.xml
+
+
+ artemis-ffm
+ jar
+ Apache Artemis FFM
+
+
+ 5000
+
+ UTF-8
+ UTF-8
+
+
+ -Dtest.stress.time=${test.stress.time} --enable-native-access=ALL-UNNAMED
+
+
+
+
+
+ org.slf4j
+ slf4j-api
+ ${slf4j.version}
+ provided
+
+
+
+
+ junit
+ junit
+ test
+
+
+
+ org.junit.vintage
+ junit-vintage-engine
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
+
+ org.slf4j
+ slf4j-simple
+ ${slf4j.version}
+ test
+
+
+
+
+
+ jdk24onwards
+
+ [24,)
+
+
+
+
+ maven-compiler-plugin
+
+ 24
+
+
+
+ java24-compile
+ compile
+
+ compile
+
+
+ ${project.build.outputDirectory}
+
+ ${project.basedir}/src/main/java24
+
+
+
+
+ java24-test-compile
+ test-compile
+
+ testCompile
+
+
+ ${project.basedir}/src/test/java24
+
+
+
+
+
+ maven-jar-plugin
+
+
+
+ default-jar
+ package
+
+ jar
+
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${basedir}/target/output/
+
+
+
+
+ org.apache.felix
+ maven-bundle-plugin
+ 5.1.9
+
+
+ bundle-manifest
+
+ manifest
+
+ process-classes
+
+
+ default-bundle
+
+ bundle
+
+ package
+
+
+
+
+ org.apache.artemis.ffm
+ ${project.version}
+ org.apache.artemis.nativo.jlibaio;version="${project.version}"
+
+ !java.lang.foreign,*
+ *
+
+ osgi.ee;filter:="(&(osgi.ee=JavaSE)(version=17))"
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/artemis-journal/src/main/java/org/apache/activemq/artemis/core/io/aio/ActiveMQFileLock.java b/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/LibaioContext.java
similarity index 61%
rename from artemis-journal/src/main/java/org/apache/activemq/artemis/core/io/aio/ActiveMQFileLock.java
rename to artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/LibaioContext.java
index e44b9362081..f2c59a09d3a 100644
--- a/artemis-journal/src/main/java/org/apache/activemq/artemis/core/io/aio/ActiveMQFileLock.java
+++ b/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/LibaioContext.java
@@ -14,30 +14,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.activemq.artemis.core.io.aio;
+package org.apache.artemis.nativo.jlibaio;
+import java.io.Closeable;
import java.io.IOException;
-import java.nio.channels.FileChannel;
-import java.nio.channels.FileLock;
-import org.apache.activemq.artemis.nativo.jlibaio.LibaioFile;
+public class LibaioContext implements Closeable {
-public class ActiveMQFileLock extends FileLock {
-
- private final LibaioFile file;
-
- public ActiveMQFileLock(final LibaioFile handle) {
- super((FileChannel) null, 0, 0, false);
- this.file = handle;
- }
-
- @Override
- public boolean isValid() {
- return true;
+ public static boolean isSupported() {
+ return false;
}
@Override
- public void release() throws IOException {
- file.close();
+ public void close() throws IOException {
}
}
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..8ee2eb76cd9
--- /dev/null
+++ b/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/LibaioFile.java
@@ -0,0 +1,40 @@
+/*
+ * 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 java.io.Closeable;
+import java.io.IOException;
+
+public class LibaioFile implements Closeable {
+
+ private final String errorMsg = "This is not supported for JDK < 24.";
+
+ LibaioFile(int fd, LibaioContext ctx) {
+ }
+
+ public int getBlockSize() throws IOException {
+ throw new UnsupportedOperationException(errorMsg);
+ }
+
+ public void fill(int alignment, long size) throws IOException {
+ throw new UnsupportedOperationException(errorMsg);
+ }
+
+ @Override
+ public void close() throws IOException {
+ }
+}
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/package-info.java b/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/package-info.java
new file mode 100644
index 00000000000..4fc230206dd
--- /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 {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/main/java24/org/apache/artemis/nativo/jlibaio/LibaioContext.java b/artemis-ffm/src/main/java24/org/apache/artemis/nativo/jlibaio/LibaioContext.java
new file mode 100644
index 00000000000..ec88df24cce
--- /dev/null
+++ b/artemis-ffm/src/main/java24/org/apache/artemis/nativo/jlibaio/LibaioContext.java
@@ -0,0 +1,489 @@
+/*
+ * 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 final AtomicBoolean shuttingDown = new AtomicBoolean(false);
+
+ private static final AtomicInteger contexts = new AtomicInteger(0);
+
+ public static boolean isSupported() {
+ return true;
+ }
+
+ 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/java24/org/apache/artemis/nativo/jlibaio/LibaioFile.java b/artemis-ffm/src/main/java24/org/apache/artemis/nativo/jlibaio/LibaioFile.java
new file mode 100644
index 00000000000..bf7a30cc428
--- /dev/null
+++ b/artemis-ffm/src/main/java24/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/java24/org/apache/artemis/nativo/jlibaio/ffm/AIORing.java b/artemis-ffm/src/main/java24/org/apache/artemis/nativo/jlibaio/ffm/AIORing.java
new file mode 100644
index 00000000000..fcf54a08a31
--- /dev/null
+++ b/artemis-ffm/src/main/java24/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/java24/org/apache/artemis/nativo/jlibaio/ffm/Constants.java b/artemis-ffm/src/main/java24/org/apache/artemis/nativo/jlibaio/ffm/Constants.java
new file mode 100644
index 00000000000..cf719c67494
--- /dev/null
+++ b/artemis-ffm/src/main/java24/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/java24/org/apache/artemis/nativo/jlibaio/ffm/FFMHandles.java b/artemis-ffm/src/main/java24/org/apache/artemis/nativo/jlibaio/ffm/FFMHandles.java
new file mode 100644
index 00000000000..b04306e7837
--- /dev/null
+++ b/artemis-ffm/src/main/java24/org/apache/artemis/nativo/jlibaio/ffm/FFMHandles.java
@@ -0,0 +1,126 @@
+/*
+ * 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;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class FFMHandles {
+
+ private static final Logger logger = LoggerFactory.getLogger(FFMHandles.class);
+ 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) {
+ try {
+ SymbolLookup loopup = SymbolLookup.libraryLookup(path, Arena.global());
+ if (loopup != null) {
+ logger.info("libc.so.6 found at {}", path);
+ return loopup;
+ }
+ } catch (IllegalArgumentException | SecurityException e) {
+ logger.warn("libc.so.6 not found", e);
+ }
+ }
+ logger.warn("libc.so.6 not found");
+ 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()) {
+ try {
+ SymbolLookup lookup = SymbolLookup.libraryLookup(path, Arena.global());
+ if (lookup != null) {
+ logger.info("libaio.so.1 found at {}", path);
+ return lookup;
+ }
+ } catch (IllegalArgumentException | SecurityException e) {
+ logger.warn("libaio.so.1 not found", e);
+ }
+ }
+ }
+ logger.warn("libaio.so.1 not found");
+ throw new RuntimeException("libaio.so.1 not found");
+ }
+}
diff --git a/artemis-ffm/src/main/java24/org/apache/artemis/nativo/jlibaio/ffm/FFMNativeHelper.java b/artemis-ffm/src/main/java24/org/apache/artemis/nativo/jlibaio/ffm/FFMNativeHelper.java
new file mode 100644
index 00000000000..e877b747b01
--- /dev/null
+++ b/artemis-ffm/src/main/java24/org/apache/artemis/nativo/jlibaio/ffm/FFMNativeHelper.java
@@ -0,0 +1,1133 @@
+/*
+ * 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) {
+ if (logger.isTraceEnabled()) {
+ 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);
+ if (logger.isDebugEnabled()) {
+ 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);
+ }
+ if (logger.isDebugEnabled()) {
+ 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) {
+ if (logger.isDebugEnabled()) {
+ 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);
+
+ if (logger.isDebugEnabled()) {
+ 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 {
+ if (logger.isTraceEnabled()) {
+ 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);
+ if (logger.isTraceEnabled()) {
+ 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) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("deleteContext: null ioControl");
+ }
+ return;
+ }
+ if (!ioControl.isValid()) {
+ logger.warn("deleteContext: invalid ioControl");
+ return;
+ }
+ if (logger.isDebugEnabled()) {
+ 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 {
+ if (logger.isDebugEnabled()) {
+ 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) {
+ if (logger.isTraceEnabled()) {
+ logger.trace("deleteContext: drain complete (result={})", result);
+ }
+ break;
+ }
+ if (logger.isDebugEnabled()) {
+ 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;
+ }
+ }
+ if (logger.isTraceEnabled()) {
+ 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());
+ if (logger.isDebugEnabled()) {
+ 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;
+ if (logger.isDebugEnabled()) {
+ 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);
+ }
+ if (logger.isDebugEnabled()) {
+ 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);
+ }
+ if (logger.isDebugEnabled()) {
+ 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);
+ if (logger.isDebugEnabled()) {
+ 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);
+ ioPrepPOp(iocb, fd, MemorySegment.ofBuffer(bufferWrite.duplicate().clear()), 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);
+ ioPrepPOp(iocb, fd, MemorySegment.ofBuffer(bufferWrite.duplicate().clear()), 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);
+ if (logger.isTraceEnabled()) {
+ 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) {
+ if (logger.isDebugEnabled()) {
+ 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()) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("blockedPoll: context destroyed - self-exit");
+ }
+ break;
+ }
+ int result = ringioGetEvents(ioControl.ioContext(), ioControl.events(), 1, ioControl.queueSize(), null);
+ if (result == -4) {
+ if (logger.isTraceEnabled()) {
+ 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);
+ }
+
+ if (logger.isTraceEnabled()) {
+ 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) {
+ if (logger.isTraceEnabled()) {
+ logger.trace("blockedPoll: shutdown signal detected (dumb fd={})", fd);
+ }
+ ioControl.putIOCB(iocbp);
+ running = false;
+ break;
+ }
+
+ int eventResult = (int) event.get(ValueLayout.JAVA_LONG, 16L);
+
+ if (useFdatasync && lastFile != fd) {
+ try {
+ fdatasync(arena, fd);
+ } catch (Throwable t) {
+ String errorMessage = "fdatasync failed: " + t.getMessage();
+ logger.warn("blockedPoll: {}", errorMessage);
+ }
+ }
+
+ 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);
+ }
+ });
+ if (logger.isDebugEnabled()) {
+ 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();
+ if (logger.isTraceEnabled()) {
+ 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);
+ if (logger.isTraceEnabled()) {
+ 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);
+ if (logger.isDebugEnabled()) {
+ 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;
+ }
+ if (logger.isTraceEnabled()) {
+ 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;
+ }
+ if (logger.isTraceEnabled()) {
+ 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);
+ }
+ if (logger.isDebugEnabled()) {
+ 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) {
+ if (logger.isDebugEnabled()) {
+ 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 {
+ if (logger.isDebugEnabled()) {
+ 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);
+ }
+ if (logger.isDebugEnabled()) {
+ 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/java24/org/apache/artemis/nativo/jlibaio/ffm/IOCBInit.java b/artemis-ffm/src/main/java24/org/apache/artemis/nativo/jlibaio/ffm/IOCBInit.java
new file mode 100644
index 00000000000..2f368e22986
--- /dev/null
+++ b/artemis-ffm/src/main/java24/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/java24/org/apache/artemis/nativo/jlibaio/ffm/IOControl.java b/artemis-ffm/src/main/java24/org/apache/artemis/nativo/jlibaio/ffm/IOControl.java
new file mode 100644
index 00000000000..d2089a07604
--- /dev/null
+++ b/artemis-ffm/src/main/java24/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/java24/org/apache/artemis/nativo/jlibaio/ffm/IOEvent.java b/artemis-ffm/src/main/java24/org/apache/artemis/nativo/jlibaio/ffm/IOEvent.java
new file mode 100644
index 00000000000..92b890f20c2
--- /dev/null
+++ b/artemis-ffm/src/main/java24/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/java24/org/apache/artemis/nativo/jlibaio/ffm/ReleaseCallback.java b/artemis-ffm/src/main/java24/org/apache/artemis/nativo/jlibaio/ffm/ReleaseCallback.java
new file mode 100644
index 00000000000..398fb4665a5
--- /dev/null
+++ b/artemis-ffm/src/main/java24/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/java24/org/apache/artemis/nativo/jlibaio/ffm/SharedContext.java b/artemis-ffm/src/main/java24/org/apache/artemis/nativo/jlibaio/ffm/SharedContext.java
new file mode 100644
index 00000000000..28f007d42f1
--- /dev/null
+++ b/artemis-ffm/src/main/java24/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/java24/org/apache/artemis/nativo/jlibaio/ffm/Stat.java b/artemis-ffm/src/main/java24/org/apache/artemis/nativo/jlibaio/ffm/Stat.java
new file mode 100644
index 00000000000..4084615f4f3
--- /dev/null
+++ b/artemis-ffm/src/main/java24/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/test/java24/org/apache/artemis/nativo/jlibaio/test/CallbackCachelTest.java b/artemis-ffm/src/test/java24/org/apache/artemis/nativo/jlibaio/test/CallbackCachelTest.java
new file mode 100644
index 00000000000..c13173c2de0
--- /dev/null
+++ b/artemis-ffm/src/test/java24/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/java24/org/apache/artemis/nativo/jlibaio/test/LibaioStressTest.java b/artemis-ffm/src/test/java24/org/apache/artemis/nativo/jlibaio/test/LibaioStressTest.java
new file mode 100644
index 00000000000..4243067fb57
--- /dev/null
+++ b/artemis-ffm/src/test/java24/org/apache/artemis/nativo/jlibaio/test/LibaioStressTest.java
@@ -0,0 +1,266 @@
+/*
+ * 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.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);
+ }
+
+ /**
+ * 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/java24/org/apache/artemis/nativo/jlibaio/test/LibaioTest.java b/artemis-ffm/src/test/java24/org/apache/artemis/nativo/jlibaio/test/LibaioTest.java
new file mode 100644
index 00000000000..2c605d7c1f1
--- /dev/null
+++ b/artemis-ffm/src/test/java24/org/apache/artemis/nativo/jlibaio/test/LibaioTest.java
@@ -0,0 +1,789 @@
+/*
+ * 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() {
+
+ 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/java24/org/apache/artemis/nativo/jlibaio/test/LoadedTest.java b/artemis-ffm/src/test/java24/org/apache/artemis/nativo/jlibaio/test/LoadedTest.java
new file mode 100644
index 00000000000..ffb86d3646b
--- /dev/null
+++ b/artemis-ffm/src/test/java24/org/apache/artemis/nativo/jlibaio/test/LoadedTest.java
@@ -0,0 +1,33 @@
+/*
+ * 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.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);
+ }
+
+}
diff --git a/artemis-ffm/src/test/java24/org/apache/artemis/nativo/jlibaio/test/OpenCloseContextTest.java b/artemis-ffm/src/test/java24/org/apache/artemis/nativo/jlibaio/test/OpenCloseContextTest.java
new file mode 100644
index 00000000000..a29965a6693
--- /dev/null
+++ b/artemis-ffm/src/test/java24/org/apache/artemis/nativo/jlibaio/test/OpenCloseContextTest.java
@@ -0,0 +1,181 @@
+/*
+ * 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);
+
+ @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/java24/org/apache/artemis/nativo/jlibaio/test/ReusableLatch.java b/artemis-ffm/src/test/java24/org/apache/artemis/nativo/jlibaio/test/ReusableLatch.java
new file mode 100644
index 00000000000..9d88e6189b1
--- /dev/null
+++ b/artemis-ffm/src/test/java24/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/java24/org/apache/artemis/nativo/jlibaio/test/ffm/FFMNativeHelperTest.java b/artemis-ffm/src/test/java24/org/apache/artemis/nativo/jlibaio/test/ffm/FFMNativeHelperTest.java
new file mode 100644
index 00000000000..9c41f12aca4
--- /dev/null
+++ b/artemis-ffm/src/test/java24/org/apache/artemis/nativo/jlibaio/test/ffm/FFMNativeHelperTest.java
@@ -0,0 +1,389 @@
+/*
+ * 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 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/java24/org/apache/artemis/nativo/jlibaio/test/ffm/IOCBLayoutTest.java b/artemis-ffm/src/test/java24/org/apache/artemis/nativo/jlibaio/test/ffm/IOCBLayoutTest.java
new file mode 100644
index 00000000000..71731650357
--- /dev/null
+++ b/artemis-ffm/src/test/java24/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/java24/org/apache/artemis/nativo/jlibaio/test/ffm/IOControlTest.java b/artemis-ffm/src/test/java24/org/apache/artemis/nativo/jlibaio/test/ffm/IOControlTest.java
new file mode 100644
index 00000000000..2bc78fa757a
--- /dev/null
+++ b/artemis-ffm/src/test/java24/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-journal/pom.xml b/artemis-journal/pom.xml
index 5edc4c9ea8a..df24e62ee9f 100644
--- a/artemis-journal/pom.xml
+++ b/artemis-journal/pom.xml
@@ -91,5 +91,53 @@
${project.version}test
+
+ org.apache.artemis
+ artemis-ffm
+
+
+
+ jdk24onwards
+
+ [24,)
+
+
+
+
+ maven-compiler-plugin
+
+
+ java24-compile
+ compile
+
+ compile
+
+
+ 24
+ ${project.basedir}/src/main/java24
+ true
+
+
+
+
+
+ maven-jar-plugin
+
+
+
+ default-jar
+ process-test-classes
+
+ jar
+
+
+
+
+
+
+
+
diff --git a/artemis-journal/src/main/java/org/apache/activemq/artemis/core/io/SequentialFileFactory.java b/artemis-journal/src/main/java/org/apache/activemq/artemis/core/io/SequentialFileFactory.java
index c999d936f48..f7c0d00204b 100644
--- a/artemis-journal/src/main/java/org/apache/activemq/artemis/core/io/SequentialFileFactory.java
+++ b/artemis-journal/src/main/java/org/apache/activemq/artemis/core/io/SequentialFileFactory.java
@@ -34,6 +34,18 @@ default IOCriticalErrorListener getCriticalErrorListener() {
default void setCriticalErrorListener(IOCriticalErrorListener listener) {
}
+ default SequentialFileFactory disableBufferReuse() {
+ return this;
+ }
+
+ default ByteBuffer newNativeBuffer(int size, int alignment) {
+ throw new UnsupportedOperationException();
+ }
+
+ default void freeNativeBuffer(ByteBuffer buffer) {
+ throw new UnsupportedOperationException();
+ }
+
default CriticalAnalyzer getCriticalAnalyzer() {
return null;
}
diff --git a/artemis-journal/src/main/java/org/apache/activemq/artemis/core/io/aio/AIOSequentialFileFactory.java b/artemis-journal/src/main/java/org/apache/activemq/artemis/core/io/aio/AIOSequentialFileFactory.java
index 40dabfc2350..09b699e3536 100644
--- a/artemis-journal/src/main/java/org/apache/activemq/artemis/core/io/aio/AIOSequentialFileFactory.java
+++ b/artemis-journal/src/main/java/org/apache/activemq/artemis/core/io/aio/AIOSequentialFileFactory.java
@@ -132,8 +132,10 @@ public void enableBufferReuse() {
this.reuseBuffers = true;
}
- public void disableBufferReuse() {
+ @Override
+ public AIOSequentialFileFactory disableBufferReuse() {
this.reuseBuffers = false;
+ return this;
}
@Override
diff --git a/artemis-journal/src/main/java/org/apache/activemq/artemis/core/io/aio2/AIO2Helper.java b/artemis-journal/src/main/java/org/apache/activemq/artemis/core/io/aio2/AIO2Helper.java
new file mode 100644
index 00000000000..b6b5a145d87
--- /dev/null
+++ b/artemis-journal/src/main/java/org/apache/activemq/artemis/core/io/aio2/AIO2Helper.java
@@ -0,0 +1,75 @@
+/*
+ * 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.activemq.artemis.core.io.aio2;
+
+import java.io.File;
+import java.lang.invoke.MethodHandles;
+
+import org.apache.activemq.artemis.core.io.IOCriticalErrorListener;
+import org.apache.activemq.artemis.core.io.SequentialFileFactory;
+import org.apache.activemq.artemis.utils.critical.CriticalAnalyzer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * AIO2 helper for JDK less than version 24.
+ * This version uses stub implementations that throw UnsupportedOperationException.
+ * For JDK 24+, see the real implementation in src/main/java24.
+ */
+public class AIO2Helper {
+
+ private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ public static boolean isSupported() {
+ logger.debug("AIO2Helper from earlier JDKs being used");
+ return false;
+ }
+
+ public static long getTotalMaxIO() {
+ return 0;
+ }
+
+ public static SequentialFileFactory getAIO2SequentialFileFactory(File journalDir, int maxIO) {
+ return null;
+ }
+
+ public static SequentialFileFactory getAIO2SequentialFileFactory(File journalDir,
+ IOCriticalErrorListener listener,
+ int maxIO) {
+ return null;
+ }
+
+ public static SequentialFileFactory getAIO2SequentialFileFactory(File journalDir,
+ int bufferSize,
+ int bufferTimeout,
+ int maxIO,
+ boolean logRates) {
+ return null;
+ }
+
+ public static SequentialFileFactory getAIO2SequentialFileFactory(File journalDir,
+ int bufferSize,
+ int bufferTimeout,
+ int maxIO,
+ boolean logRates,
+ IOCriticalErrorListener listener,
+ CriticalAnalyzer analyzer) {
+ return null;
+ }
+
+}
\ No newline at end of file
diff --git a/artemis-journal/src/main/java/org/apache/activemq/artemis/core/io/mapped/MappedSequentialFileFactory.java b/artemis-journal/src/main/java/org/apache/activemq/artemis/core/io/mapped/MappedSequentialFileFactory.java
index 8f144598c04..e9381d3d4aa 100644
--- a/artemis-journal/src/main/java/org/apache/activemq/artemis/core/io/mapped/MappedSequentialFileFactory.java
+++ b/artemis-journal/src/main/java/org/apache/activemq/artemis/core/io/mapped/MappedSequentialFileFactory.java
@@ -98,6 +98,7 @@ public MappedSequentialFileFactory enableBufferReuse() {
return this;
}
+ @Override
public MappedSequentialFileFactory disableBufferReuse() {
this.bufferPooling = false;
return this;
diff --git a/artemis-journal/src/main/java/org/apache/activemq/artemis/core/io/nio/NIOSequentialFileFactory.java b/artemis-journal/src/main/java/org/apache/activemq/artemis/core/io/nio/NIOSequentialFileFactory.java
index ead344b8f03..c176f680d5b 100644
--- a/artemis-journal/src/main/java/org/apache/activemq/artemis/core/io/nio/NIOSequentialFileFactory.java
+++ b/artemis-journal/src/main/java/org/apache/activemq/artemis/core/io/nio/NIOSequentialFileFactory.java
@@ -110,8 +110,10 @@ public void enableBufferReuse() {
this.bufferPooling = true;
}
- public void disableBufferReuse() {
+ @Override
+ public NIOSequentialFileFactory disableBufferReuse() {
this.bufferPooling = false;
+ return this;
}
@Override
diff --git a/artemis-journal/src/main/java24/org/apache/activemq/artemis/core/io/aio2/AIO2Helper.java b/artemis-journal/src/main/java24/org/apache/activemq/artemis/core/io/aio2/AIO2Helper.java
new file mode 100644
index 00000000000..9785020735c
--- /dev/null
+++ b/artemis-journal/src/main/java24/org/apache/activemq/artemis/core/io/aio2/AIO2Helper.java
@@ -0,0 +1,95 @@
+/*
+ * 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.activemq.artemis.core.io.aio2;
+
+import java.io.File;
+import java.lang.invoke.MethodHandles;
+
+import org.apache.activemq.artemis.core.io.IOCriticalErrorListener;
+import org.apache.activemq.artemis.core.io.SequentialFileFactory;
+import org.apache.activemq.artemis.utils.critical.CriticalAnalyzer;
+import org.apache.artemis.nativo.jlibaio.LibaioContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * AIO2 helper for JDK 24+.
+ * This version uses the real AIO2SequentialFileFactory implementation with Panama FFM support.
+ * For JDK < 24, see the stub version in src/main/java24.
+ */
+public class AIO2Helper {
+
+ private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ public static boolean isSupported() {
+ return AIO2SequentialFileFactory.isSupported();
+ }
+
+ public static long getTotalMaxIO() {
+ return 0;
+ }
+
+ public static SequentialFileFactory getAIO2SequentialFileFactory(File journalDir, int maxIO) {
+ try {
+ return new AIO2SequentialFileFactory(journalDir, maxIO);
+ } catch (UnsupportedOperationException | LinkageError e) {
+ logger.debug("AIO2 not available: {}", e.getMessage(), e);
+ return null;
+ }
+ }
+
+ public static SequentialFileFactory getAIO2SequentialFileFactory(File journalDir,
+ IOCriticalErrorListener listener,
+ int maxIO) {
+ try {
+ return new AIO2SequentialFileFactory(journalDir, listener, maxIO);
+ } catch (UnsupportedOperationException | LinkageError e) {
+ logger.debug("AIO2 not available: {}", e.getMessage(), e);
+ return null;
+ }
+ }
+
+ public static SequentialFileFactory getAIO2SequentialFileFactory(File journalDir,
+ int bufferSize,
+ int bufferTimeout,
+ int maxIO,
+ boolean logRates) {
+ try {
+ return new AIO2SequentialFileFactory(journalDir, bufferSize, bufferTimeout, maxIO, logRates);
+ } catch (UnsupportedOperationException | LinkageError e) {
+ logger.debug("AIO2 not available: {}", e.getMessage(), e);
+ return null;
+ }
+ }
+
+ public static SequentialFileFactory getAIO2SequentialFileFactory(File journalDir,
+ int bufferSize,
+ int bufferTimeout,
+ int maxIO,
+ boolean logRates,
+ IOCriticalErrorListener listener,
+ CriticalAnalyzer analyzer) {
+ try {
+ return new AIO2SequentialFileFactory(journalDir, bufferSize, bufferTimeout, maxIO, logRates, listener, analyzer);
+ } catch (UnsupportedOperationException | LinkageError e) {
+ logger.debug("AIO2 not available: {}", e.getMessage(), e);
+ return null;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/artemis-journal/src/main/java24/org/apache/activemq/artemis/core/io/aio2/AIO2SequentialFile.java b/artemis-journal/src/main/java24/org/apache/activemq/artemis/core/io/aio2/AIO2SequentialFile.java
new file mode 100644
index 00000000000..034f2e3a746
--- /dev/null
+++ b/artemis-journal/src/main/java24/org/apache/activemq/artemis/core/io/aio2/AIO2SequentialFile.java
@@ -0,0 +1,327 @@
+/*
+ * 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.activemq.artemis.core.io.aio2;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.nio.ByteBuffer;
+import java.util.PriorityQueue;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.apache.activemq.artemis.api.core.ActiveMQException;
+import org.apache.activemq.artemis.api.core.ActiveMQNativeIOError;
+import org.apache.activemq.artemis.core.io.AbstractSequentialFile;
+import org.apache.activemq.artemis.core.io.DummyCallback;
+import org.apache.activemq.artemis.core.io.IOCallback;
+import org.apache.activemq.artemis.core.io.SequentialFile;
+import org.apache.activemq.artemis.core.journal.impl.SimpleWaitIOCallback;
+import org.apache.artemis.nativo.jlibaio.LibaioFile;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class is implementing Runnable to reuse a callback to close it.
+ */
+public class AIO2SequentialFile extends AbstractSequentialFile {
+
+ private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private boolean opened = false;
+
+ private LibaioFile aioFile;
+
+ private final AIO2SequentialFileFactory aioFactory;
+
+ /**
+ * Used to determine the next writing sequence
+ */
+ private final AtomicLong nextWritingSequence = new AtomicLong(0);
+
+ /**
+ * AIO can't guarantee ordering over callbacks.
+ *
+ * We use this {@link PriorityQueue} to hold values until they are in order
+ */
+ final PriorityQueue pendingCallbackList = new PriorityQueue<>();
+
+ /**
+ * Used to determine the next writing sequence. This is accessed from a single thread (the Poller Thread)
+ */
+ private long nextReadSequence = 0;
+
+ public AIO2SequentialFile(final AIO2SequentialFileFactory factory,
+ final int bufferSize,
+ final long bufferTimeoutMilliseconds,
+ final File directory,
+ final String fileName) {
+ super(directory, fileName, factory);
+ this.aioFactory = factory;
+ }
+
+ @Override
+ public ByteBuffer map(int position, long size) throws IOException {
+ return null;
+ }
+
+ @Override
+ public boolean isOpen() {
+ return opened;
+ }
+
+ @Override
+ public int calculateBlockStart(final int position) {
+ return factory.calculateBlockSize(position);
+ }
+
+ @Override
+ public SequentialFile cloneFile() {
+ return new AIO2SequentialFile(aioFactory, -1, -1, getFile().getParentFile(), getFile().getName());
+ }
+
+ @Override
+ public void close() throws IOException, InterruptedException, ActiveMQException {
+ close(true, true);
+ }
+
+ @Override
+ public synchronized void close(boolean waitSync,
+ boolean blockOnWait) throws IOException, InterruptedException, ActiveMQException {
+ // a double call on close, should result on it waitingNotPending before another close is called
+ if (!opened) {
+ return;
+ }
+
+ aioFactory.beforeClose();
+
+ super.close();
+ opened = false;
+ this.timedBuffer = null;
+
+ try {
+ aioFile.close();
+ } catch (Throwable e) {
+ // an exception here would means a double
+ logger.debug("Exeption while closing file", e);
+ } finally {
+ aioFile = null;
+ aioFactory.afterClose();
+ }
+ }
+
+ @Override
+ public synchronized void fill(final int size) throws Exception {
+ logger.trace("Filling file: {}", getFileName());
+
+ checkOpened();
+ aioFile.fill(aioFactory.getAlignment(), size);
+
+ fileSize = aioFile.getSize();
+ }
+
+ @Override
+ public void open() throws Exception {
+ open(aioFactory.getMaxIO(), true);
+ }
+
+ @Override
+ public synchronized void open(final int maxIO, final boolean useExecutor) throws ActiveMQException, IOException {
+ // in case we are opening a file that was just closed, we need to wait previous executions to be done
+ if (opened) {
+ return;
+ }
+ opened = true;
+
+ logger.trace("Opening file: {}", getFileName());
+
+ try {
+ aioFile = aioFactory.libaioContext.openFile(getFile(), factory.isDatasync());
+ } catch (IOException e) {
+ logger.error("Error opening file: {}", getFileName());
+ factory.onIOError(e, e.getMessage(), this);
+ throw new ActiveMQNativeIOError(e.getMessage(), e);
+ }
+
+ position.set(0);
+
+ fileSize = aioFile.getSize();
+ }
+
+ @Override
+ public int read(final ByteBuffer bytes, final IOCallback callback) throws ActiveMQException {
+ checkOpened();
+ int bytesToRead = bytes.limit();
+ long positionToRead = position.getAndAdd(bytesToRead);
+
+ bytes.rewind();
+
+ try {
+ // We don't send the buffer to the callback on read,
+ // because we want the buffer available.
+ // Sending it through the callback would make it released
+ aioFile.read(positionToRead, bytesToRead, bytes, getCallback(callback, null));
+ } catch (IOException e) {
+ logger.error("IOError reading file: {}", getFileName(), e);
+ factory.onIOError(e, e.getMessage(), this);
+ throw new ActiveMQNativeIOError(e.getMessage(), e);
+ }
+
+ return bytesToRead;
+ }
+
+ @Override
+ public int read(final ByteBuffer bytes) throws Exception {
+ SimpleWaitIOCallback waitCompletion = new SimpleWaitIOCallback();
+
+ int bytesRead = read(bytes, waitCompletion);
+
+ waitCompletion.waitCompletion();
+
+ return bytesRead;
+ }
+
+ @Override
+ public void writeDirect(final ByteBuffer bytes, final boolean sync) throws Exception {
+ if (logger.isTraceEnabled()) {
+ logger.trace("Write Direct, Sync: {} File: {}", sync, getFileName());
+ }
+
+ if (sync) {
+ SimpleWaitIOCallback completion = new SimpleWaitIOCallback();
+
+ writeDirect(bytes, true, completion);
+
+ completion.waitCompletion();
+ } else {
+ writeDirect(bytes, false, DummyCallback.getInstance());
+ }
+ }
+
+ @Override
+ public void blockingWriteDirect(ByteBuffer bytes, boolean sync, boolean releaseBuffer) throws Exception {
+ logger.trace("Write Direct, Sync: true File: {}", getFileName());
+
+ final SimpleWaitIOCallback completion = new SimpleWaitIOCallback();
+
+ try {
+ checkOpened();
+ } catch (Exception e) {
+ logger.warn(e.getMessage(), e);
+ completion.onError(-1, e.getClass() + " during blocking write direct: " + e.getMessage());
+ return;
+ }
+
+ final int bytesToWrite = factory.calculateBlockSize(bytes.limit());
+
+ final long positionToWrite = position.getAndAdd(bytesToWrite);
+
+ final AIO2SequentialFileFactory.AIO2SequentialCallback runnableCallback = getCallback(completion, bytes, releaseBuffer);
+ runnableCallback.initWrite(positionToWrite, bytesToWrite);
+ runnableCallback.run();
+
+ completion.waitCompletion();
+ }
+
+ /**
+ * Note: Parameter sync is not used on AIO
+ */
+ @Override
+ public void writeDirect(final ByteBuffer bytes, final boolean sync, final IOCallback callback) {
+ try {
+ checkOpened();
+ } catch (Exception e) {
+ logger.warn(e.getMessage(), e);
+ callback.onError(-1, e.getClass() + " during write direct: " + e.getMessage());
+ return;
+ }
+
+ final int bytesToWrite = factory.calculateBlockSize(bytes.limit());
+
+ final long positionToWrite = position.getAndAdd(bytesToWrite);
+
+ AIO2SequentialFileFactory.AIO2SequentialCallback runnableCallback = getCallback(callback, bytes);
+ runnableCallback.initWrite(positionToWrite, bytesToWrite);
+ runnableCallback.run();
+ }
+
+ AIO2SequentialFileFactory.AIO2SequentialCallback getCallback(IOCallback originalCallback, ByteBuffer buffer) {
+ return getCallback(originalCallback, buffer, true);
+ }
+
+ AIO2SequentialFileFactory.AIO2SequentialCallback getCallback(IOCallback originalCallback,
+ ByteBuffer buffer,
+ boolean releaseBuffer) {
+ AIO2SequentialFileFactory.AIO2SequentialCallback callback = aioFactory.getCallback();
+ callback.init(this.nextWritingSequence.getAndIncrement(), originalCallback, aioFile, this, buffer, releaseBuffer);
+ return callback;
+ }
+
+ void done(AIO2SequentialFileFactory.AIO2SequentialCallback callback) {
+ if (callback.writeSequence == -1) {
+ callback.sequentialDone();
+ }
+
+ if (callback.writeSequence == nextReadSequence) {
+ nextReadSequence++;
+ try {
+ callback.sequentialDone();
+ } finally {
+ flushCallbacks();
+ }
+ } else {
+ pendingCallbackList.add(callback);
+ }
+
+ }
+
+ private void flushCallbacks() {
+ while (!pendingCallbackList.isEmpty() && pendingCallbackList.peek().writeSequence == nextReadSequence) {
+ AIO2SequentialFileFactory.AIO2SequentialCallback callback = pendingCallbackList.poll();
+ try {
+ callback.sequentialDone();
+ } finally {
+ nextReadSequence++;
+ }
+ }
+ }
+
+ @Override
+ public void sync() {
+ throw new UnsupportedOperationException("This method is not supported on AIO");
+ }
+
+ @Override
+ public long size() throws Exception {
+ if (aioFile == null) {
+ return getFile().length();
+ } else {
+ return aioFile.getSize();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "AIOSequentialFile{" + getFileName() + ", opened=" + opened + '}';
+ }
+
+ private void checkOpened() {
+ if (aioFile == null || !opened) {
+ throw new NullPointerException("File not opened, file=null on fileName = " + getFileName());
+ }
+ }
+
+}
diff --git a/artemis-journal/src/main/java24/org/apache/activemq/artemis/core/io/aio2/AIO2SequentialFileFactory.java b/artemis-journal/src/main/java24/org/apache/activemq/artemis/core/io/aio2/AIO2SequentialFileFactory.java
new file mode 100644
index 00000000000..f2b5c81cdbd
--- /dev/null
+++ b/artemis-journal/src/main/java24/org/apache/activemq/artemis/core/io/aio2/AIO2SequentialFileFactory.java
@@ -0,0 +1,586 @@
+/*
+ * 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.activemq.artemis.core.io.aio2;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.foreign.MemorySegment;
+import java.lang.invoke.MethodHandles;
+import java.nio.ByteBuffer;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import io.netty.util.internal.PlatformDependent;
+import org.apache.activemq.artemis.ArtemisConstants;
+import org.apache.activemq.artemis.api.core.ActiveMQException;
+import org.apache.activemq.artemis.api.core.ActiveMQExceptionType;
+import org.apache.activemq.artemis.api.core.ActiveMQInterruptedException;
+import org.apache.activemq.artemis.core.io.AbstractSequentialFileFactory;
+import org.apache.activemq.artemis.core.io.IOCallback;
+import org.apache.activemq.artemis.core.io.IOCriticalErrorListener;
+import org.apache.activemq.artemis.core.io.SequentialFile;
+import org.apache.activemq.artemis.journal.ActiveMQJournalLogger;
+import org.apache.artemis.nativo.jlibaio.LibaioContext;
+import org.apache.artemis.nativo.jlibaio.LibaioFile;
+import org.apache.artemis.nativo.jlibaio.SubmitInfo;
+import org.apache.activemq.artemis.utils.ByteUtil;
+import org.apache.activemq.artemis.utils.PowerOf2Util;
+import org.apache.activemq.artemis.utils.critical.CriticalAnalyzer;
+import org.jctools.queues.MpmcArrayQueue;
+import org.jctools.queues.atomic.MpmcAtomicArrayQueue;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public final class AIO2SequentialFileFactory extends AbstractSequentialFileFactory {
+
+ private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ // This is useful in cases where you want to disable loading the native library. (e.g. testsuite)
+ private static final boolean DISABLED = System.getProperty(AIO2SequentialFileFactory.class.getName() + ".DISABLED") != null;
+
+ static {
+ // This is usually only used on testsuite.
+ // In case it's used, I would rather have it on the loggers so we know what's happening
+ if (DISABLED) {
+
+ // This is only used in tests, hence I'm not creating a Logger for this
+ logger.info("{}.DISABLED = true", AIO2SequentialFileFactory.class.getName());
+ }
+ }
+
+ private final ReuseBuffersController buffersControl = new ReuseBuffersController();
+
+ private volatile boolean reuseBuffers = true;
+
+ private Thread pollerThread;
+
+ volatile LibaioContext libaioContext;
+
+ private final Queue callbackPool;
+
+ private final AtomicBoolean running = new AtomicBoolean(false);
+
+ private static final String AIO_TEST_FILE = ".aio-test";
+
+ @Override
+ public boolean isSyncSupported() {
+ return false;
+ }
+
+ public void beforeClose() {
+ }
+
+ public void afterClose() {
+ }
+
+ public AIO2SequentialFileFactory(final File journalDir, int maxIO) {
+ this(journalDir, ArtemisConstants.DEFAULT_JOURNAL_BUFFER_SIZE_AIO, ArtemisConstants.DEFAULT_JOURNAL_BUFFER_TIMEOUT_AIO, maxIO, false, null, null);
+ }
+
+ public AIO2SequentialFileFactory(final File journalDir, final IOCriticalErrorListener listener, int maxIO) {
+ this(journalDir, ArtemisConstants.DEFAULT_JOURNAL_BUFFER_SIZE_AIO, ArtemisConstants.DEFAULT_JOURNAL_BUFFER_TIMEOUT_AIO, maxIO, false, listener, null);
+ }
+
+ public AIO2SequentialFileFactory(final File journalDir,
+ final int bufferSize,
+ final int bufferTimeout,
+ final int maxIO,
+ final boolean logRates) {
+ this(journalDir, bufferSize, bufferTimeout, maxIO, logRates, null, null);
+ }
+
+ public AIO2SequentialFileFactory(final File journalDir,
+ final int bufferSize,
+ final int bufferTimeout,
+ final int maxIO,
+ final boolean logRates,
+ final IOCriticalErrorListener listener,
+ final CriticalAnalyzer analyzer) {
+ super(journalDir, true, bufferSize, bufferTimeout, maxIO, logRates, listener, analyzer);
+ logger.debug("CONSTRUCTOR: bufferSize={}, DEFAULT={}", this.bufferSize, ArtemisConstants.DEFAULT_JOURNAL_BUFFER_SIZE_AIO);
+ if (maxIO == 1) {
+ logger.warn("Using journal-max-io 1 isn't a proper use of ASYNCIO journal: consider rise this value or use NIO.");
+ }
+ final int adjustedMaxIO = Math.max(2, maxIO);
+ callbackPool = PlatformDependent.hasUnsafe() ? new MpmcArrayQueue<>(adjustedMaxIO) : new MpmcAtomicArrayQueue<>(adjustedMaxIO);
+ logger.trace("New AIO File Created");
+ }
+
+ public AIO2SequentialCallback getCallback() {
+ AIO2SequentialCallback callback = callbackPool.poll();
+ if (callback == null) {
+ callback = new AIO2SequentialCallback();
+ }
+
+ return callback;
+ }
+
+ public void enableBufferReuse() {
+ this.reuseBuffers = true;
+ }
+
+ @Override
+ public AIO2SequentialFileFactory disableBufferReuse() {
+ this.reuseBuffers = false;
+ return this;
+ }
+
+ @Override
+ public SequentialFile createSequentialFile(final String fileName) {
+ return new AIO2SequentialFile(this, bufferSize, bufferTimeout, journalDir, fileName);
+ }
+
+ @Override
+ public boolean isSupportsCallbacks() {
+ return true;
+ }
+
+ public static boolean isSupported() {
+ return !DISABLED && LibaioContext.isSupported();
+ }
+
+ public static boolean isSupported(File journalPath) {
+ if (!isSupported()) {
+ return false;
+ }
+
+ File aioTestFile = new File(journalPath, AIO_TEST_FILE);
+ try {
+ int fd = LibaioContext.open(aioTestFile.getAbsolutePath(), true);
+ LibaioContext.close(fd);
+ aioTestFile.delete();
+ } catch (Exception e) {
+ // try to handle the file using plain Java
+ // return false if and only if we can create/remove the file using
+ // plain Java but not using AIO
+ try {
+ if (!aioTestFile.exists()) {
+ if (!aioTestFile.createNewFile()) {
+ return true;
+ }
+ }
+ if (!aioTestFile.delete()) {
+ return true;
+ }
+ } catch (Exception ie) {
+ // we can not even create the test file using plain java
+ return true;
+ }
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public ByteBuffer allocateDirectBuffer(final int size) {
+
+ final int alignedSize = calculateBlockSize(size);
+
+ // The buffer on AIO has to be a multiple of getAlignment()
+ ByteBuffer buffer = LibaioContext.newAlignedBuffer(alignedSize, getAlignment()).asByteBuffer();
+
+ buffer.limit(size);
+
+ return buffer;
+ }
+
+ @Override
+ public void releaseDirectBuffer(final ByteBuffer buffer) {
+ buffer.clear();
+ LibaioContext.freeBuffer(MemorySegment.ofBuffer(buffer));
+ }
+
+ @Override
+ public ByteBuffer newBuffer(int size) {
+ return newBuffer(size, true);
+ }
+
+ @Override
+ public ByteBuffer newBuffer(int size, boolean zeroed) {
+ final int alignedSize = calculateBlockSize(size);
+ return buffersControl.newBuffer(alignedSize, zeroed);
+ }
+
+ @Override
+ public void clearBuffer(final ByteBuffer directByteBuffer) {
+ directByteBuffer.position(0);
+ if (PlatformDependent.hasUnsafe()) {
+ // that's the same semantic of libaioContext.memsetBuffer: it hasn't any JNI cost
+ ByteUtil.zeros(directByteBuffer, 0, directByteBuffer.limit());
+ } else {
+ // JNI cost
+ libaioContext.memsetBuffer(directByteBuffer);
+ }
+ }
+
+ @Override
+ public int getAlignment() {
+ if (alignment < 0) {
+ alignment = calculateAlignment(journalDir);
+ }
+ return alignment;
+ }
+
+ @Override
+ public ByteBuffer newNativeBuffer(int size, int alignment) {
+ return LibaioContext.newAlignedBuffer(size, alignment).asByteBuffer();
+ }
+
+
+ @Override
+ public void freeNativeBuffer(ByteBuffer buffer) {
+ LibaioContext.freeBuffer(MemorySegment.ofBuffer(buffer));
+ }
+
+ private static int calculateAlignment(File journalDir) {
+ File checkFile = null;
+ int alignment;
+ try {
+ journalDir.mkdirs();
+ checkFile = File.createTempFile("journalCheck", ".tmp", journalDir);
+ checkFile.mkdirs();
+ checkFile.createNewFile();
+ alignment = LibaioContext.getBlockSize(checkFile);
+ } catch (Throwable e) {
+ logger.warn(e.getMessage(), e);
+ alignment = 512;
+ } finally {
+ if (checkFile != null) {
+ checkFile.delete();
+ }
+ }
+ return alignment;
+ }
+
+ // For tests only
+ @Override
+ public ByteBuffer wrapBuffer(final byte[] bytes) {
+ ByteBuffer newbuffer = newBuffer(bytes.length);
+ newbuffer.put(bytes);
+ return newbuffer;
+ }
+
+ @Override
+ public int calculateBlockSize(final int position) {
+ final int alignment = getAlignment();
+ if (!PowerOf2Util.isPowOf2(alignment)) {
+ return align(position, alignment);
+ } else {
+ return PowerOf2Util.align(position, alignment);
+ }
+ }
+
+ /**
+ * It can be used to align {@code size} if alignment is not a power of 2: otherwise better to use
+ * {@link PowerOf2Util#align(int, int)} instead.
+ */
+ private static int align(int size, int alignment) {
+ return (size / alignment + (size % alignment != 0 ? 1 : 0)) * alignment;
+ }
+
+ @Override
+ public synchronized void releaseBuffer(final ByteBuffer buffer) {
+ // resetting buffer offsets to original
+ buffer.clear();
+ LibaioContext.freeBuffer(MemorySegment.ofBuffer(buffer));
+ }
+
+ @Override
+ public void start() {
+ if (running.compareAndSet(false, true)) {
+ super.start();
+
+ this.libaioContext = new LibaioContext(maxIO, true, dataSync);
+
+ this.running.set(true);
+
+ pollerThread = new PollerThread();
+ pollerThread.start();
+ }
+
+ }
+
+ @Override
+ public void stop() {
+ if (this.running.compareAndSet(true, false)) {
+ buffersControl.stop();
+
+ libaioContext.close();
+ libaioContext = null;
+
+ if (pollerThread != null) {
+ try {
+ pollerThread.join(AbstractSequentialFileFactory.EXECUTOR_TIMEOUT * 1000);
+
+ if (pollerThread.isAlive()) {
+ ActiveMQJournalLogger.LOGGER.timeoutOnPollerShutdown(new Exception("trace"));
+ }
+ } catch (InterruptedException e) {
+ throw new ActiveMQInterruptedException(e);
+ }
+ }
+
+ super.stop();
+ }
+ }
+
+ /**
+ * The same callback is used for Runnable executor. This way we can save some memory over the pool.
+ */
+ public class AIO2SequentialCallback implements SubmitInfo, Runnable, Comparable {
+
+ IOCallback callback;
+ boolean error = false;
+ AIO2SequentialFile sequentialFile;
+ ByteBuffer buffer;
+ LibaioFile libaioFile;
+ String errorMessage;
+ int errorCode = -1;
+ long writeSequence;
+ boolean releaseBuffer;
+ long position;
+ int bytes;
+
+ @Override
+ public String toString() {
+ return "AIOSequentialCallback{" + "error=" + error + ", errorMessage='" + errorMessage + '\'' + ", errorCode=" + errorCode + ", writeSequence=" + writeSequence + ", releaseBuffer=" + releaseBuffer + ", position=" + position + '}';
+ }
+
+ public AIO2SequentialCallback initWrite(long positionToWrite, int bytesToWrite) {
+ this.position = positionToWrite;
+ this.bytes = bytesToWrite;
+ return this;
+ }
+
+ @Override
+ public void run() {
+ try {
+ libaioFile.write(position, bytes, buffer, this);
+ } catch (IOException e) {
+ callback.onError(ActiveMQExceptionType.IO_ERROR.getCode(), e.getClass() + " during write to " + sequentialFile.getFileName() + ": " + e.getMessage());
+ onIOError(e, "Failed to write to file", sequentialFile);
+ }
+ }
+
+ @Override
+ public int compareTo(AIO2SequentialCallback other) {
+ if (this == other || this.writeSequence == other.writeSequence) {
+ return 0;
+ } else if (other.writeSequence < this.writeSequence) {
+ return 1;
+ } else {
+ return -1;
+ }
+ }
+
+ public AIO2SequentialCallback init(long writeSequence,
+ IOCallback IOCallback,
+ LibaioFile libaioFile,
+ AIO2SequentialFile sequentialFile,
+ ByteBuffer usedBuffer,
+ boolean releaseBuffer) {
+ this.callback = IOCallback;
+ this.sequentialFile = sequentialFile;
+ this.error = false;
+ this.buffer = usedBuffer;
+ this.libaioFile = libaioFile;
+ this.writeSequence = writeSequence;
+ this.errorMessage = null;
+ this.releaseBuffer = releaseBuffer;
+ return this;
+ }
+
+ @Override
+ public void onError(int errno, String message) {
+ if (logger.isTraceEnabled()) {
+ logger.trace("AIO on error issued. Error(code: {} msg: {})", errno, message);
+ }
+
+ this.error = true;
+ this.errorCode = errno;
+ this.errorMessage = message;
+ }
+
+ /**
+ * this is called by libaio.
+ */
+ @Override
+ public void done() {
+ this.sequentialFile.done(this);
+ }
+
+ /**
+ * This is callbed by the AIOSequentialFile, after determined the callbacks were returned in sequence
+ */
+ public void sequentialDone() {
+
+ if (error) {
+ if (callback != null) {
+ callback.onError(errorCode, errorMessage);
+ }
+ onIOError(new ActiveMQException(errorCode, errorMessage), errorMessage);
+ errorMessage = null;
+ } else {
+ if (callback != null) {
+ callback.done();
+ }
+
+ if (buffer != null && reuseBuffers && releaseBuffer) {
+ buffersControl.bufferDone(buffer);
+ }
+
+ callbackPool.offer(AIO2SequentialCallback.this);
+ }
+ }
+ }
+
+ private class PollerThread extends Thread {
+
+ private PollerThread() {
+ super("activemq-libaio-poller");
+ }
+
+ @Override
+ public void run() {
+ while (running.get()) {
+ // To optimize performance, libaioContext.poll should always be invoked from the same thread.
+ // This approach leverages kernel-level efficiencies in context switching.
+ // Consistent polling from a dedicated thread will yield substantial performance gains.
+ try {
+ libaioContext.poll();
+ } catch (Throwable e) {
+ logger.warn(e.getMessage(), e);
+ onIOError(new ActiveMQException("Error on libaio poll"), e.getMessage());
+ }
+ }
+ }
+ }
+
+ /**
+ * Class that will control buffer-reuse
+ */
+ private class ReuseBuffersController {
+
+ private volatile long bufferReuseLastTime = System.currentTimeMillis();
+
+ private final Queue reuseBuffersQueue = new ConcurrentLinkedQueue<>();
+
+ private boolean stopped = false;
+
+ private int alignedBufferSize = 0;
+
+ private int getAlignedBufferSize() {
+ if (alignedBufferSize == 0) {
+ alignedBufferSize = calculateBlockSize(bufferSize);
+ }
+
+ return alignedBufferSize;
+ }
+
+ public ByteBuffer newBuffer(final int size, final boolean zeroed) {
+ // if a new buffer wasn't requested in 10 seconds, we clear the queue
+ // This is being done this way as we don't need another Timeout Thread
+ // just to cleanup this
+ if (bufferSize > 0 && System.currentTimeMillis() - bufferReuseLastTime > 10000) {
+ if (logger.isTraceEnabled()) {
+ logger.trace("Clearing reuse buffers queue with {} elements", reuseBuffersQueue.size());
+ }
+
+ bufferReuseLastTime = System.currentTimeMillis();
+
+ clearPoll();
+ }
+
+ // if a buffer is bigger than the configured-bufferSize, we just create a new
+ // buffer.
+ if (size > getAlignedBufferSize()) {
+ return LibaioContext.newAlignedBuffer(size, getAlignment()).asByteBuffer();
+ } else {
+ // We need to allocate buffers following the rules of the storage
+ // being used (AIO/NIO)
+ final int alignedSize;
+
+ if (size < getAlignedBufferSize()) {
+ alignedSize = getAlignedBufferSize();
+ } else {
+ alignedSize = calculateBlockSize(size);
+ }
+
+ // Try getting a buffer from the queue...
+ ByteBuffer buffer = reuseBuffersQueue.poll();
+
+ if (buffer == null) {
+ // if empty create a new one.
+ buffer = LibaioContext.newAlignedBuffer(alignedSize, getAlignment()).asByteBuffer();
+
+ buffer.limit(calculateBlockSize(size));
+ } else {
+ if (zeroed) {
+ clearBuffer(buffer);
+ } else {
+ buffer.position(0);
+ }
+
+ // set the limit of the buffer to the bufferSize being required
+ buffer.limit(calculateBlockSize(size));
+ }
+
+ buffer.rewind();
+
+ return buffer;
+ }
+ }
+
+ public synchronized void stop() {
+ stopped = true;
+ clearPoll();
+ }
+
+ public synchronized void clearPoll() {
+ ByteBuffer reusedBuffer;
+
+ while ((reusedBuffer = reuseBuffersQueue.poll()) != null) {
+ releaseBuffer(reusedBuffer);
+ }
+ }
+
+ public void bufferDone(final ByteBuffer buffer) {
+ synchronized (this) {
+
+ if (stopped) {
+ releaseBuffer(buffer);
+ } else {
+ bufferReuseLastTime = System.currentTimeMillis();
+
+ // If a buffer has any other than the configured bufferSize, the buffer
+ // will be just sent to GC
+ if (buffer.capacity() == getAlignedBufferSize()) {
+ reuseBuffersQueue.offer(buffer);
+ } else {
+ releaseBuffer(buffer);
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ return AIO2SequentialFileFactory.class.getSimpleName() + "(buffersControl.stopped=" + buffersControl.stopped + "):" + super.toString();
+ }
+}
diff --git a/artemis-server/pom.xml b/artemis-server/pom.xml
index 9b8d065022b..334bdea6893 100644
--- a/artemis-server/pom.xml
+++ b/artemis-server/pom.xml
@@ -300,6 +300,12 @@
${hamcrest.version}test
+
+ org.apache.artemis
+ artemis-ffm
+ ${project.version}
+ compile
+
diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/deployers/impl/FileConfigurationParser.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/deployers/impl/FileConfigurationParser.java
index bd6c5ccc3ff..d439afcf524 100644
--- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/deployers/impl/FileConfigurationParser.java
+++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/deployers/impl/FileConfigurationParser.java
@@ -91,6 +91,7 @@
import org.apache.activemq.artemis.core.config.storage.DatabaseStorageConfiguration;
import org.apache.activemq.artemis.core.config.storage.FileStorageConfiguration;
import org.apache.activemq.artemis.core.io.aio.AIOSequentialFileFactory;
+import org.apache.activemq.artemis.core.io.aio2.AIO2Helper;
import org.apache.activemq.artemis.core.security.Role;
import org.apache.activemq.artemis.core.server.ActiveMQMessageBundle;
import org.apache.activemq.artemis.core.server.ActiveMQServerLogger;
@@ -804,6 +805,17 @@ public void parseMainConfig(final Element e, final Configuration config) throws
// settings in xml. If we fall back later on these settings can be ignored.
boolean supportsAIO = AIOSequentialFileFactory.isSupported();
+ if (!supportsAIO) {
+ if (validateAIO) {
+ ActiveMQServerLogger.LOGGER.AIONotFound();
+ }
+ config.setJournalType(JournalType.NIO);
+ }
+ } else if (config.getJournalType() == JournalType.ASYNCIO_2) {
+ // We do the check here to see if AIO is supported so we can use the correct defaults and/or use correct
+ // settings in xml. If we fall back later on these settings can be ignored.
+ boolean supportsAIO = AIO2Helper.isSupported();
+
if (!supportsAIO) {
if (validateAIO) {
ActiveMQServerLogger.LOGGER.AIONotFound();
diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/impl/journal/JournalStorageManager.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/impl/journal/JournalStorageManager.java
index 6196897ed3e..8a36ef557c0 100644
--- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/impl/journal/JournalStorageManager.java
+++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/impl/journal/JournalStorageManager.java
@@ -44,6 +44,7 @@
import org.apache.activemq.artemis.core.io.SequentialFile;
import org.apache.activemq.artemis.core.io.SequentialFileFactory;
import org.apache.activemq.artemis.core.io.aio.AIOSequentialFileFactory;
+import org.apache.activemq.artemis.core.io.aio2.AIO2Helper;
import org.apache.activemq.artemis.core.io.mapped.MappedSequentialFileFactory;
import org.apache.activemq.artemis.core.io.nio.NIOSequentialFileFactory;
import org.apache.activemq.artemis.core.journal.EncoderPersister;
@@ -167,6 +168,16 @@ protected void init(Configuration config, IOCriticalErrorListener criticalErrorL
}
journalFF = new AIOSequentialFileFactory(config.getJournalLocation(), config.getJournalBufferSize_AIO(), config.getJournalBufferTimeout_AIO(), config.getJournalMaxIO_AIO(), config.isLogJournalWriteRate(), criticalErrorListener, getCriticalAnalyzer());
+ if (config.getJournalDeviceBlockSize() != null) {
+ journalFF.setAlignment(config.getJournalDeviceBlockSize());
+ }
+ break;
+ case ASYNCIO_2:
+ if (criticalErrorListener != null) {
+ ActiveMQServerLogger.LOGGER.journalUseAIO_2();
+ }
+ journalFF = AIO2Helper.getAIO2SequentialFileFactory(config.getJournalLocation(), config.getJournalBufferSize_AIO(), config.getJournalBufferTimeout_AIO(), config.getJournalMaxIO_AIO(), config.isLogJournalWriteRate(), criticalErrorListener, getCriticalAnalyzer());
+
if (config.getJournalDeviceBlockSize() != null) {
journalFF.setAlignment(config.getJournalDeviceBlockSize());
}
diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQServerLogger.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQServerLogger.java
index f14d8b7f7dc..89d33f277ea 100644
--- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQServerLogger.java
+++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQServerLogger.java
@@ -1550,4 +1550,12 @@ void slowConsumerDetected(String sessionID,
@LogMessage(id = 224164, value = "Failed to recover stored configuration for divert named '{}': {}. To repair this record create a new divert with the same name via the management API.", level = LogMessage.Level.WARN)
void failedToRecoverStoredDivertConfiguration(String divertName, String divert);
+
+ @LogMessage(id = 224165, value = "Panama FFM libaio (AsyncIO_2) is not available, switching the configuration into JNI AIO (AsyncIO)", level = LogMessage.Level.INFO)
+ void switchingAIO();
+
+ @LogMessage(id = 224166, value = "Using Panama FFM AIO Version 2 Journal", level = LogMessage.Level.INFO)
+ void journalUseAIO_2();
+
+
}
\ No newline at end of file
diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/JournalType.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/JournalType.java
index 5f20f816415..1145797105b 100644
--- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/JournalType.java
+++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/JournalType.java
@@ -18,7 +18,7 @@
public enum JournalType {
- NIO, ASYNCIO, MAPPED;
+ NIO, ASYNCIO, MAPPED, ASYNCIO_2;
public static final String validValues;
@@ -40,6 +40,7 @@ public static JournalType getType(String type) {
return switch (type) {
case "NIO" -> NIO;
case "ASYNCIO" -> ASYNCIO;
+ case "ASYNCIO_2" -> ASYNCIO_2;
case "MAPPED" -> MAPPED;
default -> throw new IllegalStateException("Invalid JournalType:" + type + " valid Types: " + validValues);
};
diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/ActiveMQServerImpl.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/ActiveMQServerImpl.java
index e0d8055f958..5a77a03165d 100644
--- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/ActiveMQServerImpl.java
+++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/ActiveMQServerImpl.java
@@ -84,6 +84,7 @@
import org.apache.activemq.artemis.core.io.IOCriticalErrorListener;
import org.apache.activemq.artemis.core.io.SequentialFile;
import org.apache.activemq.artemis.core.io.aio.AIOSequentialFileFactory;
+import org.apache.activemq.artemis.core.io.aio2.AIO2Helper;
import org.apache.activemq.artemis.core.journal.JournalLoadInformation;
import org.apache.activemq.artemis.core.journal.RecordInfo;
import org.apache.activemq.artemis.core.management.impl.ActiveMQServerControlImpl;
@@ -3374,6 +3375,13 @@ synchronized boolean initialisePart1(boolean scalingDown) throws Exception {
ServerStatus.starting(this);
+ if (configuration.getJournalType() == JournalType.ASYNCIO_2) {
+ if (!AIO2Helper.isSupported()) {
+ ActiveMQServerLogger.LOGGER.switchingAIO();
+ configuration.setJournalType(JournalType.ASYNCIO);
+ }
+ }
+
if (configuration.getJournalType() == JournalType.ASYNCIO) {
if (!AIOSequentialFileFactory.isSupported()) {
ActiveMQServerLogger.LOGGER.switchingNIO();
diff --git a/artemis-server/src/main/resources/schema/artemis-configuration.xsd b/artemis-server/src/main/resources/schema/artemis-configuration.xsd
index 2af3e1b1449..0dec7a52726 100644
--- a/artemis-server/src/main/resources/schema/artemis-configuration.xsd
+++ b/artemis-server/src/main/resources/schema/artemis-configuration.xsd
@@ -705,6 +705,7 @@
+
diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/core/persistence/impl/journal/JournalStorageManagerTest.java b/artemis-server/src/test/java/org/apache/activemq/artemis/core/persistence/impl/journal/JournalStorageManagerTest.java
index 8be81812e03..19c6535e578 100644
--- a/artemis-server/src/test/java/org/apache/activemq/artemis/core/persistence/impl/journal/JournalStorageManagerTest.java
+++ b/artemis-server/src/test/java/org/apache/activemq/artemis/core/persistence/impl/journal/JournalStorageManagerTest.java
@@ -37,6 +37,7 @@
import org.apache.activemq.artemis.core.config.Configuration;
import org.apache.activemq.artemis.core.io.SequentialFile;
import org.apache.activemq.artemis.core.io.aio.AIOSequentialFileFactory;
+import org.apache.activemq.artemis.core.io.aio2.AIO2Helper;
import org.apache.activemq.artemis.core.postoffice.PostOffice;
import org.apache.activemq.artemis.core.server.JournalType;
import org.apache.activemq.artemis.core.server.impl.JournalLoader;
@@ -48,6 +49,7 @@
import org.apache.activemq.artemis.utils.actors.OrderedExecutorFactory;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -68,6 +70,17 @@ public static Collection