/* * 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.buffer; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.Semaphore; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.LockSupport; import io.netty.buffer.Unpooled; import org.apache.activemq.artemis.api.core.ActiveMQBuffer; import org.apache.activemq.artemis.api.core.ActiveMQInterruptedException; import org.apache.activemq.artemis.core.buffers.impl.ChannelBufferWrapper; import org.apache.activemq.artemis.core.io.IOCallback; import org.apache.activemq.artemis.core.journal.EncodingSupport; import org.apache.activemq.artemis.journal.ActiveMQJournalLogger; public final class TimedBuffer { // Constants ----------------------------------------------------- // Attributes ---------------------------------------------------- private TimedBufferObserver bufferObserver; // If the TimedBuffer is idle - i.e. no records are being added, then it's pointless the timer flush thread // in spinning and checking the time - and using up CPU in the process - this semaphore is used to // prevent that private final Semaphore spinLimiter = new Semaphore(1); private CheckTimer timerRunnable = new CheckTimer(); private final int bufferSize; private final ActiveMQBuffer buffer; private int bufferLimit = 0; private List<IOCallback> callbacks; private final int timeout; private final AtomicLong pendingSyncs = new AtomicLong(); private Thread timerThread; private volatile boolean started; // We use this flag to prevent flush occurring between calling checkSize and addBytes // CheckSize must always be followed by it's corresponding addBytes otherwise the buffer // can get in an inconsistent state private boolean delayFlush; // for logging write rates private final boolean logRates; private long bytesFlushed = 0; private final AtomicLong flushesDone = new AtomicLong(0); private Timer logRatesTimer; private TimerTask logRatesTimerTask; // no need to be volatile as every access is synchronized private boolean spinning = false; // Static -------------------------------------------------------- // Constructors -------------------------------------------------- // Public -------------------------------------------------------- public TimedBuffer(final int size, final int timeout, final boolean logRates) { bufferSize = size; this.logRates = logRates; if (logRates) { logRatesTimer = new Timer(true); } // Setting the interval for nano-sleeps //prefer off heap buffer to allow further humongous allocations and reduce GC overhead buffer = new ChannelBufferWrapper(Unpooled.directBuffer(size, size)); buffer.clear(); bufferLimit = 0; callbacks = null; this.timeout = timeout; } public synchronized void start() { if (started) { return; } // Need to start with the spin limiter acquired try { spinLimiter.acquire(); } catch (InterruptedException e) { throw new ActiveMQInterruptedException(e); } timerRunnable = new CheckTimer(); timerThread = new Thread(timerRunnable, "activemq-buffer-timeout"); timerThread.start(); if (logRates) { logRatesTimerTask = new LogRatesTimerTask(); logRatesTimer.scheduleAtFixedRate(logRatesTimerTask, 2000, 2000); } started = true; } public void stop() { if (!started) { return; } flush(); bufferObserver = null; timerRunnable.close(); spinLimiter.release(); if (logRates) { logRatesTimerTask.cancel(); } while (timerThread.isAlive()) { try { timerThread.join(); } catch (InterruptedException e) { throw new ActiveMQInterruptedException(e); } } started = false; } public synchronized void setObserver(final TimedBufferObserver observer) { if (bufferObserver != null) { flush(); } bufferObserver = observer; } /** * Verify if the size fits the buffer * * @param sizeChecked */ public synchronized boolean checkSize(final int sizeChecked) { if (!started) { throw new IllegalStateException("TimedBuffer is not started"); } if (sizeChecked > bufferSize) { throw new IllegalStateException("Can't write records bigger than the bufferSize(" + bufferSize + ") on the journal"); } if (bufferLimit == 0 || buffer.writerIndex() + sizeChecked > bufferLimit) { // Either there is not enough space left in the buffer for the sized record // Or a flush has just been performed and we need to re-calculate bufferLimit flush(); delayFlush = true; final int remainingInFile = bufferObserver.getRemainingBytes(); if (sizeChecked > remainingInFile) { return false; } else { // There is enough space in the file for this size // Need to re-calculate buffer limit bufferLimit = Math.min(remainingInFile, bufferSize); return true; } } else { delayFlush = true; return true; } } public synchronized void addBytes(final ActiveMQBuffer bytes, final boolean sync, final IOCallback callback) { if (!started) { throw new IllegalStateException("TimedBuffer is not started"); } delayFlush = false; //it doesn't modify the reader index of bytes as in the original version final int readableBytes = bytes.readableBytes(); final int writerIndex = buffer.writerIndex(); buffer.setBytes(writerIndex, bytes, bytes.readerIndex(), readableBytes); buffer.writerIndex(writerIndex + readableBytes); if (callbacks == null) { callbacks = new ArrayList<>(); } callbacks.add(callback); if (sync) { final long currentPendingSyncs = pendingSyncs.get(); pendingSyncs.lazySet(currentPendingSyncs + 1); startSpin(); } } public synchronized void addBytes(final EncodingSupport bytes, final boolean sync, final IOCallback callback) { if (!started) { throw new IllegalStateException("TimedBuffer is not started"); } delayFlush = false; bytes.encode(buffer); if (callbacks == null) { callbacks = new ArrayList<>(); } callbacks.add(callback); if (sync) { final long currentPendingSyncs = pendingSyncs.get(); pendingSyncs.lazySet(currentPendingSyncs + 1); startSpin(); } } public void flush() { flush(false); } /** * force means the Journal is moving to a new file. Any pending write need to be done immediately * or data could be lost */ private void flush(final boolean force) { synchronized (this) { if (!started) { throw new IllegalStateException("TimedBuffer is not started"); } if ((force || !delayFlush) && buffer.writerIndex() > 0) { final int pos = buffer.writerIndex(); final ByteBuffer bufferToFlush = bufferObserver.newBuffer(bufferSize, pos); //bufferObserver::newBuffer doesn't necessary return a buffer with limit == pos or limit == bufferSize!! bufferToFlush.limit(pos); //perform memcpy under the hood due to the off heap buffer buffer.getBytes(0, bufferToFlush); final List<IOCallback> ioCallbacks = callbacks == null ? Collections.emptyList() : callbacks; bufferObserver.flushBuffer(bufferToFlush, pendingSyncs.get() > 0, ioCallbacks); stopSpin(); pendingSyncs.lazySet(0); callbacks = null; buffer.clear(); bufferLimit = 0; if (logRates) { logFlushed(pos); } } } } private void logFlushed(int bytes) { this.bytesFlushed += bytes; //more lightweight than XADD if single writer final long currentFlushesDone = flushesDone.get(); //flushesDone::lazySet write-Release bytesFlushed flushesDone.lazySet(currentFlushesDone + 1L); } // Package protected --------------------------------------------- // Protected ----------------------------------------------------- // Private ------------------------------------------------------- // Inner classes ------------------------------------------------- private class LogRatesTimerTask extends TimerTask { private boolean closed; private long lastExecution; private long lastBytesFlushed; private long lastFlushesDone; @Override public synchronized void run() { if (!closed) { long now = System.currentTimeMillis(); final long flushesDone = TimedBuffer.this.flushesDone.get(); //flushesDone::get read-Acquire bytesFlushed final long bytesFlushed = TimedBuffer.this.bytesFlushed; if (lastExecution != 0) { final double rate = 1000 * (double) (bytesFlushed - lastBytesFlushed) / (now - lastExecution); ActiveMQJournalLogger.LOGGER.writeRate(rate, (long) (rate / (1024 * 1024))); final double flushRate = 1000 * (double) (flushesDone - lastFlushesDone) / (now - lastExecution); ActiveMQJournalLogger.LOGGER.flushRate(flushRate); } lastExecution = now; lastBytesFlushed = bytesFlushed; lastFlushesDone = flushesDone; } } @Override public synchronized boolean cancel() { closed = true; return super.cancel(); } } private class CheckTimer implements Runnable { private volatile boolean closed = false; @Override public void run() { int waitTimes = 0; long lastFlushTime = 0; long estimatedOptimalBatch = Runtime.getRuntime().availableProcessors(); final Semaphore spinLimiter = TimedBuffer.this.spinLimiter; final long timeout = TimedBuffer.this.timeout; while (!closed) { boolean flushed = false; final long currentPendingSyncs = pendingSyncs.get(); if (currentPendingSyncs > 0) { if (bufferObserver != null) { final boolean checkpoint = System.nanoTime() > lastFlushTime + timeout; if (checkpoint || currentPendingSyncs >= estimatedOptimalBatch) { flush(); if (checkpoint) { estimatedOptimalBatch = currentPendingSyncs; } else { estimatedOptimalBatch = Math.max(estimatedOptimalBatch, currentPendingSyncs); } lastFlushTime = System.nanoTime(); //a flush has been requested flushed = true; } } } if (flushed) { waitTimes = 0; } else { //instead of interruptible sleeping, perform progressive parks depending on the load waitTimes = TimedBuffer.wait(waitTimes, spinLimiter); } } } public void close() { closed = true; } } private static int wait(int waitTimes, Semaphore spinLimiter) { if (waitTimes < 10) { //doesn't make sense to spin loop here, because of the lock around flush/addBytes operations! Thread.yield(); waitTimes++; } else if (waitTimes < 20) { LockSupport.parkNanos(1L); waitTimes++; } else if (waitTimes < 50) { LockSupport.parkNanos(10L); waitTimes++; } else if (waitTimes < 100) { LockSupport.parkNanos(100L); waitTimes++; } else if (waitTimes < 1000) { LockSupport.parkNanos(1000L); waitTimes++; } else { LockSupport.parkNanos(100_000L); try { spinLimiter.acquire(); spinLimiter.release(); } catch (InterruptedException e) { throw new ActiveMQInterruptedException(e); } } return waitTimes; } /** * Sub classes (tests basically) can use this to override disabling spinning */ protected void stopSpin() { if (spinning) { try { // We acquire the spinLimiter semaphore - this prevents the timer flush thread unnecessarily spinning // when the buffer is inactive spinLimiter.acquire(); } catch (InterruptedException e) { throw new ActiveMQInterruptedException(e); } spinning = false; } } /** * Sub classes (tests basically) can use this to override disabling spinning */ protected void startSpin() { if (!spinning) { spinLimiter.release(); spinning = true; } } }