/* * 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.aio; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.activemq.artemis.ArtemisConstants; 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.jlibaio.LibaioContext; import org.apache.activemq.artemis.jlibaio.LibaioFile; import org.apache.activemq.artemis.jlibaio.SubmitInfo; import org.apache.activemq.artemis.jlibaio.util.CallbackCache; import org.apache.activemq.artemis.journal.ActiveMQJournalLogger; import org.jboss.logging.Logger; public final class AIOSequentialFileFactory extends AbstractSequentialFileFactory { private static final Logger logger = Logger.getLogger(AIOSequentialFileFactory.class); private final ReuseBuffersController buffersControl = new ReuseBuffersController(); private volatile boolean reuseBuffers = true; private Thread pollerThread; volatile LibaioContext<AIOSequentialCallback> libaioContext; private final CallbackCache<AIOSequentialCallback> callbackPool; private final AtomicBoolean running = new AtomicBoolean(false); private static final String AIO_TEST_FILE = ".aio-test"; public AIOSequentialFileFactory(final File journalDir, int maxIO) { this(journalDir, ArtemisConstants.DEFAULT_JOURNAL_BUFFER_SIZE_AIO, ArtemisConstants.DEFAULT_JOURNAL_BUFFER_TIMEOUT_AIO, maxIO, false, null); } public AIOSequentialFileFactory(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); } public AIOSequentialFileFactory(final File journalDir, final int bufferSize, final int bufferTimeout, final int maxIO, final boolean logRates) { this(journalDir, bufferSize, bufferTimeout, maxIO, logRates, null); } public AIOSequentialFileFactory(final File journalDir, final int bufferSize, final int bufferTimeout, final int maxIO, final boolean logRates, final IOCriticalErrorListener listener) { super(journalDir, true, bufferSize, bufferTimeout, maxIO, logRates, listener); callbackPool = new CallbackCache<>(maxIO); } public AIOSequentialCallback getCallback() { AIOSequentialCallback callback = callbackPool.get(); if (callback == null) { callback = new AIOSequentialCallback(); } return callback; } public void enableBufferReuse() { this.reuseBuffers = true; } public void disableBufferReuse() { this.reuseBuffers = false; } @Override public SequentialFile createSequentialFile(final String fileName) { return new AIOSequentialFile(this, bufferSize, bufferTimeout, journalDir, fileName, writeExecutor); } @Override public boolean isSupportsCallbacks() { return true; } public static boolean isSupported() { return 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) { int blocks = size / getAlignment(); if (size % getAlignment() != 0) { blocks++; } // The buffer on AIO has to be a multiple of getAlignment() ByteBuffer buffer = LibaioContext.newAlignedBuffer(blocks * getAlignment(), getAlignment()); buffer.limit(size); return buffer; } @Override public void releaseDirectBuffer(final ByteBuffer buffer) { LibaioContext.freeBuffer(buffer); } @Override public ByteBuffer newBuffer(int size) { if (size % getAlignment() != 0) { size = (size / getAlignment() + 1) * getAlignment(); } return buffersControl.newBuffer(size); } @Override public void clearBuffer(final ByteBuffer directByteBuffer) { directByteBuffer.position(0); libaioContext.memsetBuffer(directByteBuffer); } @Override public int getAlignment() { if (alignment < 0) { File checkFile = null; 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) { int alignment = getAlignment(); int pos = (position / alignment + (position % alignment != 0 ? 1 : 0)) * alignment; return pos; } /* (non-Javadoc) * @see org.apache.activemq.artemis.core.io.SequentialFileFactory#releaseBuffer(java.nio.ByteBuffer) */ @Override public synchronized void releaseBuffer(final ByteBuffer buffer) { LibaioContext.freeBuffer(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 AIOSequentialCallback implements SubmitInfo, Runnable, Comparable<AIOSequentialCallback> { IOCallback callback; boolean error = false; AIOSequentialFile sequentialFile; ByteBuffer buffer; LibaioFile libaioFile; String errorMessage; int errorCode = -1; long writeSequence; long position; int bytes; @Override public String toString() { return "AIOSequentialCallback{" + "error=" + error + ", errorMessage='" + errorMessage + '\'' + ", errorCode=" + errorCode + ", writeSequence=" + writeSequence + ", position=" + position + '}'; } public AIOSequentialCallback 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(-1, e.getMessage()); } } @Override public int compareTo(AIOSequentialCallback other) { if (this == other || this.writeSequence == other.writeSequence) { return 0; } else if (other.writeSequence < this.writeSequence) { return 1; } else { return -1; } } public AIOSequentialCallback init(long writeSequence, IOCallback IOCallback, LibaioFile libaioFile, AIOSequentialFile sequentialFile, ByteBuffer usedBuffer) { this.callback = IOCallback; this.sequentialFile = sequentialFile; this.error = false; this.buffer = usedBuffer; this.libaioFile = libaioFile; this.writeSequence = writeSequence; this.errorMessage = null; return this; } @Override public void onError(int errno, String 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) { callback.onError(errorCode, errorMessage); errorMessage = null; } else { if (callback != null) { callback.done(); } if (buffer != null && reuseBuffers) { buffersControl.bufferDone(buffer); } callbackPool.put(AIOSequentialCallback.this); } } } private class PollerThread extends Thread { private PollerThread() { super("Apache ActiveMQ Artemis libaio poller"); } @Override public void run() { while (running.get()) { try { libaioContext.poll(); } catch (Throwable e) { ActiveMQJournalLogger.LOGGER.warn(e.getMessage(), e); } } } } /** * Class that will control buffer-reuse */ private class ReuseBuffersController { private volatile long bufferReuseLastTime = System.currentTimeMillis(); private final ConcurrentLinkedQueue<ByteBuffer> reuseBuffersQueue = new ConcurrentLinkedQueue<>(); private boolean stopped = false; public ByteBuffer newBuffer(final int size) { // 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 " + reuseBuffersQueue.size() + " elements"); } bufferReuseLastTime = System.currentTimeMillis(); clearPoll(); } // if a buffer is bigger than the configured-bufferSize, we just create a new // buffer. if (size > bufferSize) { return LibaioContext.newAlignedBuffer(size, getAlignment()); } else { // We need to allocate buffers following the rules of the storage // being used (AIO/NIO) int 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(size, getAlignment()); buffer.limit(alignedSize); } else { clearBuffer(buffer); // set the limit of the buffer to the bufferSize being required buffer.limit(alignedSize); } 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() == bufferSize) { reuseBuffersQueue.offer(buffer); } else { releaseBuffer(buffer); } } } } } @Override public String toString() { return AIOSequentialFileFactory.class.getSimpleName() + "(buffersControl.stopped=" + buffersControl.stopped + "):" + super.toString(); } }