configuration,
useEpoll = ConfigurationHelper.getBooleanProperty(TransportConstants.USE_EPOLL_PROP_NAME, TransportConstants.DEFAULT_USE_EPOLL, configuration);
useKQueue = ConfigurationHelper.getBooleanProperty(TransportConstants.USE_KQUEUE_PROP_NAME, TransportConstants.DEFAULT_USE_KQUEUE, configuration);
+ useIoUring = ConfigurationHelper.getBooleanProperty(TransportConstants.USE_IOURING_PROP_NAME, TransportConstants.DEFAULT_USE_IOURING, configuration);
useServlet = ConfigurationHelper.getBooleanProperty(TransportConstants.USE_SERVLET_PROP_NAME, TransportConstants.DEFAULT_USE_SERVLET, configuration);
host = ConfigurationHelper.getStringProperty(TransportConstants.HOST_PROP_NAME, TransportConstants.DEFAULT_HOST, configuration);
@@ -551,27 +557,43 @@ public synchronized void start() {
return;
}
- if (remotingThreads == -1) {
+ boolean defaultRemotingThreads = remotingThreads == -1;
+
+ if (defaultRemotingThreads) {
// Default to number of cores * 3
remotingThreads = Runtime.getRuntime().availableProcessors() * 3;
}
String connectorType;
- if (useEpoll && CheckDependencies.isEpollAvailable()) {
+ if (useIoUring && CheckDependencies.isIoUringAvailable()) {
+ //IO_URING should default to 1 remotingThread unless specified in config
+ remotingThreads = defaultRemotingThreads ? 1 : remotingThreads;
+
+ if (useGlobalWorkerPool) {
+ group = SharedEventLoopGroup.getInstance((threadFactory -> new MultiThreadIoEventLoopGroup(remotingThreads, threadFactory, NettyIoUringSupport.newHandlerFactory())));
+ } else {
+ group = new MultiThreadIoEventLoopGroup(remotingThreads, NettyIoUringSupport.newHandlerFactory());
+ }
+
+ connectorType = IOURING_CONNECTOR_TYPE;
+ channelClazz = NettyIoUringSupport.socketChannelClass();
+
+ logger.debug("Connector {} using native io_uring", this);
+ } else if (useEpoll && CheckDependencies.isEpollAvailable()) {
if (useGlobalWorkerPool) {
- group = SharedEventLoopGroup.getInstance((threadFactory -> new EpollEventLoopGroup(remotingThreads, threadFactory)));
+ group = SharedEventLoopGroup.getInstance((threadFactory -> new MultiThreadIoEventLoopGroup(remotingThreads, threadFactory, EpollIoHandler.newFactory())));
} else {
- group = new EpollEventLoopGroup(remotingThreads);
+ group = new MultiThreadIoEventLoopGroup(remotingThreads, EpollIoHandler.newFactory());
}
connectorType = EPOLL_CONNECTOR_TYPE;
channelClazz = EpollSocketChannel.class;
logger.debug("Connector {} using native epoll", this);
} else if (useKQueue && CheckDependencies.isKQueueAvailable()) {
if (useGlobalWorkerPool) {
- group = SharedEventLoopGroup.getInstance((threadFactory -> new KQueueEventLoopGroup(remotingThreads, threadFactory)));
+ group = SharedEventLoopGroup.getInstance((threadFactory -> new MultiThreadIoEventLoopGroup(remotingThreads, threadFactory, KQueueIoHandler.newFactory())));
} else {
- group = new KQueueEventLoopGroup(remotingThreads);
+ group = new MultiThreadIoEventLoopGroup(remotingThreads, KQueueIoHandler.newFactory());
}
connectorType = KQUEUE_CONNECTOR_TYPE;
channelClazz = KQueueSocketChannel.class;
@@ -579,10 +601,10 @@ public synchronized void start() {
} else {
if (useGlobalWorkerPool) {
channelClazz = NioSocketChannel.class;
- group = SharedEventLoopGroup.getInstance((threadFactory -> new NioEventLoopGroup(remotingThreads, threadFactory)));
+ group = SharedEventLoopGroup.getInstance((threadFactory -> new MultiThreadIoEventLoopGroup(remotingThreads, threadFactory, NioIoHandler.newFactory())));
} else {
channelClazz = NioSocketChannel.class;
- group = new NioEventLoopGroup(remotingThreads);
+ group = new MultiThreadIoEventLoopGroup(remotingThreads, NioIoHandler.newFactory());
}
connectorType = NIO_CONNECTOR_TYPE;
channelClazz = NioSocketChannel.class;
@@ -756,14 +778,14 @@ public void initChannel(Channel channel) throws Exception {
engine.setEnabledProtocols(originalProtocols);
}
- if (verifyHost) {
- SSLParameters sslParameters = engine.getSSLParameters();
- sslParameters.setEndpointIdentificationAlgorithm("HTTPS");
- engine.setSSLParameters(sslParameters);
- }
+ // Set the endpoint identification algorithm explicitly (rather than only when enabling host
+ // verification) so the behavior doesn't depend on the SSL provider's default.
+ SSLParameters sslParameters = engine.getSSLParameters();
+ sslParameters.setEndpointIdentificationAlgorithm(verifyHost ? "HTTPS" : null);
+ engine.setSSLParameters(sslParameters);
if (sniHost != null) {
- SSLParameters sslParameters = engine.getSSLParameters();
+ sslParameters = engine.getSSLParameters();
sslParameters.setServerNames(Arrays.asList(new SNIHostName(sniHost)));
engine.setSSLParameters(sslParameters);
}
diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/remoting/impl/netty/TransportConstants.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/remoting/impl/netty/TransportConstants.java
index 1876b723a4c7..7f9d804a94d2 100644
--- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/remoting/impl/netty/TransportConstants.java
+++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/remoting/impl/netty/TransportConstants.java
@@ -74,6 +74,8 @@ public class TransportConstants {
public static final String USE_KQUEUE_PROP_NAME = "useKQueue";
+ public static final String USE_IOURING_PROP_NAME = "useIoUring";
+
/**
* @deprecated Use USE_GLOBAL_WORKER_POOL_PROP_NAME
*/
@@ -228,6 +230,8 @@ public class TransportConstants {
public static final boolean DEFAULT_USE_KQUEUE = true;
+ public static final boolean DEFAULT_USE_IOURING = false;
+
public static final boolean DEFAULT_USE_INVM = false;
public static final boolean DEFAULT_USE_SERVLET = false;
@@ -443,6 +447,7 @@ private static int parseDefaultVariable(String variableName, int defaultValue) {
allowableAcceptorKeys.add(TransportConstants.USE_NIO_PROP_NAME);
allowableAcceptorKeys.add(TransportConstants.USE_EPOLL_PROP_NAME);
allowableAcceptorKeys.add(TransportConstants.USE_KQUEUE_PROP_NAME);
+ allowableAcceptorKeys.add(TransportConstants.USE_IOURING_PROP_NAME);
allowableAcceptorKeys.add(TransportConstants.USE_INVM_PROP_NAME);
//noinspection deprecation
allowableAcceptorKeys.add(TransportConstants.PROTOCOL_PROP_NAME);
@@ -523,6 +528,7 @@ private static int parseDefaultVariable(String variableName, int defaultValue) {
allowableConnectorKeys.add(TransportConstants.USE_NIO_GLOBAL_WORKER_POOL_PROP_NAME);
allowableConnectorKeys.add(TransportConstants.USE_EPOLL_PROP_NAME);
allowableConnectorKeys.add(TransportConstants.USE_KQUEUE_PROP_NAME);
+ allowableConnectorKeys.add(TransportConstants.USE_IOURING_PROP_NAME);
allowableConnectorKeys.add(TransportConstants.USE_GLOBAL_WORKER_POOL_PROP_NAME);
allowableConnectorKeys.add(TransportConstants.HOST_PROP_NAME);
allowableConnectorKeys.add(TransportConstants.PORT_PROP_NAME);
diff --git a/artemis-distribution/src/main/resources/bin/artemis b/artemis-distribution/src/main/resources/bin/artemis
index 8f44f9589313..9381da38613b 100755
--- a/artemis-distribution/src/main/resources/bin/artemis
+++ b/artemis-distribution/src/main/resources/bin/artemis
@@ -94,6 +94,7 @@ exec "$JAVACMD" $JAVA_ARGS $ARTEMIS_CLUSTER_PROPS \
-classpath "$CLASSPATH" \
-Dartemis.home="$ARTEMIS_HOME" \
-Djava.library.path="$ARTEMIS_HOME/bin/lib/linux-$(uname -m)" \
+ --enable-native-access=ALL-UNNAMED \
$DEBUG_ARGS \
$JAVA_ARGS_APPEND \
org.apache.activemq.artemis.boot.Artemis "$@"
diff --git a/artemis-distribution/src/main/resources/bin/artemis.cmd b/artemis-distribution/src/main/resources/bin/artemis.cmd
index 273bf851e290..47a2097937d7 100755
--- a/artemis-distribution/src/main/resources/bin/artemis.cmd
+++ b/artemis-distribution/src/main/resources/bin/artemis.cmd
@@ -52,6 +52,7 @@ set JVM_ARGS=%JAVA_ARGS%
if not "%ARTEMIS_CLUSTER_PROPS%"=="" set JVM_ARGS=%JVM_ARGS% %ARTEMIS_CLUSTER_PROPS%
set JVM_ARGS=%JVM_ARGS% -classpath %ARTEMIS_HOME%\lib\artemis-boot.jar
set JVM_ARGS=%JVM_ARGS% -Dartemis.home=%ARTEMIS_HOME%
+set JVM_ARGS=%JVM_ARGS% --enable-native-access=ALL-UNNAMED
if not "%DEBUG_ARGS%"=="" set JVM_ARGS=%JVM_ARGS% %DEBUG_ARGS%
if not "%JAVA_ARGS_APPEND%"=="" set JVM_ARGS=%JVM_ARGS% %JAVA_ARGS_APPEND%
diff --git a/artemis-features/src/main/resources/features.xml b/artemis-features/src/main/resources/features.xml
index c00193f8e640..6a9cf4dbbd67 100644
--- a/artemis-features/src/main/resources/features.xml
+++ b/artemis-features/src/main/resources/features.xml
@@ -33,7 +33,8 @@
mvn:io.netty/netty-resolver/${netty.version}
mvn:io.netty/netty-transport/${netty.version}
mvn:io.netty/netty-buffer/${netty.version}
- mvn:io.netty/netty-codec/${netty.version}
+ mvn:io.netty/netty-codec-base/${netty.version}
+ mvn:io.netty/netty-codec-compression/${netty.version}
mvn:io.netty/netty-codec-socks/${netty.version}
mvn:io.netty/netty-codec-haproxy/${netty.version}
mvn:io.netty/netty-codec-http/${netty.version}
@@ -44,6 +45,8 @@
mvn:io.netty/netty-transport-native-epoll/${netty.version}
mvn:io.netty/netty-transport-classes-kqueue/${netty.version}
mvn:io.netty/netty-transport-native-kqueue/${netty.version}
+ mvn:io.netty/netty-transport-classes-io_uring/${netty.version}
+ mvn:io.netty/netty-transport-native-io_uring/${netty.version}
mvn:io.netty/netty-transport-native-unix-common/${netty.version}
diff --git a/artemis-jms-client-osgi/pom.xml b/artemis-jms-client-osgi/pom.xml
index 130dbe62a8f6..cee6ee6e4432 100644
--- a/artemis-jms-client-osgi/pom.xml
+++ b/artemis-jms-client-osgi/pom.xml
@@ -79,7 +79,7 @@
org.glassfish.json*;resolution:=optional,
de.dentrassi.crypto.pem;resolution:=optional,
- io.netty.buffer;io.netty.*;version="[4.1,5)",
+ io.netty.*;version="[4.2,5)",
*
<_exportcontents>org.apache.activemq.artemis.*;-noimport:=true
diff --git a/artemis-journal/src/main/java/org/apache/activemq/artemis/core/io/mapped/MappedFile.java b/artemis-journal/src/main/java/org/apache/activemq/artemis/core/io/mapped/MappedFile.java
index f8159d1ee790..942527037053 100644
--- a/artemis-journal/src/main/java/org/apache/activemq/artemis/core/io/mapped/MappedFile.java
+++ b/artemis-journal/src/main/java/org/apache/activemq/artemis/core/io/mapped/MappedFile.java
@@ -27,6 +27,7 @@
import io.netty.buffer.Unpooled;
import io.netty.util.internal.PlatformDependent;
import org.apache.activemq.artemis.core.buffers.impl.ChannelBufferWrapper;
+import org.apache.activemq.artemis.core.io.util.DirectByteBufferReleaser;
import org.apache.activemq.artemis.core.journal.EncodingSupport;
import org.apache.activemq.artemis.utils.PowerOf2Util;
import org.apache.activemq.artemis.utils.Env;
@@ -241,8 +242,8 @@ public void close() {
} catch (IOException e) {
throw new IllegalStateException(e);
} finally {
- //unmap in a deterministic way: do not rely on GC to do it
- PlatformDependent.freeDirectBuffer(this.buffer);
+ //unmap in a deterministic way when possible: falls back to GC-triggered cleanup if native freeing is unavailable
+ DirectByteBufferReleaser.freeDirectBuffer(this.buffer);
}
}
}
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 8f144598c042..91cc1e1b17dc 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
@@ -19,11 +19,11 @@
import java.io.File;
import java.nio.ByteBuffer;
-import io.netty.util.internal.PlatformDependent;
import org.apache.activemq.artemis.core.io.AbstractSequentialFileFactory;
import org.apache.activemq.artemis.core.io.IOCriticalErrorListener;
import org.apache.activemq.artemis.core.io.SequentialFile;
import org.apache.activemq.artemis.core.io.util.ByteBufferPool;
+import org.apache.activemq.artemis.core.io.util.DirectByteBufferReleaser;
import org.apache.activemq.artemis.utils.PowerOf2Util;
import org.apache.activemq.artemis.utils.ByteUtil;
import org.apache.activemq.artemis.utils.Env;
@@ -90,7 +90,7 @@ public ByteBuffer allocateDirectBuffer(final int size) {
@Override
public void releaseDirectBuffer(ByteBuffer buffer) {
- PlatformDependent.freeDirectBuffer(buffer);
+ DirectByteBufferReleaser.freeDirectBuffer(buffer);
}
public MappedSequentialFileFactory enableBufferReuse() {
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 ead344b8f037..3c53a5993e0a 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
@@ -27,6 +27,7 @@
import org.apache.activemq.artemis.core.io.IOCriticalErrorListener;
import org.apache.activemq.artemis.core.io.SequentialFile;
import org.apache.activemq.artemis.core.io.util.ByteBufferPool;
+import org.apache.activemq.artemis.core.io.util.DirectByteBufferReleaser;
import org.apache.activemq.artemis.utils.PowerOf2Util;
import org.apache.activemq.artemis.utils.Env;
import org.apache.activemq.artemis.utils.critical.CriticalAnalyzer;
@@ -134,9 +135,7 @@ public ByteBuffer allocateDirectBuffer(final int size) {
@Override
public void releaseDirectBuffer(ByteBuffer buffer) {
- if (buffer.isDirect()) {
- PlatformDependent.freeDirectBuffer(buffer);
- }
+ DirectByteBufferReleaser.freeDirectBuffer(buffer);
}
@Override
@@ -165,7 +164,17 @@ public void releaseBuffer(ByteBuffer buffer) {
@Override
public void clearBuffer(final ByteBuffer buffer) {
if (buffer.isDirect()) {
- PlatformDependent.setMemory(PlatformDependent.directBufferAddress(buffer), buffer.limit(), (byte) 0);
+ if (PlatformDependent.hasUnsafe()) {
+ PlatformDependent.setMemory(PlatformDependent.directBufferAddress(buffer), buffer.limit(), (byte) 0);
+ } else {
+ final int position = buffer.position();
+ final byte[] zeros = new byte[Math.min(buffer.limit(), 8192)];
+ buffer.position(0);
+ while (buffer.hasRemaining()) {
+ buffer.put(zeros, 0, Math.min(zeros.length, buffer.remaining()));
+ }
+ buffer.position(position);
+ }
} else {
Arrays.fill(buffer.array(), buffer.arrayOffset(), buffer.limit(), (byte) 0);
}
diff --git a/artemis-journal/src/main/java/org/apache/activemq/artemis/core/io/util/DirectByteBufferReleaser.java b/artemis-journal/src/main/java/org/apache/activemq/artemis/core/io/util/DirectByteBufferReleaser.java
new file mode 100644
index 000000000000..a639281c274a
--- /dev/null
+++ b/artemis-journal/src/main/java/org/apache/activemq/artemis/core/io/util/DirectByteBufferReleaser.java
@@ -0,0 +1,82 @@
+/*
+ * 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.util;
+
+import java.lang.invoke.MethodHandles;
+import java.nio.ByteBuffer;
+
+import io.netty.util.internal.PlatformDependent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Best-effort helper to eagerly free direct {@link ByteBuffer}s that were allocated through the JDK, e.g. via
+ * {@link ByteBuffer#allocateDirect(int)} or {@link java.nio.channels.FileChannel#map}.
+ *
+ * Freeing such a buffer eagerly is only an optimization to release native memory promptly rather than waiting for the
+ * buffer's {@link java.lang.ref.Cleaner} to run during GC. On JDK 24+ running without {@code sun.misc.Unsafe}, Netty is
+ * unable to free "arbitrary" (JDK-allocated) direct buffers and {@link PlatformDependent#freeDirectBuffer(ByteBuffer)}
+ * throws {@link UnsupportedOperationException}. In that case the eager free is skipped and the GC-triggered
+ * {@link java.lang.ref.Cleaner} associated with the buffer reclaims the native memory instead.
+ */
+public final class DirectByteBufferReleaser {
+
+ private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private static final boolean CAN_FREE_DIRECT_BUFFER = probeCanFreeDirectBuffer();
+
+ private DirectByteBufferReleaser() {
+ }
+
+ /**
+ * Eagerly frees the given direct {@code buffer} if the platform supports it. Does nothing for {@code null},
+ * non-direct buffers, or when the platform is unable to free JDK-allocated direct buffers (in which case the memory
+ * is reclaimed by the GC-triggered {@link java.lang.ref.Cleaner} associated with the buffer).
+ *
+ * {@link PlatformDependent#freeDirectBuffer} is deprecated in favor of
+ * {@link io.netty.util.internal.CleanableDirectBuffer#clean()}, but that replacement can only free buffers Netty
+ * itself allocated via {@link PlatformDependent#allocateDirect(int)}. Artemis passes buffers it allocated through
+ * the JDK ({@link ByteBuffer#allocateDirect} / {@link java.nio.channels.FileChannel#map}), so freeDirectBuffer
+ * remains the only Netty API able to free them; hence the suppression.
+ */
+ @SuppressWarnings("deprecation")
+ public static void freeDirectBuffer(ByteBuffer buffer) {
+ if (CAN_FREE_DIRECT_BUFFER && buffer != null && buffer.isDirect()) {
+ PlatformDependent.freeDirectBuffer(buffer);
+ }
+ }
+
+ /**
+ * @return whether native (eager) freeing of direct buffers is available on the current platform
+ */
+ public static boolean canFreeDirectBuffer() {
+ return CAN_FREE_DIRECT_BUFFER;
+ }
+
+ @SuppressWarnings("deprecation")
+ private static boolean probeCanFreeDirectBuffer() {
+ // Probe with a JDK-allocated direct buffer, matching how Artemis allocates the buffers passed to this class.
+ final ByteBuffer probe = ByteBuffer.allocateDirect(1);
+ try {
+ PlatformDependent.freeDirectBuffer(probe);
+ return true;
+ } catch (Throwable t) {
+ logger.debug("Unable to eagerly free direct ByteBuffers; native memory will be reclaimed by the GC-triggered Cleaner instead. On JDK 24+ enabling sun.misc.Unsafe (e.g. --sun-misc-unsafe-memory-access=allow) restores eager freeing.", t);
+ return false;
+ }
+ }
+}
diff --git a/artemis-journal/src/main/java/org/apache/activemq/artemis/core/io/util/ThreadLocalByteBufferPool.java b/artemis-journal/src/main/java/org/apache/activemq/artemis/core/io/util/ThreadLocalByteBufferPool.java
index ff3b1011b0c8..23bd32aec3bf 100644
--- a/artemis-journal/src/main/java/org/apache/activemq/artemis/core/io/util/ThreadLocalByteBufferPool.java
+++ b/artemis-journal/src/main/java/org/apache/activemq/artemis/core/io/util/ThreadLocalByteBufferPool.java
@@ -19,7 +19,6 @@
import java.nio.ByteBuffer;
import java.util.Objects;
-import io.netty.util.internal.PlatformDependent;
import org.apache.activemq.artemis.utils.PowerOf2Util;
import org.apache.activemq.artemis.utils.ByteUtil;
import org.apache.activemq.artemis.utils.Env;
@@ -56,23 +55,18 @@ public ByteBuffer borrow(final int size, boolean zeroed) {
@Override
public void release(ByteBuffer buffer) {
Objects.requireNonNull(buffer);
- boolean directBuffer = buffer.isDirect();
- if (directBuffer == direct && !buffer.isReadOnly()) {
+ if (buffer.isDirect() == direct && !buffer.isReadOnly()) {
final ByteBuffer byteBuffer = bytesPool.get();
if (byteBuffer != buffer) {
//replace with the current pooled only if greater or null
if (byteBuffer == null || buffer.capacity() > byteBuffer.capacity()) {
if (byteBuffer != null) {
//free the smaller one
- if (directBuffer) {
- PlatformDependent.freeDirectBuffer(byteBuffer);
- }
+ DirectByteBufferReleaser.freeDirectBuffer(byteBuffer);
}
bytesPool.set(buffer);
} else {
- if (directBuffer) {
- PlatformDependent.freeDirectBuffer(buffer);
- }
+ DirectByteBufferReleaser.freeDirectBuffer(buffer);
}
}
}
diff --git a/artemis-journal/src/test/java/org/apache/activemq/artemis/core/io/util/DirectByteBufferReleaserTest.java b/artemis-journal/src/test/java/org/apache/activemq/artemis/core/io/util/DirectByteBufferReleaserTest.java
new file mode 100644
index 000000000000..96d56cddf960
--- /dev/null
+++ b/artemis-journal/src/test/java/org/apache/activemq/artemis/core/io/util/DirectByteBufferReleaserTest.java
@@ -0,0 +1,86 @@
+/*
+ * 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.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.activemq.artemis.core.io.SequentialFile;
+import org.apache.activemq.artemis.core.io.nio.NIOSequentialFileFactory;
+import org.apache.activemq.artemis.utils.SpawnedVMSupport;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Regression test for the NIO journal direct-buffer release path when {@code sun.misc.Unsafe} is unavailable. In that
+ * case Netty cannot free JDK-allocated ("arbitrary") direct buffers, so the eager free must be skipped (letting the
+ * GC-triggered {@link java.lang.ref.Cleaner} reclaim the memory) rather than throwing
+ * {@link UnsupportedOperationException}.
+ *
+ * On JDK 24+ at runtime by default Netty 4.2 avoids {@code sun.misc.Unsafe} (so it runs with {@code hasUnsafe=false})
+ * unless the JVM is started with {@code --sun-misc-unsafe-memory-access=allow}. The same no-Unsafe path is also reached
+ * when Unsafe is explicitly disabled ({@code -Dio.netty.noUnsafe=true}) or on a future JDK where
+ * {@code sun.misc.Unsafe} is gone entirely.
+ */
+public class DirectByteBufferReleaserTest {
+
+ // runs in the spawned child JVM (started with -Dio.netty.noUnsafe=true to force Netty's no-Unsafe path)
+ public static void main(String[] arg) {
+ try {
+ // exercise the exact path that failed during Create auto-tune: SyncCalculation -> NIOSequentialFile.fill
+ File dir = Files.createTempDirectory("DirectByteBufferReleaserTest").toFile();
+ dir.deleteOnExit();
+ NIOSequentialFileFactory factory = new NIOSequentialFileFactory(dir, 1);
+ factory.start();
+ SequentialFile file = factory.createSequentialFile("release.dat");
+ file.open();
+ // allocates a direct ByteBuffer and then releases it; the release must not throw without Unsafe
+ file.fill(1024 * 1024);
+ file.close();
+ // force a real release of a direct buffer through the factory (bypassing the pool)
+ factory.releaseDirectBuffer(factory.allocateDirectBuffer(1024));
+ factory.stop();
+ System.exit(0);
+ } catch (Throwable e) {
+ e.printStackTrace();
+ System.exit(100);
+ }
+ }
+
+ @Test
+ public void releaseDirectBufferWithoutUnsafe() throws Exception {
+ final String javaPath = new File(new File(System.getProperty("java.home"), "bin"), "java").getAbsolutePath();
+ final List command = new ArrayList<>();
+ command.add(javaPath);
+ command.add("-cp");
+ command.add(SpawnedVMSupport.getClassPath());
+ // force Netty's no-Unsafe path (hasUnsafe=false); in that configuration Netty cannot free the journal's
+ // JDK-allocated ("arbitrary") direct buffers and PlatformDependent.freeDirectBuffer throws, which is exactly
+ // the case DirectByteBufferReleaser must catch and skip
+ command.add("-Dio.netty.noUnsafe=true");
+ command.add("-Djava.io.tmpdir=" + System.getProperty("java.io.tmpdir", "./tmp"));
+ command.add(DirectByteBufferReleaserTest.class.getName());
+
+ final ProcessBuilder builder = new ProcessBuilder(command);
+ builder.inheritIO();
+ final Process process = builder.start();
+ assertEquals(0, process.waitFor(), "releasing a direct buffer without Unsafe/native-access must not throw");
+ }
+}
diff --git a/artemis-pom/pom.xml b/artemis-pom/pom.xml
index cf5183713ad3..ac8e66d90161 100644
--- a/artemis-pom/pom.xml
+++ b/artemis-pom/pom.xml
@@ -458,6 +458,12 @@
${netty.version}