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-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-distribution/pom.xml b/artemis-distribution/pom.xml index cb75bee530b..5f4adc5a7d6 100644 --- a/artemis-distribution/pom.xml +++ b/artemis-distribution/pom.xml @@ -208,6 +208,19 @@ + + jdk24onwards + + [24,) + + + + org.apache.artemis + artemis-ffm + ${project.version} + + + apache-release diff --git a/artemis-features/src/main/resources/features.xml b/artemis-features/src/main/resources/features.xml index 14784aaa0ad..01cb4d4271d 100644 --- a/artemis-features/src/main/resources/features.xml +++ b/artemis-features/src/main/resources/features.xml @@ -83,6 +83,7 @@ mvn:com.nimbusds/nimbus-jose-jwt/${nimbus.jwt.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-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/LibaioContext.java b/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/LibaioContext.java new file mode 100644 index 00000000000..f38c29db209 --- /dev/null +++ b/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/LibaioContext.java @@ -0,0 +1,177 @@ +/* + * 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.File; +import java.io.IOException; +import java.nio.ByteBuffer; + +public class LibaioContext implements Closeable { + + public static boolean isSupported() { + return false; + } + + public static void setForceSyscall(boolean value) { + throw new UnsupportedOperationException("Not supported for JDK < 22."); + } + + public static boolean isForceSyscall() { + throw new UnsupportedOperationException("Not supported for JDK < 22."); + } + + public static long getTotalMaxIO() { + throw new UnsupportedOperationException("Not supported for JDK < 22."); + } + + public void memsetBuffer(ByteBuffer buffer) { + throw new UnsupportedOperationException("Not supported for JDK < 22."); + } + + public static void resetMaxAIO() { + throw new UnsupportedOperationException("Not supported for JDK < 22."); + } + + public LibaioContext(int queueSize, boolean useSemaphore, boolean useFdatasync) { + throw new UnsupportedOperationException("Not supported for JDK < 22."); + } + + public void submitWrite(int fd, + long position, + int size, + ByteBuffer bufferWrite, + Callback callback) throws IOException { + throw new UnsupportedOperationException("Not supported for JDK < 22."); + } + + public void submitRead(int fd, + long position, + int size, + ByteBuffer bufferWrite, + Callback callback) throws IOException { + throw new UnsupportedOperationException("Not supported for JDK < 22."); + } + + @Override + public void close() { + throw new UnsupportedOperationException("Not supported for JDK < 22."); + } + + public LibaioFile openFile(File file, boolean direct) throws IOException { + throw new UnsupportedOperationException("Not supported for JDK < 22."); + } + + public LibaioFile openFile(String file, boolean direct) throws IOException { + throw new UnsupportedOperationException("Not supported for JDK < 22."); + } + + public static LibaioFile openControlFile(String file, boolean direct) throws IOException { + throw new UnsupportedOperationException("Not supported for JDK < 22."); + } + + public int poll(Callback[] callbacks, int min, int max) { + throw new UnsupportedOperationException("Not supported for JDK < 22."); + } + + public void poll() { + throw new UnsupportedOperationException("Not supported for JDK < 22."); + } + + public static int open(String path, boolean direct) throws IOException { + throw new UnsupportedOperationException("Not supported for JDK < 22."); + } + + public static void close(int fd) throws IOException { + throw new UnsupportedOperationException("Not supported for JDK < 22."); + } + + public static ByteBuffer newAlignedBuffer(int size, int alignment) { + throw new UnsupportedOperationException("Not supported for JDK < 22."); + } + + public static void freeBuffer(ByteBuffer buffer) { + throw new UnsupportedOperationException("Not supported for JDK < 22."); + } + + void submitWrite(int fd, + Object ioControl, + long position, + int size, + ByteBuffer bufferWrite, + Callback callback) throws IOException { + throw new UnsupportedOperationException("Not supported for JDK < 22."); + } + + void submitRead(int fd, + Object ioControl, + long position, + int size, + ByteBuffer bufferWrite, + Callback callback) throws IOException { + throw new UnsupportedOperationException("Not supported for JDK < 22."); + } + + int poll(Object ioControl, Callback[] callbacks, int min, int max) { + throw new UnsupportedOperationException("Not supported for JDK < 22."); + } + + void blockedPoll(Object ioControl, boolean useFdatasync) { + throw new UnsupportedOperationException("Not supported for JDK < 22."); + } + + static int getNativeVersion() { + throw new UnsupportedOperationException("Not supported for JDK < 22."); + } + + public static boolean lock(int fd) { + throw new UnsupportedOperationException("Not supported for JDK < 22."); + } + + public static void memsetBuffer(ByteBuffer buffer, int size) { + throw new UnsupportedOperationException("Not supported for JDK < 22."); + } + + static long getSize(int fd) throws IOException { + throw new UnsupportedOperationException("Not supported for JDK < 22."); + } + + static int getBlockSizeFD(int fd) throws IOException { + throw new UnsupportedOperationException("Not supported for JDK < 22."); + } + + public static int getBlockSize(File path) throws IOException { + throw new UnsupportedOperationException("Not supported for JDK < 22."); + } + + public static int getBlockSize(String path) throws IOException { + throw new UnsupportedOperationException("Not supported for JDK < 22."); + } + + static void fallocate(int fd, long size) throws IOException { + throw new UnsupportedOperationException("Not supported for JDK < 22."); + } + + static void fill(int fd, int alignment, long size) throws IOException { + throw new UnsupportedOperationException("Not supported for JDK < 22."); + } + + static void writeInternal(int fd, long position, long size, ByteBuffer bufferWrite) throws IOException { + throw new UnsupportedOperationException("Not supported for JDK < 22."); + } + +} 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..384c55389fa --- /dev/null +++ b/artemis-ffm/src/main/java/org/apache/artemis/nativo/jlibaio/LibaioFile.java @@ -0,0 +1,64 @@ +/* + * 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; +import java.nio.ByteBuffer; + +public class LibaioFile implements Closeable { + + LibaioFile(int fd, LibaioContext ctx) { + } + + public int getBlockSize() throws IOException { + throw new UnsupportedOperationException("Not supported for JDK < 22."); + } + + public boolean lock() { + throw new UnsupportedOperationException("Not supported for JDK < 22."); + } + + @Override + public void close() throws IOException { + throw new UnsupportedOperationException("Not supported for JDK < 22."); + } + + public long getSize() throws IOException { + throw new UnsupportedOperationException("Not supported for JDK < 22."); + } + + public void write(long position, int size, ByteBuffer buffer, Callback callback) throws IOException { + throw new UnsupportedOperationException("Not supported for JDK < 22."); + } + + public void read(long position, int size, ByteBuffer buffer, Callback callback) throws IOException { + throw new UnsupportedOperationException("Not supported for JDK < 22."); + } + + public ByteBuffer newBuffer(int size) { + throw new UnsupportedOperationException("Not supported for JDK < 22."); + } + + public void fill(int alignment, long size) throws IOException { + throw new UnsupportedOperationException("Not supported for JDK < 22."); + } + + public void fallocate(long size) throws IOException { + throw new UnsupportedOperationException("Not supported for JDK < 22."); + } +} 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..b75b2ca1d3e --- /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/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..ed2595f04d2 --- /dev/null +++ b/artemis-ffm/src/main/java24/org/apache/artemis/nativo/jlibaio/LibaioContext.java @@ -0,0 +1,512 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.artemis.nativo.jlibaio; + +import org.apache.artemis.nativo.jlibaio.ffm.FFMNativeHelper; +import org.apache.artemis.nativo.jlibaio.ffm.IOControl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.lang.foreign.MemorySegment; +import java.nio.ByteBuffer; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +/** + * This class is used as an aggregator for the {@link LibaioFile}. + *
+ * It holds native data, and it will share a libaio queue that can be used by multiple files. + *
+ * You need to use the poll methods to read the result of write and read submissions. + *
+ * You also need to use the special buffer created by {@link LibaioFile} as you need special alignments + * when dealing with O_DIRECT files. + *
+ * A Single controller can server multiple files. There's no need to create one controller per file. + *
+ * Interesting reading for this. + */ +public class LibaioContext implements Closeable { + /* Notice: After making changes to the native interface, mvn 'generate-sources' must occur at least once to generate the updated include file. + This is because the maven compiler plugin is the one generating org_apache_activemq_artemis_native_jlibaio_LibaioContext.h + So that file needs to be updated before Cmake comes along to compile the module. + This normally happens as needed in the regular mvn build, so if you use e.g 'mvn clean install -Ppodman' then no extra step is needed, + specific attention is only required if you e.g run the build scripts directly yourself. */ + + private static final Logger logger = LoggerFactory.getLogger(LibaioContext.class); + + private static final AtomicLong totalMaxIO = new AtomicLong(0); + + /** + * The Native layer will look at this version. + */ + private static final int EXPECTED_NATIVE_VERSION = 200; + + private static boolean loaded = true; + + private static final AtomicBoolean shuttingDown = new AtomicBoolean(false); + + private static final AtomicInteger contexts = new AtomicInteger(0); + + public static boolean isLoaded() { + return loaded; + } + + public static boolean isSupported() { + return true; + } + + private static boolean loadLibrary(final String name) { + try { + logger.debug("Loading {}", name); + System.loadLibrary(name); + if (getNativeVersion() != EXPECTED_NATIVE_VERSION) { + NativeLogger.incompatibleNativeLibrary(); + return false; + } else { + return true; + } + } catch (Throwable e) { + logger.debug(name + " -> not possible to load native library", e); + return false; + } + + } + + static { + if (System.getProperty("org.apache.activemq.artemis.native.jlibaio.FORCE_SYSCALL") != null) { + LibaioContext.setForceSyscall(true); + } + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + shuttingDown.set(true); + checkShutdown(); + } + }); + } + + private static void checkShutdown() { + if (contexts.get() == 0 && shuttingDown.get()) { + shutdownHook(); + } + } + + private static void shutdownHook() { + FFMNativeHelper.shutdownHook(); + } + + public static void setForceSyscall(boolean value) { + FFMNativeHelper.setForceSyscall(value); + } + + /** + * The system may choose to set this if a failing condition happened inside the code. + */ + public static boolean isForceSyscall() { + return FFMNativeHelper.isForceSyscall(); + } + + /** + * This is used to validate leaks on tests. + * + * @return the number of allocated aio, to be used on test checks. + */ + public static long getTotalMaxIO() { + return totalMaxIO.get(); + } + + /** + * It will reset all the positions on the buffer to 0, using memset. + * + * @param buffer a native buffer. + * s + */ + public void memsetBuffer(ByteBuffer buffer) { + memsetBuffer(buffer, buffer.limit()); + } + + /** + * This is used on tests validating for leaks. + */ + public static void resetMaxAIO() { + totalMaxIO.set(0); + } + + /** + * the native ioContext including the structure created. + */ + private final IOControl ioContext; + + private final AtomicBoolean closed = new AtomicBoolean(false); + + final Semaphore ioSpace; + + final int queueSize; + + final boolean useFdatasync; + + final FFMNativeHelper ffmNativeHelper; + + /** + * The queue size here will use resources defined on the kernel parameter + * fs.aio-max-nr . + * + * @param queueSize the size to be initialize on libaio + * io_queue_init which can't be higher than /proc/sys/fs/aio-max-nr. + * @param useSemaphore should block on a semaphore avoiding using more submits than what's available. + * @param useFdatasync should use fdatasync before calling callbacks. + */ + public LibaioContext(int queueSize, boolean useSemaphore, boolean useFdatasync) { + try { + this.ffmNativeHelper = new FFMNativeHelper<>(this::releaseSemaphore); + contexts.incrementAndGet(); + this.ioContext = newContext(queueSize); + this.useFdatasync = useFdatasync; + } catch (Exception e) { + throw e; + } + this.queueSize = queueSize; + totalMaxIO.addAndGet(queueSize); + if (useSemaphore) { + this.ioSpace = new Semaphore(queueSize); + } else { + this.ioSpace = null; + } + } + + /** + * Documented at {@link LibaioFile#write(long, int, ByteBuffer, SubmitInfo)} + * + * @param fd the file descriptor + * @param position the write position + * @param size number of bytes to use + * @param bufferWrite the native buffer + * @param callback a callback + * @throws IOException in case of error + */ + public void submitWrite(int fd, + long position, + int size, + ByteBuffer bufferWrite, + Callback callback) throws IOException { + if (closed.get()) { + throw new IOException("Libaio Context is closed!"); + } + try { + if (ioSpace != null) { + ioSpace.acquire(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException(e.getMessage(), e); + } + submitWrite(fd, this.ioContext, position, size, bufferWrite, callback); + } + + public void submitRead(int fd, + long position, + int size, + ByteBuffer bufferWrite, + Callback callback) throws IOException { + if (closed.get()) { + throw new IOException("Libaio Context is closed!"); + } + try { + if (ioSpace != null) { + ioSpace.acquire(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException(e.getMessage(), e); + } + submitRead(fd, this.ioContext, position, size, bufferWrite, callback); + } + + /** + * This is used to close the libaio queues and cleanup the native data used. + *
+ * It is unsafe to close the controller while you have pending writes or files open as + * this could cause core dumps or VM crashes. + */ + @Override + public void close() { + if (!closed.getAndSet(true)) { + + if (ioSpace != null) { + try { + ioSpace.tryAcquire(queueSize, 10, TimeUnit.SECONDS); + } catch (Exception e) { + logger.warn(e.getMessage(), e); + } + } + totalMaxIO.addAndGet(-queueSize); + + if (ioContext != null) { + deleteContext(ioContext); + } + contexts.decrementAndGet(); + checkShutdown(); + } + } + + /** + * It will open a file. If you set the direct flag = false then you won't need to use the special buffer. + * Notice: This will create an empty file if the file doesn't already exist. + * + * @param file the file to be open. + * @param direct will set ODIRECT. + * @return It will return a LibaioFile instance. + * @throws IOException in case of error. + */ + public LibaioFile openFile(File file, boolean direct) throws IOException { + return openFile(file.getPath(), direct); + } + + /** + * It will open a file. If you set the direct flag = false then you won't need to use the special buffer. + * Notice: This will create an empty file if the file doesn't already exist. + * + * @param file the file to be open. + * @param direct should use O_DIRECT when opening the file. + * @return a new open file. + * @throws IOException in case of error. + */ + public LibaioFile openFile(String file, boolean direct) throws IOException { + checkNotNull(file, "path"); + checkNotNull(ioContext, "IOContext"); + + // note: the native layer will throw an IOException in case of errors + int res = LibaioContext.open(file, direct); + + return new LibaioFile<>(res, this); + } + + /** + * It will open a file disassociated with any sort of factory. + * This is useful when you won't use reading / writing through libaio like locking files. + * + * @param file a file name + * @param direct will use O_DIRECT + * @return a new file + * @throws IOException in case of error. + */ + public static LibaioFile openControlFile(String file, boolean direct) throws IOException { + checkNotNull(file, "path"); + + // note: the native layer will throw an IOException in case of errors + int res = LibaioContext.open(file, direct); + + return new LibaioFile<>(res, null); + } + + /** + * Checks that the given argument is not null. If it is, throws {@link NullPointerException}. + * Otherwise, returns the argument. + */ + private static T checkNotNull(T arg, String text) { + if (arg == null) { + throw new NullPointerException(text); + } + return arg; + } + + /** + * It will poll the libaio queue for results. It should block until min is reached + * Results are placed on the callback. + *
+ * This shouldn't be called concurrently. You should provide your own synchronization if you need more than one + * Thread polling for any reason. + *
+ * Notice that the native layer will invoke {@link SubmitInfo#onError(int, String)} in case of failures, + * but it won't call done method for you. + * + * @param callbacks area to receive the callbacks passed on submission.The size of this callback has to + * be greater than the parameter max. + * @param min the minimum number of elements to receive. It will block until this is achieved. + * @param max The maximum number of elements to receive. + * @return Number of callbacks returned. + * @see LibaioFile#write(long, int, ByteBuffer, SubmitInfo) + * @see LibaioFile#read(long, int, ByteBuffer, SubmitInfo) + */ + public int poll(Callback[] callbacks, int min, int max) { + int released = poll(ioContext, callbacks, min, max); + if (ioSpace != null) { + if (released > 0) { + ioSpace.release(released); + } + } + return released; + } + + /** + * It will start polling and will keep doing until the context is closed. + * This will call callbacks on {@link SubmitInfo#onError(int, String)} and + * {@link SubmitInfo#done()}. + * In case of error, both {@link SubmitInfo#onError(int, String)} and + * {@link SubmitInfo#done()} are called. + */ + public void poll() { + if (!closed.get()) { + blockedPoll(ioContext, useFdatasync); + } + } + + private void releaseSemaphore() { + if (ioSpace != null) { + ioSpace.release(); + } + } + + /** + * This is the queue for libaio, initialized with queueSize. + */ + private IOControl newContext(int queueSize) { + return this.ffmNativeHelper.newContext(queueSize); + } + + /** + * Internal method to be used when closing the controller. + */ + private void deleteContext(IOControl ioControl) { + this.ffmNativeHelper.deleteContext(ioControl); + } + + /** + * it will return a file descriptor. + * + * @param path the file name. + * @param direct translates as O_DIRECT On open + * @return a fd from open C call. + */ + public static int open(String path, boolean direct) throws IOException { + return FFMNativeHelper.open(path, direct); + } + + public static void close(int fd) throws IOException { + FFMNativeHelper.close(fd); + } + + /** + * Buffers for O_DIRECT need to use posix_memalign. + *
+ * Documented at {@link LibaioFile#newBuffer(int)}. + * + * @param size needs to be % alignment + * @param alignment the alignment used at the dispositive + * @return a new native buffer used with posix_memalign + */ + public static MemorySegment newAlignedBuffer(int size, int alignment) { + return FFMNativeHelper.newAlignedBuffer(size, alignment); + } + + /** + * This will call posix free to release the inner buffer allocated at {@link #newAlignedBuffer(int, int)}. + * + * @param buffer a native buffer allocated with {@link #newAlignedBuffer(int, int)}. + */ + public static void freeBuffer(MemorySegment buffer) { + FFMNativeHelper.freeBuffer(buffer); + } + + /** + * Documented at {@link LibaioFile#write(long, int, ByteBuffer, SubmitInfo)}. + */ + void submitWrite(int fd, + IOControl ioControl, + long position, + int size, + ByteBuffer bufferWrite, + Callback callback) throws IOException { + this.ffmNativeHelper.submitWrite(fd, ioControl, position, size, bufferWrite, callback); + } + + /** + * Documented at {@link LibaioFile#read(long, int, ByteBuffer, SubmitInfo)}. + */ + void submitRead(int fd, + IOControl ioControl, + long position, + int size, + ByteBuffer bufferWrite, + Callback callback) throws IOException { + this.ffmNativeHelper.submitRead(fd, ioControl, position, size, bufferWrite, callback); + } + + /** + * Note: this shouldn't be done concurrently. + * This method will block until the min condition is satisfied on the poll. + *

+ * The callbacks will include the original callback sent at submit (read or write). + */ + int poll(IOControl ioControl, Callback[] callbacks, int min, int max) { + return this.ffmNativeHelper.poll(ioControl, callbacks, min, max); + } + + /** + * This method will block as long as the context is open. + */ + void blockedPoll(IOControl ioControl, boolean useFdatasync) { + this.ffmNativeHelper.blockedPoll(ioControl, useFdatasync); + } + + static int getNativeVersion() { + return FFMNativeHelper.getNativeVersion(); + } + + public static boolean lock(int fd) { + return FFMNativeHelper.lock(fd); + } + + public static void memsetBuffer(ByteBuffer buffer, int size) { + FFMNativeHelper.memsetBuffer(buffer, size); + } + + static long getSize(int fd) throws IOException { + return FFMNativeHelper.getSize(fd); + } + + static int getBlockSizeFD(int fd) throws IOException { + return FFMNativeHelper.getBlockSizeFD(fd); + } + + public static int getBlockSize(File path) throws IOException { + return getBlockSize(path.getAbsolutePath()); + } + + public static int getBlockSize(String path) throws IOException { + return FFMNativeHelper.getBlockSize(path); + } + + static void fallocate(int fd, long size) throws IOException { + FFMNativeHelper.fallocate(fd, size); + } + + static void fill(int fd, int alignment, long size) throws IOException { + FFMNativeHelper.fill(fd, alignment, size); + } + + static void writeInternal(int fd, long position, long size, ByteBuffer bufferWrite) throws IOException { + FFMNativeHelper.writeInternal(fd, position, size, bufferWrite); + } +} diff --git a/artemis-ffm/src/main/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..314b66e6901 --- /dev/null +++ b/artemis-ffm/src/main/java24/org/apache/artemis/nativo/jlibaio/ffm/FFMHandles.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.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")); + + static final MethodHandle FCNTL_HANDLE = LINKER.downcallHandle(STDLIB.find("fcntl").orElseThrow(() -> new UnsatisfiedLinkError("fcntl not found in STDLIB")), FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_LONG)); + + 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..092d4258c3b --- /dev/null +++ b/artemis-ffm/src/main/java24/org/apache/artemis/nativo/jlibaio/ffm/FFMNativeHelper.java @@ -0,0 +1,1147 @@ +/* + * 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.FCNTL_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) { + lastFile = fd; + try { + if (openedWithODirect(fd)) { + if (logger.isTraceEnabled()) { + logger.trace("blockedPoll: fdatasync is not needed, as file fd={} is opened with O_DIRECT", fd); + } + } else { + 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 boolean openedWithODirect(int fd) throws Throwable { + final int F_GETFL = 3; + int flag = (int) FCNTL_HANDLE.invoke(fd, F_GETFL, 0L); + return (flag & O_DIRECT) != 0; + } + + 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/main/java24/org/apache/artemis/nativo/jlibaio/util/CallbackCache.java b/artemis-ffm/src/main/java24/org/apache/artemis/nativo/jlibaio/util/CallbackCache.java new file mode 100644 index 00000000000..59e036a81f6 --- /dev/null +++ b/artemis-ffm/src/main/java24/org/apache/artemis/nativo/jlibaio/util/CallbackCache.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.artemis.nativo.jlibaio.util; + +import org.apache.artemis.nativo.jlibaio.SubmitInfo; + +/** + * this is an utility class where you can reuse Callback objects for your LibaioContext usage. + */ +public class CallbackCache { + + private final SubmitInfo[] pool; + + private int put = 0; + private int get = 0; + private int available = 0; + private final int size; + + private final Object lock = new Object(); + + public CallbackCache(int size) { + this.pool = new SubmitInfo[size]; + this.size = size; + } + + public Callback get() { + synchronized (lock) { + if (available <= 0) { + return null; + } else { + Callback retValue = (Callback) pool[get]; + pool[get] = null; + if (retValue == null) { + throw new NullPointerException("You should initialize the pool before using it"); + } + available--; + get++; + if (get >= size) { + get = 0; + } + return retValue; + } + } + } + + public CallbackCache put(Callback callback) { + if (callback == null) { + return null; + } + synchronized (lock) { + if (available < size) { + available++; + pool[put++] = callback; + if (put >= size) { + put = 0; + } + } + } + return this; + } +} diff --git a/artemis-ffm/src/test/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..f1ca22ea602 --- /dev/null +++ b/artemis-ffm/src/test/java24/org/apache/artemis/nativo/jlibaio/test/LibaioStressTest.java @@ -0,0 +1,272 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.artemis.nativo.jlibaio.test; + +import org.apache.artemis.nativo.jlibaio.LibaioContext; +import org.apache.artemis.nativo.jlibaio.LibaioFile; +import org.apache.artemis.nativo.jlibaio.SubmitInfo; +import org.apache.artemis.nativo.jlibaio.util.CallbackCache; +import org.junit.After; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.lang.foreign.MemorySegment; +import java.nio.ByteBuffer; +import java.util.concurrent.TimeUnit; + +/** + * This test is using a different package from {@link LibaioFile} + * as I need to validate public methods on the API + */ +public class LibaioStressTest { + + private static final Logger logger = LoggerFactory.getLogger(LibaioStressTest.class); + + private static final int STRESS_TIME = Integer.parseInt(System.getProperty("test.stress.time", "5000")); + + static { + logger.debug("LibaioStressTest:: -Dtest.stress.time=" + STRESS_TIME); + } + + @BeforeClass + public static void testAssume() { + Assume.assumeTrue(LibaioContext.isLoaded()); + } + + /** + * This is just an arbitrary number for a number of elements you need to pass to the libaio init method + * Some of the tests are using half of this number, so if anyone decide to change this please use an even number. + */ + private static final int LIBAIO_QUEUE_SIZE = 4096; + + private int errors = 0; + + private boolean running = true; + + @Rule + public TemporaryFolder temporaryFolder; + + public LibaioContext control; + + @Before + public void setUpFactory() { + control = new LibaioContext<>(LIBAIO_QUEUE_SIZE, true, false); + } + + @After + public void deleteFactory() { + control.close(); + validateLibaio(); + } + + public void validateLibaio() { + Assert.assertEquals(0, LibaioContext.getTotalMaxIO()); + } + + public LibaioStressTest() { + /* + * I didn't use /tmp for three reasons + * - Most systems now will use tmpfs which is not compatible with O_DIRECT + * - This would fill up /tmp in case of failures. + * - target is cleaned up every time you do a mvn clean, so it's safer + */ + File parent = new File("./target"); + parent.mkdirs(); + temporaryFolder = new TemporaryFolder(parent); + } + + @Test + public void testOpen() throws Exception { + LibaioFile fileDescriptor = control.openFile(temporaryFolder.newFile("test.bin"), true); + fileDescriptor.close(); + } + + CallbackCache callbackCache = new CallbackCache<>(LIBAIO_QUEUE_SIZE); + + class MyClass implements SubmitInfo { + + ReusableLatch reusableLatch; + + @Override + public void onError(int errno, String message) { + + } + + @Override + public void done() { + try { + reusableLatch.countDown(); + reusableLatch = null; + callbackCache.put(this); + } catch (Throwable e) { + e.printStackTrace(); + System.exit(-1); + } + } + } + + @Test + public void testForceSyscall() { + Assert.assertFalse(LibaioContext.isForceSyscall()); + LibaioContext.setForceSyscall(true); + Assert.assertTrue(LibaioContext.isForceSyscall()); + LibaioContext.setForceSyscall(false); + } + + @Test + public void testStressWritesNoSleeps() throws Exception { + testStressWrites(false); + } + + @Test + public void testStressWrites() throws Exception { + testStressWrites(true); + } + + private void testStressWrites(boolean sleeps) throws Exception { + Assume.assumeFalse(LibaioContext.isForceSyscall()); + + Thread t = new Thread() { + @Override + public void run() { + control.poll(); + } + }; + + t.start(); + + Thread t2 = new Thread(() -> { + while (running) { + try { + Thread.sleep(1000); + } catch (Exception e) { + } + // this is just to make things more interesting from the POV of testing + System.gc(); + } + }); + + t2.start(); + + Thread test1 = startThread("test1.bin", sleeps); + Thread test2 = startThread("test2.bin", sleeps); + Thread.sleep(STRESS_TIME); // Configured timeout on the test + running = false; + test2.join(); + test1.join(); + t2.join(); + + Assert.assertFalse(LibaioContext.isForceSyscall()); + return; + } + + private Thread startThread(String name, boolean sleeps) { + Thread t_test = new Thread(() -> { + try { + doFile(name, sleeps); + } catch (IOException e) { + e.printStackTrace(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }); + t_test.start(); + + return t_test; + } + + private void doFile(String fileName, boolean sleeps) throws IOException, InterruptedException { + ReusableLatch latchWrites = new ReusableLatch(0); + + File file = temporaryFolder.newFile(fileName); + LibaioFile fileDescriptor = control.openFile(file, true); + + // ByteBuffer buffer = ByteBuffer.allocateDirect(4096); + MemorySegment memorySegment = LibaioContext.newAlignedBuffer(4096, 4096); + ByteBuffer buffer = memorySegment.asByteBuffer(); + + int maxSize = 4096 * LIBAIO_QUEUE_SIZE; + fileDescriptor.fill(4096, maxSize); + for (int i = 0; i < 4096; i++) { + buffer.put((byte) 'a'); + } + + buffer.rewind(); + + int pos = 0; + + long count = 0; + + long nextBreak = System.currentTimeMillis() + 3000; + + while (running) { + count++; + + if (System.currentTimeMillis() > nextBreak) { + if (!latchWrites.await(10, TimeUnit.SECONDS)) { + System.err.println("Latch did not complete for some reason"); + errors++; + return; + } + fileDescriptor.close(); + + fileDescriptor = control.openFile(file, true); + pos = 0; + // we close / open a file every 5 seconds + nextBreak = System.currentTimeMillis() + 5000; + } + + if (count % (sleeps ? 1_000 : 100_000) == 0) { + logger.debug("Writen " + count + " buffers at " + fileName); + } + MyClass myClass = callbackCache.get(); + + if (myClass == null) { + myClass = new MyClass(); + } + + myClass.reusableLatch = latchWrites; + myClass.reusableLatch.countUp(); + + if (sleeps) { + if (count % 100 == 0) { + Thread.sleep(100); + } + } + fileDescriptor.write(pos, 4096, buffer, myClass); + pos += 4096; + + if (pos >= maxSize) { + pos = 0; + } + + } + + fileDescriptor.close(); + } + +} diff --git a/artemis-ffm/src/test/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..9f54400911f --- /dev/null +++ b/artemis-ffm/src/test/java24/org/apache/artemis/nativo/jlibaio/test/LibaioTest.java @@ -0,0 +1,790 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.artemis.nativo.jlibaio.test; + +import org.apache.artemis.nativo.jlibaio.LibaioContext; +import org.apache.artemis.nativo.jlibaio.LibaioFile; +import org.apache.artemis.nativo.jlibaio.SubmitInfo; +import org.junit.After; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.lang.foreign.MemorySegment; +import java.lang.ref.Cleaner; +import java.lang.ref.WeakReference; +import java.nio.ByteBuffer; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * This test is using a different package from {@link LibaioFile} + * as I need to validate public methods on the API + */ +public class LibaioTest { + + private static final Logger logger = LoggerFactory.getLogger(LibaioTest.class); + + @BeforeClass + public static void testAIO() { + Assume.assumeTrue(LibaioContext.isLoaded()); + + File parent = new File("./target"); + File file = new File(parent, "testFile"); + + try { + parent.mkdirs(); + + boolean failed = false; + try (LibaioContext control = new LibaioContext<>(1, true, true); LibaioFile fileDescriptor = control.openFile(file, true)) { + fileDescriptor.fallocate(4 * 1024); + } catch (Exception e) { + e.printStackTrace(); + failed = true; + } + + Assume.assumeFalse("There is not enough support to libaio", failed); + } finally { + file.delete(); + } + } + + /** + * This is just an arbitrary number for a number of elements you need to pass to the libaio init method + * Some of the tests are using half of this number, so if anyone decide to change this please use an even number. + */ + private static final int LIBAIO_QUEUE_SIZE = 50; + + @Rule + public TemporaryFolder temporaryFolder; + + public LibaioContext control; + + @Before + public void setUpFactory() { + control = new LibaioContext<>(LIBAIO_QUEUE_SIZE, true, true); + } + + @After + public void deleteFactory() { + control.close(); + validateLibaio(); + } + + public void validateLibaio() { + Assert.assertEquals(0, LibaioContext.getTotalMaxIO()); + } + + public LibaioTest() { + /* + * I didn't use /tmp for three reasons + * - Most systems now will use tmpfs which is not compatible with O_DIRECT + * - This would fill up /tmp in case of failures. + * - target is cleaned up every time you do a mvn clean, so it's safer + */ + File parent = new File("./target"); + parent.mkdirs(); + temporaryFolder = new TemporaryFolder(parent); + } + + @Test + public void testOpen() throws Exception { + LibaioFile fileDescriptor = control.openFile(temporaryFolder.newFile("test.bin"), true); + fileDescriptor.close(); + } + + @Test + public void testInitAndFallocate10M() throws Exception { + testInit(10 * 1024 * 1024); + } + + @Test + public void testInitAndFallocate10M100K() throws Exception { + testInit(10 * 1024 * 1024 + 100 * 1024); + } + + private void testInit(int size) throws IOException { + LibaioFile fileDescriptor = control.openFile(temporaryFolder.newFile("test.bin"), true); + fileDescriptor.fallocate(size); + + MemorySegment buffer = fileDescriptor.newBuffer(size); + fileDescriptor.read(0, size, buffer.asByteBuffer(), new TestInfo()); + + TestInfo[] callbacks = new TestInfo[1]; + control.poll(callbacks, 1, 1); + + fileDescriptor.close(); + + buffer.asByteBuffer().position(0); + + LibaioFile fileDescriptor2 = control.openFile(temporaryFolder.newFile("test2.bin"), true); + fileDescriptor2.fill(fileDescriptor.getBlockSize(), size); + fileDescriptor2.read(0, size, buffer.asByteBuffer(), new TestInfo()); + + control.poll(callbacks, 1, 1); + for (int i = 0; i < size; i++) { + Assert.assertEquals(0, buffer.asByteBuffer().get()); + } + + LibaioContext.freeBuffer(buffer); + } + + @Test + public void testInitAndFallocate10K() throws Exception { + testInit(10 * 4096); + } + + @Test + public void testInitAndFallocate20K() throws Exception { + testInit(20 * 4096); + } + + @Test + public void testSubmitWriteOnTwoFiles() throws Exception { + + File file1 = temporaryFolder.newFile("test.bin"); + File file2 = temporaryFolder.newFile("test2.bin"); + + fillupFile(file1, LIBAIO_QUEUE_SIZE / 2); + fillupFile(file2, LIBAIO_QUEUE_SIZE / 2); + + LibaioFile[] fileDescriptor = new LibaioFile[]{control.openFile(file1, true), control.openFile(file2, true)}; + + Assert.assertEquals((LIBAIO_QUEUE_SIZE / 2) * 4096, fileDescriptor[0].getSize()); + Assert.assertEquals((LIBAIO_QUEUE_SIZE / 2) * 4096, fileDescriptor[1].getSize()); + Assert.assertEquals(fileDescriptor[0].getBlockSize(), fileDescriptor[1].getBlockSize()); + Assert.assertEquals(LibaioContext.getBlockSize(temporaryFolder.getRoot()), LibaioContext.getBlockSize(file1)); + Assert.assertEquals(LibaioContext.getBlockSize(file1), LibaioContext.getBlockSize(file2)); + logger.debug("blockSize = " + fileDescriptor[0].getBlockSize()); + logger.debug("blockSize /tmp= " + LibaioContext.getBlockSize("/tmp")); + + MemorySegment buffer = LibaioContext.newAlignedBuffer(4096, 4096); + + try { + for (int i = 0; i < 4096; i++) { + buffer.asByteBuffer().put((byte) 'a'); + } + + TestInfo callback = new TestInfo(); + TestInfo[] callbacks = new TestInfo[LIBAIO_QUEUE_SIZE]; + + for (int i = 0; i < LIBAIO_QUEUE_SIZE / 2; i++) { + for (LibaioFile file : fileDescriptor) { + file.write(i * 4096, 4096, buffer.asByteBuffer(), callback); + } + } + + Assert.assertEquals(LIBAIO_QUEUE_SIZE, control.poll(callbacks, LIBAIO_QUEUE_SIZE, LIBAIO_QUEUE_SIZE)); + + for (Object returnedCallback : callbacks) { + Assert.assertSame(returnedCallback, callback); + } + + for (LibaioFile file : fileDescriptor) { + MemorySegment bigbuffer = LibaioContext.newAlignedBuffer(4096 * 25, 4096); + file.read(0, 4096 * 25, bigbuffer.asByteBuffer(), callback); + Assert.assertEquals(1, control.poll(callbacks, 1, LIBAIO_QUEUE_SIZE)); + + for (Object returnedCallback : callbacks) { + Assert.assertSame(returnedCallback, callback); + } + + for (int i = 0; i < 4096 * 25; i++) { + Assert.assertEquals((byte) 'a', bigbuffer.asByteBuffer().get()); + } + + LibaioContext.freeBuffer(bigbuffer); + + file.close(); + } + } finally { + LibaioContext.freeBuffer(buffer); + } + } + + @Test + public void testSubmitWriteAndRead() throws Exception { + TestInfo callback = new TestInfo(); + + TestInfo[] callbacks = new TestInfo[LIBAIO_QUEUE_SIZE]; + + LibaioFile fileDescriptor = control.openFile(temporaryFolder.newFile("test.bin"), true); + + // ByteBuffer buffer = ByteBuffer.allocateDirect(4096); + MemorySegment buffer = LibaioContext.newAlignedBuffer(4096, 4096); + + try { + for (int i = 0; i < 4096; i++) { + buffer.asByteBuffer().put((byte) 'a'); + } + + buffer.asByteBuffer().rewind(); + + fileDescriptor.write(0, 4096, buffer.asByteBuffer(), callback); + + int retValue = control.poll(callbacks, 1, LIBAIO_QUEUE_SIZE); + Assert.assertEquals(1, retValue); + + Assert.assertSame(callback, callbacks[0]); + + LibaioContext.freeBuffer(buffer); + + buffer = LibaioContext.newAlignedBuffer(4096, 4096); + + for (int i = 0; i < 4096; i++) { + buffer.asByteBuffer().put((byte) 'B'); + } + + fileDescriptor.write(0, 4096, buffer.asByteBuffer(), new TestInfo()); + + Assert.assertEquals(1, control.poll(callbacks, 1, LIBAIO_QUEUE_SIZE)); + + buffer.asByteBuffer().rewind(); + + fileDescriptor.read(0, 4096, buffer.asByteBuffer(), new TestInfo()); + + Assert.assertEquals(1, control.poll(callbacks, 1, LIBAIO_QUEUE_SIZE)); + + for (int i = 0; i < 4096; i++) { + Assert.assertEquals('B', buffer.asByteBuffer().get()); + } + } finally { + LibaioContext.freeBuffer(buffer); + fileDescriptor.close(); + } + } + + @Test + /* + * This file is making use of libaio without O_DIRECT + * We won't need special buffers on this case. + */ public void testSubmitWriteAndReadRegularBuffers() throws Exception { + TestInfo callback = new TestInfo(); + + TestInfo[] callbacks = new TestInfo[LIBAIO_QUEUE_SIZE]; + + File file = temporaryFolder.newFile("test.bin"); + + fillupFile(file, LIBAIO_QUEUE_SIZE); + + LibaioFile fileDescriptor = control.openFile(file, false); + + final int BUFFER_SIZE = 50; + + ByteBuffer buffer = ByteBuffer.allocateDirect(BUFFER_SIZE); + + try { + for (int i = 0; i < BUFFER_SIZE; i++) { + buffer.put((byte) 'a'); + } + + buffer.rewind(); + + fileDescriptor.write(0, BUFFER_SIZE, buffer, callback); + + int retValue = control.poll(callbacks, 1, LIBAIO_QUEUE_SIZE); + logger.debug("Return from poll::" + retValue); + Assert.assertEquals(1, retValue); + + Assert.assertSame(callback, callbacks[0]); + + buffer.rewind(); + + for (int i = 0; i < BUFFER_SIZE; i++) { + buffer.put((byte) 'B'); + } + + fileDescriptor.write(0, BUFFER_SIZE, buffer, new TestInfo()); + + Assert.assertEquals(1, control.poll(callbacks, 1, LIBAIO_QUEUE_SIZE)); + + buffer.rewind(); + + fileDescriptor.read(0, 50, buffer, new TestInfo()); + + Assert.assertEquals(1, control.poll(callbacks, 1, LIBAIO_QUEUE_SIZE)); + + for (int i = 0; i < BUFFER_SIZE; i++) { + Assert.assertEquals('B', buffer.get()); + } + } finally { + fileDescriptor.close(); + } + } + + @Test + public void testSubmitRead() throws Exception { + + TestInfo callback = new TestInfo(); + + TestInfo[] callbacks = new TestInfo[LIBAIO_QUEUE_SIZE]; + + File file = temporaryFolder.newFile("test.bin"); + + fillupFile(file, LIBAIO_QUEUE_SIZE); + + LibaioFile fileDescriptor = control.openFile(file, true); + + MemorySegment buffer = LibaioContext.newAlignedBuffer(4096, 4096); + + final int BUFFER_SIZE = 4096; + try { + for (int i = 0; i < BUFFER_SIZE; i++) { + buffer.asByteBuffer().put((byte) '@'); + } + + fileDescriptor.write(0, BUFFER_SIZE, buffer.asByteBuffer(), callback); + Assert.assertEquals(1, control.poll(callbacks, 1, LIBAIO_QUEUE_SIZE)); + Assert.assertSame(callback, callbacks[0]); + + buffer.asByteBuffer().rewind(); + + fileDescriptor.read(0, BUFFER_SIZE, buffer.asByteBuffer(), callback); + + Assert.assertEquals(1, control.poll(callbacks, 1, LIBAIO_QUEUE_SIZE)); + + Assert.assertSame(callback, callbacks[0]); + + for (int i = 0; i < BUFFER_SIZE; i++) { + Assert.assertEquals('@', buffer.asByteBuffer().get()); + } + } finally { + LibaioContext.freeBuffer(buffer); + fileDescriptor.close(); + } + } + + @Test + @Ignore + public void testInvalidWrite() throws Exception { + + TestInfo callback = new TestInfo(); + + TestInfo[] callbacks = new TestInfo[LIBAIO_QUEUE_SIZE]; + + File file = temporaryFolder.newFile("test.bin"); + + fillupFile(file, LIBAIO_QUEUE_SIZE); + + LibaioFile fileDescriptor = control.openFile(file, true); + + try { + ByteBuffer buffer = ByteBuffer.allocateDirect(300); + for (int i = 0; i < 300; i++) { + buffer.put((byte) 'z'); + } + + fileDescriptor.write(0, 300, buffer, callback); + + Assert.assertEquals(1, control.poll(callbacks, 1, LIBAIO_QUEUE_SIZE)); + + Assert.assertTrue(callbacks[0].isError()); + + // Error condition + Assert.assertSame(callbacks[0], callback); + + logger.debug("Error:" + callbacks[0]); + + MemorySegment memorySegment = fileDescriptor.newBuffer(4096); + buffer = memorySegment.asByteBuffer(); + for (int i = 0; i < 4096; i++) { + buffer.put((byte) 'z'); + } + + callback = new TestInfo(); + + fileDescriptor.write(0, 4096, buffer, callback); + + Assert.assertEquals(1, control.poll(callbacks, 1, 1)); + + Assert.assertSame(callback, callbacks[0]); + + fileDescriptor.write(5, 4096, buffer, callback); + + Assert.assertEquals(1, control.poll(callbacks, 1, 1)); + + Assert.assertTrue(callbacks[0].isError()); + + callbacks = null; + callback = null; + + TestInfo.checkLeaks(); + } finally { + fileDescriptor.close(); + } + } + + @Test + public void testLeaks() throws Exception { + File file = temporaryFolder.newFile("test.bin"); + + fillupFile(file, LIBAIO_QUEUE_SIZE * 2); + + TestInfo[] callbacks = new TestInfo[LIBAIO_QUEUE_SIZE]; + + LibaioFile fileDescriptor = control.openFile(file, true); + + MemorySegment bufferWrite = LibaioContext.newAlignedBuffer(4096, 4096); + + try { + for (int i = 0; i < 4096; i++) { + bufferWrite.asByteBuffer().put((byte) 'B'); + } + + for (int j = 0; j < LIBAIO_QUEUE_SIZE * 2; j++) { + for (int i = 0; i < LIBAIO_QUEUE_SIZE; i++) { + TestInfo countClass = new TestInfo(); + fileDescriptor.write(i * 4096, 4096, bufferWrite.asByteBuffer(), countClass); + } + + Assert.assertEquals(LIBAIO_QUEUE_SIZE, control.poll(callbacks, LIBAIO_QUEUE_SIZE, LIBAIO_QUEUE_SIZE)); + + for (int i = 0; i < LIBAIO_QUEUE_SIZE; i++) { + Assert.assertNotNull(callbacks[i]); + callbacks[i] = null; + } + } + + TestInfo.checkLeaks(); + } finally { + LibaioContext.freeBuffer(bufferWrite); + } + } + + @Test + public void testLock() throws Exception { + File file = temporaryFolder.newFile("test.bin"); + + LibaioFile fileDescriptor = control.openFile(file, true); + fileDescriptor.lock(); + + fileDescriptor.close(); + } + + @Test + public void testAlloc() throws Exception { + File file = temporaryFolder.newFile("test.bin"); + + LibaioFile fileDescriptor = control.openFile(file, true); + fileDescriptor.fill(fileDescriptor.getBlockSize(), 10 * 1024 * 1024); + + fileDescriptor.close(); + } + + @Test + public void testReleaseNullBuffer() throws Exception { + boolean failed = false; + try { + LibaioContext.freeBuffer(null); + } catch (Exception expected) { + failed = true; + } + + Assert.assertTrue("Exception happened!", failed); + + } + + @Test + public void testMemset() throws Exception { + + MemorySegment memorySegment = LibaioContext.newAlignedBuffer(4096 * 8, 4096); + ByteBuffer buffer = memorySegment.asByteBuffer(); + + for (int i = 0; i < buffer.capacity(); i++) { + buffer.put((byte) 'z'); + } + + buffer.position(0); + + for (int i = 0; i < buffer.capacity(); i++) { + Assert.assertEquals((byte) 'z', buffer.get()); + } + + control.memsetBuffer(buffer); + + buffer.position(0); + + for (int i = 0; i < buffer.capacity(); i++) { + Assert.assertEquals((byte) 0, buffer.get()); + } + + LibaioContext.freeBuffer(memorySegment); + + } + + @Test + @Ignore + public void testIOExceptionConditions() throws Exception { + boolean exceptionThrown = false; + + control.close(); + control = new LibaioContext<>(LIBAIO_QUEUE_SIZE, false, true); + try { + // There is no space for a queue this huge, the native layer should throw the exception + LibaioContext newController = new LibaioContext(Integer.MAX_VALUE, false, true); + } catch (RuntimeException e) { + exceptionThrown = true; + } + + Assert.assertTrue(exceptionThrown); + exceptionThrown = false; + + try { + // this should throw an exception, we shouldn't be able to open a directory! + control.openFile(temporaryFolder.getRoot(), true); + } catch (IOException expected) { + exceptionThrown = true; + } + + Assert.assertTrue(exceptionThrown); + + exceptionThrown = false; + + LibaioFile fileDescriptor = control.openFile(temporaryFolder.newFile(), true); + fileDescriptor.close(); + try { + fileDescriptor.close(); + } catch (IOException expected) { + exceptionThrown = true; + } + + Assert.assertTrue(exceptionThrown); + + fileDescriptor = control.openFile(temporaryFolder.newFile(), true); + + MemorySegment memorySegment = fileDescriptor.newBuffer(4096); + ByteBuffer buffer = memorySegment.asByteBuffer(); + + try { + for (int i = 0; i < 4096; i++) { + buffer.put((byte) 'a'); + } + + for (int i = 0; i < LIBAIO_QUEUE_SIZE; i++) { + fileDescriptor.write(i * 4096, 4096, buffer, new TestInfo()); + } + + boolean ex = false; + try { + fileDescriptor.write(0, 4096, buffer, new TestInfo()); + } catch (Exception e) { + ex = true; + } + + Assert.assertTrue(ex); + + TestInfo[] callbacks = new TestInfo[LIBAIO_QUEUE_SIZE]; + Assert.assertEquals(LIBAIO_QUEUE_SIZE, control.poll(callbacks, LIBAIO_QUEUE_SIZE, LIBAIO_QUEUE_SIZE)); + + // it should be possible to write now after queue space being released + fileDescriptor.write(0, 4096, buffer, new TestInfo()); + Assert.assertEquals(1, control.poll(callbacks, 1, 100)); + + TestInfo errorCallback = new TestInfo(); + // odd positions will have failures through O_DIRECT + fileDescriptor.read(3, 4096, buffer, errorCallback); + Assert.assertEquals(1, control.poll(callbacks, 1, 50)); + Assert.assertTrue(callbacks[0].isError()); + Assert.assertSame(errorCallback, (callbacks[0])); + + // to help GC and the checkLeaks + callbacks = null; + errorCallback = null; + + TestInfo.checkLeaks(); + + exceptionThrown = false; + try { + LibaioContext.newAlignedBuffer(300, 4096); + } catch (RuntimeException e) { + exceptionThrown = true; + } + + Assert.assertTrue(exceptionThrown); + + exceptionThrown = false; + try { + LibaioContext.newAlignedBuffer(-4096, 4096); + } catch (RuntimeException e) { + exceptionThrown = true; + } + + Assert.assertTrue(exceptionThrown); + } finally { + LibaioContext.freeBuffer(memorySegment); + } + } + + @Test + public void testBlockedCallback() throws Exception { + final LibaioContext blockedContext = new LibaioContext(LIBAIO_QUEUE_SIZE, true, true); + Thread t = new Thread() { + @Override + public void run() { + blockedContext.poll(); + } + }; + + t.start(); + + int NUMBER_OF_BLOCKS = LIBAIO_QUEUE_SIZE * 10; + + final CountDownLatch latch = new CountDownLatch(NUMBER_OF_BLOCKS); + + File file = temporaryFolder.newFile("sub-file.txt"); + LibaioFile aioFile = blockedContext.openFile(file, true); + aioFile.fill(aioFile.getBlockSize(), NUMBER_OF_BLOCKS * 4096); + + final AtomicInteger errors = new AtomicInteger(0); + + class MyCallback implements SubmitInfo { + + @Override + public void onError(int errno, String message) { + errors.incrementAndGet(); + } + + @Override + public void done() { + latch.countDown(); + } + } + + MyCallback callback = new MyCallback(); + + MemorySegment memorySegment = LibaioContext.newAlignedBuffer(4096, 4096); + ByteBuffer buffer = memorySegment.asByteBuffer(); + + for (int i = 0; i < 4096; i++) { + buffer.put((byte) 'a'); + } + + long start = System.currentTimeMillis(); + + for (int i = 0; i < NUMBER_OF_BLOCKS; i++) { + aioFile.write(i * 4096, 4096, buffer, callback); + } + + long end = System.currentTimeMillis(); + + latch.await(); + + logger.debug("time = " + (end - start) + " writes/second=" + NUMBER_OF_BLOCKS * 1000L / (end - start)); + + blockedContext.close(); + t.join(); + } + + private void fillupFile(File file, int blocks) throws IOException { + FileOutputStream fileOutputStream = new FileOutputStream(file); + byte[] bufferWrite = new byte[4096]; + for (int i = 0; i < 4096; i++) { + bufferWrite[i] = (byte) 0; + } + + for (int i = 0; i < blocks; i++) { + fileOutputStream.write(bufferWrite); + } + + fileOutputStream.close(); + } + + static class TestInfo implements SubmitInfo { + + static final Cleaner cleaner; + + static { + Cleaner tempCleaner; + try { + tempCleaner = Cleaner.create(); + } catch (Throwable e) { + e.printStackTrace(); + tempCleaner = null; + } + cleaner = tempCleaner; + } + + static AtomicInteger count = new AtomicInteger(); + + public static void checkLeaks() throws InterruptedException { + for (int i = 0; count.get() != 0 && i < 50; i++) { + WeakReference reference = new WeakReference(new Object()); + while (reference.get() != null) { + System.gc(); + Thread.sleep(100); + } + } + Assert.assertEquals(0, count.get()); + } + + boolean error = false; + String errorMessage; + int errno; + + TestInfo() { + count.incrementAndGet(); + cleaner.register(this, count::decrementAndGet); + + } + + @Override + public void onError(int errno, String message) { + this.errno = errno; + this.errorMessage = message; + this.error = true; + } + + @Override + public void done() { + } + + public int getErrno() { + return errno; + } + + public void setErrno(int errno) { + this.errno = errno; + } + + public boolean isError() { + return error; + } + + public void setError(boolean error) { + this.error = error; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + } +} diff --git a/artemis-journal/src/main/java/org/apache/activemq/artemis/core/io/aio/ActiveMQFileLock.java b/artemis-ffm/src/test/java24/org/apache/artemis/nativo/jlibaio/test/LoadedTest.java similarity index 58% rename from artemis-journal/src/main/java/org/apache/activemq/artemis/core/io/aio/ActiveMQFileLock.java rename to artemis-ffm/src/test/java24/org/apache/artemis/nativo/jlibaio/test/LoadedTest.java index e44b9362081..c531fd9a858 100644 --- a/artemis-journal/src/main/java/org/apache/activemq/artemis/core/io/aio/ActiveMQFileLock.java +++ b/artemis-ffm/src/test/java24/org/apache/artemis/nativo/jlibaio/test/LoadedTest.java @@ -14,30 +14,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.activemq.artemis.core.io.aio; -import java.io.IOException; -import java.nio.channels.FileChannel; -import java.nio.channels.FileLock; +package org.apache.artemis.nativo.jlibaio.test; -import org.apache.activemq.artemis.nativo.jlibaio.LibaioFile; +import org.apache.artemis.nativo.jlibaio.LibaioContext; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Test; -public class ActiveMQFileLock extends FileLock { +public class LoadedTest { - private final LibaioFile file; + private static final String OS = System.getProperty("os.name").toLowerCase(); + private static final boolean IS_LINUX = OS.startsWith("linux"); - public ActiveMQFileLock(final LibaioFile handle) { - super((FileChannel) null, 0, 0, false); - this.file = handle; + @Test + public void testValidateIsLoaded() { + Assume.assumeTrue(IS_LINUX); + Assert.assertTrue(LibaioContext.isLoaded()); } - @Override - public boolean isValid() { - return true; - } - - @Override - public void release() throws IOException { - file.close(); - } } 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..88a28a36233 --- /dev/null +++ b/artemis-ffm/src/test/java24/org/apache/artemis/nativo/jlibaio/test/OpenCloseContextTest.java @@ -0,0 +1,186 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.artemis.nativo.jlibaio.test; + +import org.apache.artemis.nativo.jlibaio.LibaioContext; +import org.apache.artemis.nativo.jlibaio.LibaioFile; +import org.apache.artemis.nativo.jlibaio.SubmitInfo; +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.lang.foreign.MemorySegment; +import java.nio.ByteBuffer; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class OpenCloseContextTest { + + Logger logger = LoggerFactory.getLogger(OpenCloseContextTest.class); + + @BeforeClass + public static void testAIO() { + Assume.assumeTrue(LibaioContext.isLoaded()); + } + + @Rule + public TemporaryFolder folder; + + public OpenCloseContextTest() { + folder = new TemporaryFolder(new File("./target")); + } + + @Test + public void testRepeatOpenCloseContext() throws Exception { + MemorySegment memorySegment = LibaioContext.newAlignedBuffer(512, 512); + ByteBuffer buffer = memorySegment.asByteBuffer(); + for (int i = 0; i < 512; i++) { + buffer.put((byte) 'x'); + } + + for (int i = 0; i < 10; i++) { + logger.debug("#test " + i); + final LibaioContext control = new LibaioContext<>(5, true, true); + Thread t = new Thread() { + @Override + public void run() { + control.poll(); + } + }; + t.start(); + LibaioFile file = control.openFile(folder.newFile(), true); + file.fill(file.getBlockSize(), 4 * 1024); + final CountDownLatch insideMethod = new CountDownLatch(1); + final CountDownLatch awaitInside = new CountDownLatch(1); + file.write(0, 512, buffer, new SubmitInfo() { + @Override + public void onError(int errno, String message) { + + } + + @Override + public void done() { + insideMethod.countDown(); + try { + awaitInside.await(); + } catch (Throwable e) { + e.printStackTrace(); + } + logger.debug("done"); + } + }); + + insideMethod.await(); + + file.write(512, 512, buffer, new SubmitInfo() { + @Override + public void onError(int errno, String message) { + } + + @Override + public void done() { + } + }); + + awaitInside.countDown(); + control.close(); + + t.join(); + } + + } + + @Test + public void testRepeatOpenCloseContext2() throws Exception { + MemorySegment memorySegment = LibaioContext.newAlignedBuffer(512, 512); + ByteBuffer buffer = memorySegment.asByteBuffer(); + for (int i = 0; i < 512; i++) { + buffer.put((byte) 'x'); + } + + for (int i = 0; i < 10; i++) { + logger.debug("#test " + i); + final LibaioContext control = new LibaioContext<>(5, true, true); + Thread t = new Thread() { + @Override + public void run() { + control.poll(); + } + }; + t.start(); + LibaioFile file = control.openFile(folder.newFile(), true); + file.fill(file.getBlockSize(), 4 * 1024); + final CountDownLatch insideMethod = new CountDownLatch(1); + final CountDownLatch awaitInside = new CountDownLatch(1); + file.write(0, 512, buffer, new SubmitInfo() { + @Override + public void onError(int errno, String message) { + + } + + @Override + public void done() { + insideMethod.countDown(); + try { + awaitInside.await(100, TimeUnit.MILLISECONDS); + } catch (Throwable e) { + e.printStackTrace(); + } + logger.debug("done"); + } + }); + + insideMethod.await(); + + file.write(512, 512, buffer, new SubmitInfo() { + @Override + public void onError(int errno, String message) { + } + + @Override + public void done() { + } + }); + + awaitInside.countDown(); + + control.close(); + + t.join(); + } + + } + + @Test + public void testCloseAndStart() throws Exception { + final LibaioContext control2 = new LibaioContext<>(5, true, true); + + final LibaioContext control = new LibaioContext<>(5, true, true); + control.close(); + control.poll(); + + control2.close(); + control2.poll(); + } + +} diff --git a/artemis-ffm/src/test/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..46516d095dd 100644 --- a/artemis-journal/pom.xml +++ b/artemis-journal/pom.xml @@ -92,4 +92,56 @@ test + + + jdk24onwards + + [24,) + + + + org.apache.artemis + artemis-ffm + ${project.version} + compile + + + + + + 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..f0df184b2f8 --- /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 22. + * This version uses stub implementations that throw UnsupportedOperationException. + * For JDK 22+, see the real implementation in src/main/java22. + */ +public class AIO2Helper { + + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + public static boolean isSupported() { + logger.info("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..e2b41afb72e --- /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 22+. + * This version uses the real AIO2SequentialFileFactory implementation with Panama FFM support. + * For JDK < 22, see the stub version in src/main/java. + */ +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..0c628303087 --- /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.isLoaded(); + } + + 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 875ca798626..32e2677d920 100644 --- a/artemis-server/pom.xml +++ b/artemis-server/pom.xml @@ -382,5 +382,20 @@ + + jdk24onwards + + [24,) + + + + org.apache.artemis + artemis-ffm + ${project.version} + compile + true + + + 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..207bf167e13 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 @@ -804,6 +804,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 = AIOSequentialFileFactory.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 1f69606a568..6476fefea93 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 @@ -1547,4 +1547,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 beb52d1624d..55ae013b97c 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 getParams() { private static ExecutorService ioExecutor; private static ExecutorService testExecutor; + @BeforeEach + public void assumeAIOisSupported() { + switch (journalType) { + case ASYNCIO -> + assumeTrue(AIOSequentialFileFactory.isSupported(), "AIO is not supported on this platform"); + + case ASYNCIO_2 -> + assumeTrue(AIO2Helper.isSupported(), "AIO2 is not supported on this platform"); + } + } + @BeforeAll public static void initExecutors() { executor = Executors.newSingleThreadExecutor(); @@ -88,9 +101,6 @@ public static void destroyExecutors() { */ @TestTemplate public void testFixJournalFileSize() throws Exception { - if (journalType == JournalType.ASYNCIO) { - assumeTrue(AIOSequentialFileFactory.isSupported(), "AIO is not supported on this platform"); - } final Configuration configuration = createDefaultInVMConfig().setJournalType(journalType); final ExecutorFactory executorFactory = new OrderedExecutorFactory(executor); final ExecutorFactory ioExecutorFactory = new OrderedExecutorFactory(ioExecutor); @@ -102,9 +112,6 @@ public void testFixJournalFileSize() throws Exception { @TestTemplate public void testAddBytesToLargeMessageNotLeakingByteBuffer() throws Exception { - if (journalType == JournalType.ASYNCIO) { - assumeTrue(AIOSequentialFileFactory.isSupported(), "AIO is not supported on this platform"); - } final Configuration configuration = createDefaultInVMConfig().setJournalType(journalType); final ExecutorFactory executorFactory = new OrderedExecutorFactory(executor); final ExecutorFactory ioExecutorFactory = new OrderedExecutorFactory(ioExecutor); diff --git a/pom.xml b/pom.xml index 2fcae7bbf64..27b4199aab9 100644 --- a/pom.xml +++ b/pom.xml @@ -58,6 +58,7 @@ artemis-jakarta-client-osgi artemis-jms-server artemis-jakarta-server + artemis-ffm artemis-journal artemis-ra artemis-jakarta-ra diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/artemis/tests/util/ActiveMQTestBase.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/artemis/tests/util/ActiveMQTestBase.java index 3a65d746f65..594da62ef14 100644 --- a/tests/artemis-test-support/src/main/java/org/apache/activemq/artemis/tests/util/ActiveMQTestBase.java +++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/artemis/tests/util/ActiveMQTestBase.java @@ -99,6 +99,7 @@ import org.apache.activemq.artemis.core.io.IOCallback; 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.nio.NIOSequentialFileFactory; import org.apache.activemq.artemis.core.journal.PreparedTransactionInfo; import org.apache.activemq.artemis.core.journal.RecordInfo; @@ -597,7 +598,9 @@ public static int getUDPDiscoveryPort(final int variant) { } public static JournalType getDefaultJournalType() { - if (AIOSequentialFileFactory.isSupported()) { + if (AIO2Helper.isSupported()) { + return JournalType.ASYNCIO_2; + } else if (AIOSequentialFileFactory.isSupported()) { return JournalType.ASYNCIO; } else { return JournalType.NIO; diff --git a/tests/integration-tests/pom.xml b/tests/integration-tests/pom.xml index 6972e0340ca..62841c2434a 100644 --- a/tests/integration-tests/pom.xml +++ b/tests/integration-tests/pom.xml @@ -407,6 +407,12 @@ mockserver-core test + + org.apache.artemis + artemis-ffm + ${project.version} + test + @@ -481,7 +487,8 @@ [16,) - --add-exports java.security.jgss/sun.security.krb5=ALL-UNNAMED + --add-exports java.security.jgss/sun.security.krb5=ALL-UNNAMED + diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/journal/AIO2JournalImplTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/journal/AIO2JournalImplTest.java new file mode 100644 index 00000000000..42931581fed --- /dev/null +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/journal/AIO2JournalImplTest.java @@ -0,0 +1,65 @@ +/* + * 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.tests.integration.journal; + +import java.io.File; + +import org.apache.activemq.artemis.ArtemisConstants; +import org.apache.activemq.artemis.core.io.SequentialFileFactory; +import org.apache.activemq.artemis.core.io.aio2.AIO2Helper; +import org.apache.activemq.artemis.tests.unit.core.journal.impl.JournalImplTestUnit; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; + +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +public class AIO2JournalImplTest extends JournalImplTestUnit { + + @BeforeAll + public static void hasAIO() { + assumeTrue(AIO2Helper.isSupported(), "AIO2 is not supported on this platform"); + } + + @Override + @BeforeEach + public void setUp() throws Exception { + super.setUp(); + if (!AIO2Helper.isSupported()) { + fail(String.format("libAIO is not loaded on %s %s %s", System.getProperty("os.name"), System.getProperty("os.arch"), System.getProperty("os.version"))); + } + } + + @Override + protected SequentialFileFactory getFileFactory() throws Exception { + File file = new File(getTestDir()); + + deleteDirectory(file); + + file.mkdir(); + + // forcing the alignment to be 512, as this test was hard coded around this size. + return AIO2Helper.getAIO2SequentialFileFactory(getTestDirfile(), ArtemisConstants.DEFAULT_JOURNAL_BUFFER_SIZE_AIO, 1000000, 10, false).setAlignment(512); + } + + @Override + protected int getAlignment() { + return 512; + } +} diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/journal/AIO2SequentialFileFactoryTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/journal/AIO2SequentialFileFactoryTest.java new file mode 100644 index 00000000000..bc893e19a8f --- /dev/null +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/journal/AIO2SequentialFileFactoryTest.java @@ -0,0 +1,60 @@ +/* + * 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.tests.integration.journal; + +import java.io.File; +import java.nio.ByteBuffer; + +import org.apache.activemq.artemis.core.io.SequentialFile; +import org.apache.activemq.artemis.core.io.SequentialFileFactory; +import org.apache.activemq.artemis.core.io.aio2.AIO2Helper; +import org.apache.activemq.artemis.tests.unit.core.journal.impl.SequentialFileFactoryTestBase; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +public class AIO2SequentialFileFactoryTest extends SequentialFileFactoryTestBase { + + @BeforeAll + public static void hasAIO() { + assumeTrue(AIO2Helper.isSupported(), "AIO2 is not supported on this platform"); + } + + @Override + protected SequentialFileFactory createFactory(String folder) { + return AIO2Helper.getAIO2SequentialFileFactory(new File(folder), 10); + } + + @Test + public void canCreateFactoryWithMaxIOLessThenTwo() { + SequentialFileFactory factory = AIO2Helper.getAIO2SequentialFileFactory(new File("ignore"), 1); + } + + @Test + public void testBuffer() throws Exception { + SequentialFile file = factory.createSequentialFile("filtetmp.log"); + file.open(); + ByteBuffer buff = factory.newBuffer(10); + assertEquals(factory.getAlignment(), buff.limit()); + file.close(); + factory.releaseBuffer(buff); + } +} diff --git a/tests/performance-jmh/pom.xml b/tests/performance-jmh/pom.xml index 4ba93de0f8b..dd994dd991d 100644 --- a/tests/performance-jmh/pom.xml +++ b/tests/performance-jmh/pom.xml @@ -56,6 +56,11 @@ org.apache.artemis artemis-commons + + org.apache.activemq + activemq-artemis-native + ${activemq-artemis-native-version} + org.openjdk.jmh jmh-core @@ -115,5 +120,42 @@ + + + jdk24onwards + + [24,) + + + + org.apache.artemis + artemis-ffm + ${project.version} + + + + + + maven-compiler-plugin + + + java24-compile + compile + + compile + + + 24 + 24 + 24 + ${project.basedir}/src/main/java24 + + + + + + + + diff --git a/tests/performance-jmh/src/main/java/org/apache/activemq/artemis/tests/performance/jmh/NativeLibaioBenchmarkTest.java b/tests/performance-jmh/src/main/java/org/apache/activemq/artemis/tests/performance/jmh/NativeLibaioBenchmarkTest.java new file mode 100644 index 00000000000..7d467b1756e --- /dev/null +++ b/tests/performance-jmh/src/main/java/org/apache/activemq/artemis/tests/performance/jmh/NativeLibaioBenchmarkTest.java @@ -0,0 +1,217 @@ +/* + * 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.tests.performance.jmh; + +import org.apache.activemq.artemis.nativo.jlibaio.LibaioContext; +import org.apache.activemq.artemis.nativo.jlibaio.LibaioFile; +import org.apache.activemq.artemis.nativo.jlibaio.SubmitInfo; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; + +import java.io.File; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +@State(Scope.Benchmark) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Fork(value = 2) +@Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 10, time = 200, timeUnit = TimeUnit.MILLISECONDS) +public class NativeLibaioBenchmarkTest { + + private static final int FILE_SIZE = 10000 * 4096; + private static final int BLOCK_SIZE = 4096; + + @Param({"2048"}) + private static int LIBAIO_QUEUE_SIZE; + + @Param({"10000"}) + private int recordCount; + + private File file; + private LibaioContext control; + private LibaioFile libaioFile; + + private ByteBuffer headerBuffer; + private ByteBuffer recordBuffer; + + private final AtomicReference currentLatch = new AtomicReference<>(); + + private Thread pollThread; + private volatile boolean polling = true; + + private final SubmitInfo callback = new SubmitInfo() { + @Override + public void onError(int errno, String message) { + // ignore for benchmark + } + + @Override + public void done() { + CountDownLatch latch = currentLatch.get(); + if (latch != null) { + latch.countDown(); + } + } + }; + + private long fileId = 1L; + + @Setup(Level.Trial) + public void setup() throws Exception { + file = File.createTempFile("aio-bench-jni-", ".dat"); + + control = new LibaioContext<>(LIBAIO_QUEUE_SIZE, true, true); + libaioFile = control.openFile(file, true); + + libaioFile.fallocate(FILE_SIZE); + + headerBuffer = LibaioContext.newAlignedBuffer(BLOCK_SIZE, BLOCK_SIZE); + recordBuffer = LibaioContext.newAlignedBuffer(BLOCK_SIZE, BLOCK_SIZE); + + initRecord(headerBuffer); // filling the record clock with 1 + initRecord(recordBuffer); // filling the record clock with 1 + + fillHeader(fileId); + updateRecord(recordBuffer, fileId, 0L); + + polling = true; + pollThread = new Thread(() -> { + try { + while (polling && !Thread.currentThread().isInterrupted()) { + control.poll(); + } + } catch (Throwable t) { + } + }, "aio-jmh-jni-poll-thread"); + pollThread.setDaemon(true); + pollThread.start(); + + generateGarbage(); + } + + // Garbage generator fields + private Thread garbageThread; + private volatile boolean garbage = true; + + private void generateGarbage() { + garbage = true; + garbageThread = new Thread(() -> { + while (garbage && !Thread.currentThread().isInterrupted()) { + List list = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + Byte[] garbageArray = new Byte[8192]; + list.add(garbageArray); + } + list = null; + Thread.yield(); + } + }, "aio-jmh-garbage-thread"); + garbageThread.setDaemon(true); + garbageThread.start(); + } + + @TearDown(Level.Trial) + public void tearDown() throws Exception { + stopGarbageGenerator(); + + polling = false; + + if (pollThread != null) { + pollThread.interrupt(); + pollThread.join(TimeUnit.SECONDS.toMillis(10)); + } + + if (libaioFile != null) { + libaioFile.close(); + } + if (control != null) { + control.close(); + } + if (headerBuffer != null) { + LibaioContext.freeBuffer(headerBuffer); + } + if (recordBuffer != null) { + LibaioContext.freeBuffer(recordBuffer); + } + if (file != null) { + file.delete(); + } + } + + private void stopGarbageGenerator() throws InterruptedException { + garbage = false; + if (garbageThread != null) { + garbageThread.interrupt(); + garbageThread.join(TimeUnit.SECONDS.toMillis(10)); + } + } + + @Benchmark + public void writeHeaderAndRecords() throws Exception { + CountDownLatch latch = new CountDownLatch(recordCount * 100); + currentLatch.set(latch); + + try { + fillHeader(fileId); + // libaioFile.write(0L, BLOCK_SIZE, headerBuffer, callback); + + for (int j = 0; j < 100; j++) { + for (int i = 0; i < recordCount; i++) { + updateRecord(recordBuffer, fileId, i); + long offset = BLOCK_SIZE + ((long) i * BLOCK_SIZE); + libaioFile.write(offset, BLOCK_SIZE, recordBuffer, callback); + } + } + + latch.await(); + } finally { + currentLatch.compareAndSet(latch, null); + } + } + + private void fillHeader(long fileId) { + headerBuffer.putLong(0, fileId); + } + + private void updateRecord(ByteBuffer buffer, long fileId, long recordId) { + buffer.putLong(0, fileId); + buffer.putLong(8, recordId); + } + + private void initRecord(ByteBuffer record) { + while (record.position() < BLOCK_SIZE) { + record.put((byte) 1); + } + } +} diff --git a/tests/performance-jmh/src/main/java/org/apache/activemq/artemis/tests/performance/jmh/TransactionalMessagePerfTest.java b/tests/performance-jmh/src/main/java/org/apache/activemq/artemis/tests/performance/jmh/TransactionalMessagePerfTest.java new file mode 100644 index 00000000000..b404ab5a851 --- /dev/null +++ b/tests/performance-jmh/src/main/java/org/apache/activemq/artemis/tests/performance/jmh/TransactionalMessagePerfTest.java @@ -0,0 +1,169 @@ +/* + * 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.tests.performance.jmh; + +import java.io.File; +import java.util.concurrent.TimeUnit; + +import org.apache.activemq.artemis.api.core.QueueConfiguration; +import org.apache.activemq.artemis.api.core.RoutingType; +import org.apache.activemq.artemis.api.core.SimpleString; +import org.apache.activemq.artemis.api.core.client.ActiveMQClient; +import org.apache.activemq.artemis.api.core.client.ClientConsumer; +import org.apache.activemq.artemis.api.core.client.ClientMessage; +import org.apache.activemq.artemis.api.core.client.ClientProducer; +import org.apache.activemq.artemis.api.core.client.ClientSession; +import org.apache.activemq.artemis.api.core.client.ClientSessionFactory; +import org.apache.activemq.artemis.api.core.client.ServerLocator; +import org.apache.activemq.artemis.core.config.Configuration; +import org.apache.activemq.artemis.core.config.impl.ConfigurationImpl; +import org.apache.activemq.artemis.core.server.ActiveMQServer; +import org.apache.activemq.artemis.core.server.JournalType; +import org.apache.activemq.artemis.core.server.impl.ActiveMQServerImpl; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; + +@State(Scope.Benchmark) +@Fork(1) +@Warmup(iterations = 3, time = 5) +@Measurement(iterations = 5, time = 10) +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +public class TransactionalMessagePerfTest { + + private static final int MESSAGE_SIZE = 100; + private static final SimpleString QUEUE_NAME = SimpleString.of("perfQueue"); + private static final SimpleString ADDRESS_NAME = SimpleString.of("perfAddress"); + + @Param({"NIO", "ASYNCIO", "ASYNCIO_2"}) + private String journalTypeParam; + + private ActiveMQServer server; + private ServerLocator locator; + private ClientSessionFactory sessionFactory; + private ClientSession producerSession; + private ClientSession consumerSession; + private ClientProducer producer; + private ClientConsumer consumer; + private File dataDir; + private byte[] messageBody; + + @Setup(Level.Trial) + public void setup() throws Exception { + dataDir = new File(System.getProperty("java.io.tmpdir"), "perf-test-" + System.currentTimeMillis()); + dataDir.mkdirs(); + + JournalType journalType = JournalType.getType(journalTypeParam); + + Configuration config = new ConfigurationImpl() + .setJournalDirectory(new File(dataDir, "journal").getAbsolutePath()) + .setBindingsDirectory(new File(dataDir, "bindings").getAbsolutePath()) + .setPagingDirectory(new File(dataDir, "paging").getAbsolutePath()) + .setLargeMessagesDirectory(new File(dataDir, "large-messages").getAbsolutePath()) + .setJournalType(journalType) + .setJournalSyncNonTransactional(false) + .setJournalSyncTransactional(true) + .setPersistenceEnabled(true) + .setSecurityEnabled(false) + .addAcceptorConfiguration("invm", "vm://0"); + + server = new ActiveMQServerImpl(config); + server.start(); + + locator = ActiveMQClient.createServerLocator("vm://0"); + sessionFactory = locator.createSessionFactory(); + + ClientSession setupSession = sessionFactory.createSession(false, false, false); + setupSession.createQueue(QueueConfiguration.of(QUEUE_NAME).setAddress(ADDRESS_NAME).setRoutingType(RoutingType.ANYCAST)); + setupSession.close(); + + producerSession = sessionFactory.createSession(false, false, false); + consumerSession = sessionFactory.createSession(false, true, true); + + producer = producerSession.createProducer(ADDRESS_NAME); + consumer = consumerSession.createConsumer(QUEUE_NAME); + consumerSession.start(); + + messageBody = new byte[MESSAGE_SIZE]; + } + + @Benchmark + public void sendAndConsume() throws Exception { + ClientMessage message = producerSession.createMessage(true); + message.getBodyBuffer().writeBytes(messageBody); + producer.send(message); + producerSession.commit(); + + ClientMessage receivedMessage = consumer.receive(5000); + if (receivedMessage != null) { + receivedMessage.acknowledge(); + } + } + + @TearDown(Level.Trial) + public void tearDown() throws Exception { + if (consumer != null) { + consumer.close(); + } + if (producer != null) { + producer.close(); + } + if (consumerSession != null) { + consumerSession.close(); + } + if (producerSession != null) { + producerSession.close(); + } + if (sessionFactory != null) { + sessionFactory.close(); + } + if (locator != null) { + locator.close(); + } + if (server != null) { + server.stop(); + } + if (dataDir != null && dataDir.exists()) { + deleteDirectory(dataDir); + } + } + + private void deleteDirectory(File directory) { + File[] files = directory.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + deleteDirectory(file); + } else { + file.delete(); + } + } + } + directory.delete(); + } +} diff --git a/tests/performance-jmh/src/main/java24/org/apache/activemq/artemis/tests/performance/jmh/FFMLibaioBenchmarkTest.java b/tests/performance-jmh/src/main/java24/org/apache/activemq/artemis/tests/performance/jmh/FFMLibaioBenchmarkTest.java new file mode 100644 index 00000000000..34c2c80bd5f --- /dev/null +++ b/tests/performance-jmh/src/main/java24/org/apache/activemq/artemis/tests/performance/jmh/FFMLibaioBenchmarkTest.java @@ -0,0 +1,225 @@ +/* + * 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.tests.performance.jmh; + +import org.apache.artemis.nativo.jlibaio.LibaioContext; +import org.apache.artemis.nativo.jlibaio.LibaioFile; +import org.apache.artemis.nativo.jlibaio.SubmitInfo; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; + +import java.io.File; +import java.lang.foreign.MemorySegment; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +@State(Scope.Benchmark) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Fork(value = 2) +@Warmup(iterations = 20, time = 200, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 10, time = 200, timeUnit = TimeUnit.MILLISECONDS) +public class FFMLibaioBenchmarkTest { + + private static final int FILE_SIZE = 10000 * 4096; + private static final int BLOCK_SIZE = 4096; + + @Param({"2048"}) + private int LIBAIO_QUEUE_SIZE; + + @Param({"10000"}) + private int recordCount; + + private File file; + private LibaioContext control; + private LibaioFile libaioFile; + + private MemorySegment headerSegment; + private ByteBuffer headerBuffer; + + private MemorySegment recordSegment; + private ByteBuffer recordBuffer; + + private final AtomicReference currentLatch = new AtomicReference<>(); + + private Thread pollThread; + private volatile boolean polling = true; + + private final SubmitInfo callback = new SubmitInfo() { + @Override + public void onError(int errno, String message) { + //ignore + } + + @Override + public void done() { + CountDownLatch latch = currentLatch.get(); + if (latch != null) { + latch.countDown(); + } + } + }; + + private long fileId = 1L; + + @Setup(Level.Trial) + public void setup() throws Exception { + file = File.createTempFile("aio-bench-", ".dat"); + + control = new LibaioContext<>(LIBAIO_QUEUE_SIZE, true, true); + libaioFile = control.openFile(file, true); + + //one-time file initialization + libaioFile.fallocate(FILE_SIZE); + + headerSegment = LibaioContext.newAlignedBuffer(BLOCK_SIZE, BLOCK_SIZE); + headerBuffer = headerSegment.asByteBuffer(); + + recordSegment = LibaioContext.newAlignedBuffer(BLOCK_SIZE, BLOCK_SIZE); + recordBuffer = recordSegment.asByteBuffer(); + + initRecord(headerBuffer); // filling the record clock with 1 + initRecord(recordBuffer); // filling the record clock with 1 + + fillHeader(fileId); + updateRecord(recordBuffer, fileId, 0L); + + polling = true; + pollThread = new Thread(() -> { + while (polling && !Thread.currentThread().isInterrupted()) { + try { + control.poll(); + } catch (Throwable e) { + if (polling) { + throw new RuntimeException(e); + } + break; + } + } + }, "aio-jmh-poll-thread"); + pollThread.setDaemon(true); + pollThread.start(); + + generateGarbage(); + } + + // Garbage generator fields + private Thread garbageThread; + private volatile boolean garbage = true; + + private void generateGarbage() { + garbage = true; + garbageThread = new Thread(() -> { + while (garbage && !Thread.currentThread().isInterrupted()) { + List list = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + Byte[] garbageArray = new Byte[8192]; + list.add(garbageArray); + } + list = null; + Thread.yield(); + } + }, "aio-jmh-garbage-thread"); + garbageThread.setDaemon(true); + garbageThread.start(); + } + + @TearDown(Level.Trial) + public void tearDown() throws Exception { + stopGarbageGenerator(); + + polling = false; + if (pollThread != null) { + pollThread.interrupt(); + pollThread.join(TimeUnit.SECONDS.toMillis(10)); + } + + if (libaioFile != null) { + libaioFile.close(); + } + if (control != null) { + control.close(); + } + if (headerSegment != null && headerSegment.address() != 0) { + LibaioContext.freeBuffer(headerSegment); + } + if (recordSegment != null && recordSegment.address() != 0) { + LibaioContext.freeBuffer(recordSegment); + } + if (file != null) { + file.delete(); + } + } + + private void stopGarbageGenerator() throws InterruptedException { + garbage = false; + if (garbageThread != null) { + garbageThread.interrupt(); + garbageThread.join(TimeUnit.SECONDS.toMillis(10)); + } + } + + @Benchmark + public void writeHeaderAndRecord() throws Exception { + CountDownLatch latch = new CountDownLatch(recordCount * 100); + currentLatch.set(latch); + + try { + for (int j = 0; j < 100; j++) { + for (int i = 0; i < recordCount; i++) { + updateRecord(recordBuffer, fileId, i); + long offset = BLOCK_SIZE + ((long) i * BLOCK_SIZE); + libaioFile.write(offset, BLOCK_SIZE, recordBuffer, callback); + } + } + + latch.await(); + } finally { + currentLatch.compareAndSet(latch, null); + } + } + + private void fillHeader(long fileId) { + headerBuffer.putLong(0, fileId); + } + + private void updateRecord(ByteBuffer buffer, long fileId, long recordId) { + buffer.putLong(0, fileId); + buffer.putLong(8, recordId); + } + + private void initRecord(ByteBuffer record) { + while (record.position() < BLOCK_SIZE) { + record.put((byte) 1); + } + } +} diff --git a/tests/unit-tests/pom.xml b/tests/unit-tests/pom.xml index 631efc6eb84..180590e4df8 100644 --- a/tests/unit-tests/pom.xml +++ b/tests/unit-tests/pom.xml @@ -196,6 +196,12 @@ jakarta.json-api test + + org.apache.artemis + artemis-ffm + ${project.version} + test + diff --git a/tests/unit-tests/src/test/java/org/apache/activemq/artemis/tests/unit/core/asyncio2/AIO2MultiThreadAsynchronousFileTest.java b/tests/unit-tests/src/test/java/org/apache/activemq/artemis/tests/unit/core/asyncio2/AIO2MultiThreadAsynchronousFileTest.java new file mode 100644 index 00000000000..d4ae31e2740 --- /dev/null +++ b/tests/unit-tests/src/test/java/org/apache/activemq/artemis/tests/unit/core/asyncio2/AIO2MultiThreadAsynchronousFileTest.java @@ -0,0 +1,260 @@ +/* + * 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.tests.unit.core.asyncio2; + +import java.lang.invoke.MethodHandles; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.activemq.artemis.core.io.IOCallback; +import org.apache.activemq.artemis.core.io.SequentialFile; +import org.apache.activemq.artemis.core.io.SequentialFileFactory; +import org.apache.activemq.artemis.core.io.aio2.AIO2Helper; +import org.apache.activemq.artemis.tests.util.ActiveMQTestBase; +import org.apache.activemq.artemis.utils.ActiveMQThreadFactory; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +/** + * You need to define {@literal -Djava.library.path=${project-root}/native/src/.libs} when calling the JVM + * If you are running this test in eclipse you should do: + *

    + *
  1. Run->Open Run Dialog + *
  2. Find the class on the list (you will find it if you already tried running this testcase before) + *
  3. Add {@literal -Djava.library.path=${project-root}/native/src/.libs} + *
+ */ +public class AIO2MultiThreadAsynchronousFileTest extends AIO2TestBase { + + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + AtomicInteger position = new AtomicInteger(0); + + static final int SIZE = 1024; + + static final int NUMBER_OF_THREADS = 1; + + static final int NUMBER_OF_LINES = 1000; + + ExecutorService executor; + + ExecutorService pollerExecutor; + + @BeforeAll + public static void hasAIO() { + assumeTrue(AIO2Helper.isSupported(), "AIO2 is not supported on this platform"); + } + + @Override + @BeforeEach + public void setUp() throws Exception { + super.setUp(); + pollerExecutor = Executors.newCachedThreadPool(new ActiveMQThreadFactory("ActiveMQ-FFM-AIO-poller-pool" + System.identityHashCode(this), false, this.getClass().getClassLoader())); + executor = Executors.newSingleThreadExecutor(ActiveMQThreadFactory.defaultThreadFactory(getClass().getName())); + } + + @Override + @AfterEach + public void tearDown() throws Exception { + executor.shutdown(); + pollerExecutor.shutdown(); + super.tearDown(); + } + + @Test + public void testMultipleASynchronousWrites() throws Throwable { + executeTest(false); + } + + @Test + public void testMultipleSynchronousWrites() throws Throwable { + executeTest(true); + } + + private void executeTest(final boolean sync) throws Throwable { + logger.debug(sync ? "Sync test:" : "Async test"); + + SequentialFileFactory factory = AIO2Helper.getAIO2SequentialFileFactory(getTestDirfile(), 100); + factory.start(); + factory.disableBufferReuse(); + + SequentialFile file = factory.createSequentialFile(fileName); + file.open(); + try { + logger.debug("Preallocating file"); + + file.fill(AIO2MultiThreadAsynchronousFileTest.NUMBER_OF_THREADS * AIO2MultiThreadAsynchronousFileTest.SIZE * AIO2MultiThreadAsynchronousFileTest.NUMBER_OF_LINES); + logger.debug("Done Preallocating file"); + + CountDownLatch latchStart = new CountDownLatch(AIO2MultiThreadAsynchronousFileTest.NUMBER_OF_THREADS + 1); + + List list = new ArrayList<>(AIO2MultiThreadAsynchronousFileTest.NUMBER_OF_THREADS); + for (int i = 0; i < AIO2MultiThreadAsynchronousFileTest.NUMBER_OF_THREADS; i++) { + ThreadProducer producer = new ThreadProducer("Thread " + i, latchStart, factory, file, sync); + list.add(producer); + producer.start(); + } + + latchStart.countDown(); + ActiveMQTestBase.waitForLatch(latchStart); + + long startTime = System.currentTimeMillis(); + + for (ThreadProducer producer : list) { + producer.join(); + if (producer.failed != null) { + throw producer.failed; + } + } + + long endTime = System.currentTimeMillis(); + long duration = endTime - startTime; + long numRecords = NUMBER_OF_THREADS * NUMBER_OF_LINES; + long rate = numRecords * 1000 / duration; + + logger.info("{} result: Records/Second = {} total time = {} total number of records = {}", (sync ? "Sync" : "Async"), rate, duration, numRecords); + } finally { + file.close(); + factory.stop(); + } + + } + + class ThreadProducer extends Thread { + + Throwable failed = null; + + CountDownLatch latchStart; + + boolean sync; + + SequentialFileFactory factory; + + SequentialFile libaio; + + ThreadProducer(final String name, + final CountDownLatch latchStart, + final SequentialFileFactory factory, + final SequentialFile libaio, + final boolean sync) { + super(name); + this.latchStart = latchStart; + this.factory = factory; + this.libaio = libaio; + this.sync = sync; + } + + @Override + public void run() { + super.run(); + + ByteBuffer buffer = null; + + buffer = factory.newNativeBuffer(AIO2MultiThreadAsynchronousFileTest.SIZE, 512); + + try { + + // I'm always reusing the same buffer, as I don't want any noise from + // malloc on the measurement + // Encoding buffer + AIO2MultiThreadAsynchronousFileTest.addString("Thread name=" + Thread.currentThread().getName() + ";" + "\n", buffer); + for (int local = buffer.position(); local < buffer.capacity() - 1; local++) { + buffer.put((byte) ' '); + } + buffer.put((byte) '\n'); + + latchStart.countDown(); + waitForLatch(latchStart); + + CountDownLatch latchFinishThread = null; + + if (!sync) { + latchFinishThread = new CountDownLatch(AIO2MultiThreadAsynchronousFileTest.NUMBER_OF_LINES); + } + + LinkedList list = new LinkedList<>(); + + for (int i = 0; i < AIO2MultiThreadAsynchronousFileTest.NUMBER_OF_LINES; i++) { + + if (sync) { + latchFinishThread = new CountDownLatch(1); + } + CountDownCallback callback = new CountDownCallback(latchFinishThread, null, null, 0); + if (!sync) { + list.add(callback); + } + addData(libaio, buffer, callback); + if (sync) { + waitForLatch(latchFinishThread); + assertEquals(0, callback.errorCalled); + assertTrue(callback.doneCalled); + } + } + if (!sync) { + waitForLatch(latchFinishThread); + } + + for (CountDownCallback callback : list) { + assertTrue(callback.doneCalled); + assertFalse(callback.errorCalled != 0); + } + + for (CountDownCallback callback : list) { + assertTrue(callback.doneCalled); + assertFalse(callback.errorCalled != 0); + } + + } catch (Throwable e) { + logger.warn(e.getMessage(), e); + failed = e; + } finally { + synchronized (AIO2MultiThreadAsynchronousFileTest.class) { + buffer.clear(); + factory.freeNativeBuffer(buffer); + } + } + + } + } + + private static void addString(final String str, final ByteBuffer buffer) { + byte[] bytes = str.getBytes(); + buffer.put(bytes); + } + + private void addData(final SequentialFile aio, + final ByteBuffer buffer, + final IOCallback callback) throws Exception { + aio.writeDirect(buffer, true, callback); + } + +} diff --git a/tests/unit-tests/src/test/java/org/apache/activemq/artemis/tests/unit/core/asyncio2/AIO2TestBase.java b/tests/unit-tests/src/test/java/org/apache/activemq/artemis/tests/unit/core/asyncio2/AIO2TestBase.java new file mode 100644 index 00000000000..ed4fdcb69d0 --- /dev/null +++ b/tests/unit-tests/src/test/java/org/apache/activemq/artemis/tests/unit/core/asyncio2/AIO2TestBase.java @@ -0,0 +1,143 @@ +/* + * 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.tests.unit.core.asyncio2; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.activemq.artemis.api.core.ActiveMQException; +import org.apache.activemq.artemis.core.io.IOCallback; +import org.apache.activemq.artemis.core.io.aio2.AIO2Helper; +import org.apache.artemis.nativo.jlibaio.LibaioFile; +import org.apache.activemq.artemis.tests.util.ActiveMQTestBase; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +/** + * The base class for AIO Tests + */ +public abstract class AIO2TestBase extends ActiveMQTestBase { + // The AIO Test must use a local filesystem. Sometimes $HOME is on a NFS on + // most enterprise systems + + protected String fileName = "fileUsedOnNativeTests.log"; + + @Override + @BeforeEach + public void setUp() throws Exception { + super.setUp(); + assumeTrue(AIO2Helper.isSupported(), String.format("libAIO is not loaded on %s %s %s", System.getProperty("os.name"), System.getProperty("os.arch"), System.getProperty("os.version"))); + } + + @Override + @AfterEach + public void tearDown() throws Exception { + assertEquals(0, AIO2Helper.getTotalMaxIO()); + + super.tearDown(); + } + + protected void encodeBufer(final ByteBuffer buffer) { + buffer.clear(); + int size = buffer.limit(); + for (int i = 0; i < size - 1; i++) { + buffer.put((byte) ('a' + i % 20)); + } + + buffer.put((byte) '\n'); + + } + + protected void preAlloc(final LibaioFile controller, final long size) throws ActiveMQException, IOException { + controller.fill(controller.getBlockSize(), size); + } + + protected static class CountDownCallback implements IOCallback { + + private final CountDownLatch latch; + + private final List outputList; + + private final int order; + + private final AtomicInteger errors; + + public CountDownCallback(final CountDownLatch latch, + final AtomicInteger errors, + final List outputList, + final int order) { + this.latch = latch; + + this.outputList = outputList; + + this.order = order; + + this.errors = errors; + } + + volatile boolean doneCalled = false; + + int errorCalled = 0; + + final AtomicInteger timesDoneCalled = new AtomicInteger(0); + + @Override + public void done() { + if (outputList != null) { + outputList.add(order); + } + doneCalled = true; + timesDoneCalled.incrementAndGet(); + if (latch != null) { + latch.countDown(); + } + } + + @Override + public void onError(final int errorCode, final String errorMessage) { + new Exception("Error called:: " + errorCode + " message::" + errorMessage).printStackTrace(); + errorCalled++; + if (outputList != null) { + outputList.add(order); + } + if (errors != null) { + errors.incrementAndGet(); + } + if (latch != null) { + // even thought an error happened, we need to inform the latch, + // or the test won't finish + latch.countDown(); + } + } + + public static void checkResults(final int numberOfElements, final ArrayList result) { + assertEquals(numberOfElements, result.size()); + int i = 0; + for (Integer resultI : result) { + assertEquals(i++, resultI.intValue()); + } + } + } + +}