/* * 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.store.kahadb.disk.journal; import java.io.IOException; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import java.util.zip.Adler32; import java.util.zip.Checksum; import org.apache.activemq.store.kahadb.disk.util.DataByteArrayOutputStream; import org.apache.activemq.store.kahadb.disk.util.LinkedNodeList; import org.apache.activemq.util.ByteSequence; import org.apache.activemq.util.RecoverableRandomAccessFile; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * File Appender instance that performs batched writes in the thread where the write is * queued. This appender does not honor the maxFileLength value in the journal as the * files created here are out-of-band logs used for other purposes such as journal level * compaction. */ public class TargetedDataFileAppender implements FileAppender { private static final Logger LOG = LoggerFactory.getLogger(TargetedDataFileAppender.class); private final Journal journal; private final DataFile target; private final Map<Journal.WriteKey, Journal.WriteCommand> inflightWrites; private final int maxWriteBatchSize; private boolean closed; private boolean preallocate; private WriteBatch nextWriteBatch; private int statIdx = 0; private int[] stats = new int[maxStat]; public class WriteBatch { protected final int offset; public final DataFile dataFile; public final LinkedNodeList<Journal.WriteCommand> writes = new LinkedNodeList<Journal.WriteCommand>(); public int size = Journal.BATCH_CONTROL_RECORD_SIZE; public AtomicReference<IOException> exception = new AtomicReference<IOException>(); public WriteBatch(DataFile dataFile, int offset) { this.dataFile = dataFile; this.offset = offset; this.dataFile.incrementLength(Journal.BATCH_CONTROL_RECORD_SIZE); this.size = Journal.BATCH_CONTROL_RECORD_SIZE; journal.addToTotalLength(Journal.BATCH_CONTROL_RECORD_SIZE); } public WriteBatch(DataFile dataFile, int offset, Journal.WriteCommand write) throws IOException { this(dataFile, offset); append(write); } public boolean canAppend(Journal.WriteCommand write) { int newSize = size + write.location.getSize(); if (newSize >= maxWriteBatchSize) { return false; } return true; } public void append(Journal.WriteCommand write) throws IOException { this.writes.addLast(write); write.location.setDataFileId(dataFile.getDataFileId()); write.location.setOffset(offset + size); int s = write.location.getSize(); size += s; dataFile.incrementLength(s); journal.addToTotalLength(s); } } /** * Construct a Store writer */ public TargetedDataFileAppender(Journal journal, DataFile target) { this.journal = journal; this.target = target; this.inflightWrites = this.journal.getInflightWrites(); this.maxWriteBatchSize = this.journal.getWriteBatchSize(); } @Override public Location storeItem(ByteSequence data, byte type, boolean sync) throws IOException { checkClosed(); // Write the packet our internal buffer. int size = data.getLength() + Journal.RECORD_HEAD_SPACE; final Location location = new Location(); location.setSize(size); location.setType(type); Journal.WriteCommand write = new Journal.WriteCommand(location, data, sync); enqueueWrite(write); if (sync) { writePendingBatch(); } return location; } @Override public Location storeItem(ByteSequence data, byte type, Runnable onComplete) throws IOException { checkClosed(); // Write the packet our internal buffer. int size = data.getLength() + Journal.RECORD_HEAD_SPACE; final Location location = new Location(); location.setSize(size); location.setType(type); Journal.WriteCommand write = new Journal.WriteCommand(location, data, onComplete); enqueueWrite(write); return location; } @Override public void close() throws IOException { if (!closed) { if (nextWriteBatch != null) { // force sync of current in-progress batched write. LOG.debug("Close of targeted appender flushing last batch."); writePendingBatch(); } closed = true; } } //----- Appender Configuration -------------------------------------------// public boolean isPreallocate() { return preallocate; } public void setPreallocate(boolean preallocate) { this.preallocate = preallocate; } //----- Internal Implementation ------------------------------------------// private void checkClosed() throws IOException { if (closed) { throw new IOException("The appender is clsoed"); } } private WriteBatch enqueueWrite(Journal.WriteCommand write) throws IOException { while (true) { if (nextWriteBatch == null) { nextWriteBatch = new WriteBatch(target, target.getLength(), write); break; } else { // Append to current batch if possible.. if (nextWriteBatch.canAppend(write)) { nextWriteBatch.append(write); break; } else { // Flush current batch and start a new one. writePendingBatch(); nextWriteBatch = null; } } } if (!write.sync) { inflightWrites.put(new Journal.WriteKey(write.location), write); } return nextWriteBatch; } private void writePendingBatch() throws IOException { DataFile dataFile = nextWriteBatch.dataFile; try (RecoverableRandomAccessFile file = dataFile.openRandomAccessFile(); DataByteArrayOutputStream buff = new DataByteArrayOutputStream(maxWriteBatchSize);) { // preallocate on first open of new file (length == 0) if configured to do so. // NOTE: dataFile.length cannot be used because it is updated in enqueue if (file.length() == 0L && isPreallocate()) { journal.preallocateEntireJournalDataFile(file); } Journal.WriteCommand write = nextWriteBatch.writes.getHead(); // Write an empty batch control record. buff.reset(); buff.writeInt(Journal.BATCH_CONTROL_RECORD_SIZE); buff.writeByte(Journal.BATCH_CONTROL_RECORD_TYPE); buff.write(Journal.BATCH_CONTROL_RECORD_MAGIC); buff.writeInt(0); buff.writeLong(0); while (write != null) { buff.writeInt(write.location.getSize()); buff.writeByte(write.location.getType()); buff.write(write.data.getData(), write.data.getOffset(), write.data.getLength()); write = write.getNext(); } // append 'unset' next batch (5 bytes) so read can always find eof buff.write(Journal.EOF_RECORD); ByteSequence sequence = buff.toByteSequence(); // Now we can fill in the batch control record properly. buff.reset(); buff.skip(5 + Journal.BATCH_CONTROL_RECORD_MAGIC.length); buff.writeInt(sequence.getLength() - Journal.BATCH_CONTROL_RECORD_SIZE - Journal.EOF_RECORD.length); if (journal.isChecksum()) { Checksum checksum = new Adler32(); checksum.update(sequence.getData(), sequence.getOffset() + Journal.BATCH_CONTROL_RECORD_SIZE, sequence.getLength() - Journal.BATCH_CONTROL_RECORD_SIZE - Journal.EOF_RECORD.length); buff.writeLong(checksum.getValue()); } // Now do the 1 big write. file.seek(nextWriteBatch.offset); if (maxStat > 0) { if (statIdx < maxStat) { stats[statIdx++] = sequence.getLength(); } else { long all = 0; for (; statIdx > 0;) { all += stats[--statIdx]; } LOG.trace("Ave writeSize: {}", all / maxStat); } } file.write(sequence.getData(), sequence.getOffset(), sequence.getLength()); ReplicationTarget replicationTarget = journal.getReplicationTarget(); if (replicationTarget != null) { replicationTarget.replicate(nextWriteBatch.writes.getHead().location, sequence, true); } file.sync(); signalDone(nextWriteBatch); } catch (IOException e) { LOG.info("Journal failed while writing at: {}", nextWriteBatch.offset); throw e; } } private void signalDone(WriteBatch writeBatch) { // Now that the data is on disk, remove the writes from the in // flight cache and signal any onComplete requests. Journal.WriteCommand write = writeBatch.writes.getHead(); while (write != null) { if (!write.sync) { inflightWrites.remove(new Journal.WriteKey(write.location)); } if (write.onComplete != null) { try { write.onComplete.run(); } catch (Throwable e) { LOG.info("Add exception was raised while executing the run command for onComplete", e); } } write = write.getNext(); } } }