001/** 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.activemq.store.kahadb.disk.journal; 018 019import java.io.EOFException; 020import java.io.File; 021import java.io.FileNotFoundException; 022import java.io.FilenameFilter; 023import java.io.IOException; 024import java.io.RandomAccessFile; 025import java.io.UnsupportedEncodingException; 026import java.nio.ByteBuffer; 027import java.nio.channels.ClosedByInterruptException; 028import java.nio.channels.FileChannel; 029import java.util.Collections; 030import java.util.HashMap; 031import java.util.Iterator; 032import java.util.LinkedHashMap; 033import java.util.LinkedList; 034import java.util.Map; 035import java.util.Set; 036import java.util.TreeMap; 037import java.util.concurrent.ConcurrentHashMap; 038import java.util.concurrent.Executors; 039import java.util.concurrent.Future; 040import java.util.concurrent.ScheduledExecutorService; 041import java.util.concurrent.ScheduledFuture; 042import java.util.concurrent.ThreadFactory; 043import java.util.concurrent.TimeUnit; 044import java.util.concurrent.atomic.AtomicLong; 045import java.util.concurrent.atomic.AtomicReference; 046import java.util.zip.Adler32; 047import java.util.zip.Checksum; 048 049import org.apache.activemq.store.kahadb.disk.util.LinkedNode; 050import org.apache.activemq.store.kahadb.disk.util.LinkedNodeList; 051import org.apache.activemq.store.kahadb.disk.util.Sequence; 052import org.apache.activemq.util.ByteSequence; 053import org.apache.activemq.util.DataByteArrayInputStream; 054import org.apache.activemq.util.DataByteArrayOutputStream; 055import org.apache.activemq.util.IOHelper; 056import org.apache.activemq.util.RecoverableRandomAccessFile; 057import org.apache.activemq.util.ThreadPoolUtils; 058import org.slf4j.Logger; 059import org.slf4j.LoggerFactory; 060 061/** 062 * Manages DataFiles 063 */ 064public class Journal { 065 public static final String CALLER_BUFFER_APPENDER = "org.apache.kahadb.journal.CALLER_BUFFER_APPENDER"; 066 public static final boolean callerBufferAppender = Boolean.parseBoolean(System.getProperty(CALLER_BUFFER_APPENDER, "false")); 067 068 private static final int PREALLOC_CHUNK_SIZE = 1024*1024; 069 070 // ITEM_HEAD_SPACE = length + type+ reserved space + SOR 071 public static final int RECORD_HEAD_SPACE = 4 + 1; 072 073 public static final byte USER_RECORD_TYPE = 1; 074 public static final byte BATCH_CONTROL_RECORD_TYPE = 2; 075 // Batch Control Item holds a 4 byte size of the batch and a 8 byte checksum of the batch. 076 public static final byte[] BATCH_CONTROL_RECORD_MAGIC = bytes("WRITE BATCH"); 077 public static final int BATCH_CONTROL_RECORD_SIZE = RECORD_HEAD_SPACE + BATCH_CONTROL_RECORD_MAGIC.length + 4 + 8; 078 public static final byte[] BATCH_CONTROL_RECORD_HEADER = createBatchControlRecordHeader(); 079 public static final byte[] EMPTY_BATCH_CONTROL_RECORD = createEmptyBatchControlRecordHeader(); 080 public static final int EOF_INT = ByteBuffer.wrap(new byte[]{'-', 'q', 'M', 'a'}).getInt(); 081 public static final byte EOF_EOT = '4'; 082 public static final byte[] EOF_RECORD = createEofBatchAndLocationRecord(); 083 084 private ScheduledExecutorService scheduler; 085 086 // tackle corruption when checksum is disabled or corrupt with zeros, minimize data loss 087 public void corruptRecoveryLocation(Location recoveryPosition) throws IOException { 088 DataFile dataFile = getDataFile(recoveryPosition); 089 // with corruption on recovery we have no faith in the content - slip to the next batch record or eof 090 DataFileAccessor reader = accessorPool.openDataFileAccessor(dataFile); 091 try { 092 RandomAccessFile randomAccessFile = reader.getRaf().getRaf(); 093 randomAccessFile.seek(recoveryPosition.getOffset() + 1); 094 byte[] data = new byte[getWriteBatchSize()]; 095 ByteSequence bs = new ByteSequence(data, 0, randomAccessFile.read(data)); 096 int nextOffset = 0; 097 if (findNextBatchRecord(bs, randomAccessFile) >= 0) { 098 nextOffset = Math.toIntExact(randomAccessFile.getFilePointer() - bs.remaining()); 099 } else { 100 nextOffset = Math.toIntExact(randomAccessFile.length()); 101 } 102 Sequence sequence = new Sequence(recoveryPosition.getOffset(), nextOffset - 1); 103 LOG.warn("Corrupt journal records found in '{}' between offsets: {}", dataFile.getFile(), sequence); 104 105 // skip corruption on getNextLocation 106 recoveryPosition.setOffset(nextOffset); 107 recoveryPosition.setSize(-1); 108 109 dataFile.corruptedBlocks.add(sequence); 110 } catch (IOException e) { 111 } finally { 112 accessorPool.closeDataFileAccessor(reader); 113 } 114 } 115 116 public DataFileAccessorPool getAccessorPool() { 117 return accessorPool; 118 } 119 120 public void allowIOResumption() { 121 if (appender instanceof DataFileAppender) { 122 DataFileAppender dataFileAppender = (DataFileAppender)appender; 123 dataFileAppender.shutdown = false; 124 } 125 } 126 127 public void setCleanupInterval(long cleanupInterval) { 128 this.cleanupInterval = cleanupInterval; 129 } 130 131 public long getCleanupInterval() { 132 return cleanupInterval; 133 } 134 135 public enum PreallocationStrategy { 136 SPARSE_FILE, 137 OS_KERNEL_COPY, 138 ZEROS, 139 CHUNKED_ZEROS; 140 } 141 142 public enum PreallocationScope { 143 ENTIRE_JOURNAL, 144 ENTIRE_JOURNAL_ASYNC, 145 NONE; 146 } 147 148 public enum JournalDiskSyncStrategy { 149 ALWAYS, 150 PERIODIC, 151 NEVER; 152 } 153 154 private static byte[] createBatchControlRecordHeader() { 155 try (DataByteArrayOutputStream os = new DataByteArrayOutputStream();) { 156 os.writeInt(BATCH_CONTROL_RECORD_SIZE); 157 os.writeByte(BATCH_CONTROL_RECORD_TYPE); 158 os.write(BATCH_CONTROL_RECORD_MAGIC); 159 ByteSequence sequence = os.toByteSequence(); 160 sequence.compact(); 161 return sequence.getData(); 162 } catch (IOException e) { 163 throw new RuntimeException("Could not create batch control record header.", e); 164 } 165 } 166 167 private static byte[] createEmptyBatchControlRecordHeader() { 168 try (DataByteArrayOutputStream os = new DataByteArrayOutputStream();) { 169 os.writeInt(BATCH_CONTROL_RECORD_SIZE); 170 os.writeByte(BATCH_CONTROL_RECORD_TYPE); 171 os.write(BATCH_CONTROL_RECORD_MAGIC); 172 os.writeInt(0); 173 os.writeLong(0l); 174 ByteSequence sequence = os.toByteSequence(); 175 sequence.compact(); 176 return sequence.getData(); 177 } catch (IOException e) { 178 throw new RuntimeException("Could not create empty batch control record header.", e); 179 } 180 } 181 182 private static byte[] createEofBatchAndLocationRecord() { 183 try (DataByteArrayOutputStream os = new DataByteArrayOutputStream();) { 184 os.writeInt(EOF_INT); 185 os.writeByte(EOF_EOT); 186 ByteSequence sequence = os.toByteSequence(); 187 sequence.compact(); 188 return sequence.getData(); 189 } catch (IOException e) { 190 throw new RuntimeException("Could not create eof header.", e); 191 } 192 } 193 194 public static final String DEFAULT_DIRECTORY = "."; 195 public static final String DEFAULT_ARCHIVE_DIRECTORY = "data-archive"; 196 public static final String DEFAULT_FILE_PREFIX = "db-"; 197 public static final String DEFAULT_FILE_SUFFIX = ".log"; 198 public static final int DEFAULT_MAX_FILE_LENGTH = 1024 * 1024 * 32; 199 public static final int DEFAULT_CLEANUP_INTERVAL = 1000 * 30; 200 public static final int DEFAULT_MAX_WRITE_BATCH_SIZE = 1024 * 1024 * 4; 201 202 private static final Logger LOG = LoggerFactory.getLogger(Journal.class); 203 204 protected final Map<WriteKey, WriteCommand> inflightWrites = new ConcurrentHashMap<WriteKey, WriteCommand>(); 205 206 protected File directory = new File(DEFAULT_DIRECTORY); 207 protected File directoryArchive; 208 private boolean directoryArchiveOverridden = false; 209 210 protected String filePrefix = DEFAULT_FILE_PREFIX; 211 protected String fileSuffix = DEFAULT_FILE_SUFFIX; 212 protected boolean started; 213 214 protected int maxFileLength = DEFAULT_MAX_FILE_LENGTH; 215 protected int writeBatchSize = DEFAULT_MAX_WRITE_BATCH_SIZE; 216 217 protected FileAppender appender; 218 protected DataFileAccessorPool accessorPool; 219 220 protected Map<Integer, DataFile> fileMap = new HashMap<Integer, DataFile>(); 221 protected Map<File, DataFile> fileByFileMap = new LinkedHashMap<File, DataFile>(); 222 protected LinkedNodeList<DataFile> dataFiles = new LinkedNodeList<DataFile>(); 223 224 protected final AtomicReference<Location> lastAppendLocation = new AtomicReference<Location>(); 225 protected ScheduledFuture cleanupTask; 226 protected AtomicLong totalLength = new AtomicLong(); 227 protected boolean archiveDataLogs; 228 private ReplicationTarget replicationTarget; 229 protected boolean checksum; 230 protected boolean checkForCorruptionOnStartup; 231 protected boolean enableAsyncDiskSync = true; 232 private int nextDataFileId = 1; 233 private Object dataFileIdLock = new Object(); 234 private final AtomicReference<DataFile> currentDataFile = new AtomicReference<>(null); 235 private volatile DataFile nextDataFile; 236 237 protected PreallocationScope preallocationScope = PreallocationScope.ENTIRE_JOURNAL; 238 protected PreallocationStrategy preallocationStrategy = PreallocationStrategy.SPARSE_FILE; 239 private File osKernelCopyTemplateFile = null; 240 private ByteBuffer preAllocateDirectBuffer = null; 241 private long cleanupInterval = DEFAULT_CLEANUP_INTERVAL; 242 243 protected JournalDiskSyncStrategy journalDiskSyncStrategy = JournalDiskSyncStrategy.ALWAYS; 244 245 public interface DataFileRemovedListener { 246 void fileRemoved(DataFile datafile); 247 } 248 249 private DataFileRemovedListener dataFileRemovedListener; 250 251 public synchronized void start() throws IOException { 252 if (started) { 253 return; 254 } 255 256 long start = System.currentTimeMillis(); 257 accessorPool = new DataFileAccessorPool(this); 258 started = true; 259 260 appender = callerBufferAppender ? new CallerBufferingDataFileAppender(this) : new DataFileAppender(this); 261 262 File[] files = directory.listFiles(new FilenameFilter() { 263 @Override 264 public boolean accept(File dir, String n) { 265 return dir.equals(directory) && n.startsWith(filePrefix) && n.endsWith(fileSuffix); 266 } 267 }); 268 269 if (files != null) { 270 for (File file : files) { 271 try { 272 String n = file.getName(); 273 String numStr = n.substring(filePrefix.length(), n.length()-fileSuffix.length()); 274 int num = Integer.parseInt(numStr); 275 DataFile dataFile = new DataFile(file, num); 276 fileMap.put(dataFile.getDataFileId(), dataFile); 277 totalLength.addAndGet(dataFile.getLength()); 278 } catch (NumberFormatException e) { 279 // Ignore file that do not match the pattern. 280 } 281 } 282 283 // Sort the list so that we can link the DataFiles together in the 284 // right order. 285 LinkedList<DataFile> l = new LinkedList<>(fileMap.values()); 286 Collections.sort(l); 287 for (DataFile df : l) { 288 if (df.getLength() == 0) { 289 // possibly the result of a previous failed write 290 LOG.info("ignoring zero length, partially initialised journal data file: " + df); 291 continue; 292 } else if (l.getLast().equals(df) && isUnusedPreallocated(df)) { 293 continue; 294 } 295 dataFiles.addLast(df); 296 fileByFileMap.put(df.getFile(), df); 297 298 if( isCheckForCorruptionOnStartup() ) { 299 lastAppendLocation.set(recoveryCheck(df)); 300 } 301 } 302 } 303 304 if (preallocationScope != PreallocationScope.NONE) { 305 switch (preallocationStrategy) { 306 case SPARSE_FILE: 307 break; 308 case OS_KERNEL_COPY: { 309 osKernelCopyTemplateFile = createJournalTemplateFile(); 310 } 311 break; 312 case CHUNKED_ZEROS: { 313 preAllocateDirectBuffer = allocateDirectBuffer(PREALLOC_CHUNK_SIZE); 314 } 315 break; 316 case ZEROS: { 317 preAllocateDirectBuffer = allocateDirectBuffer(getMaxFileLength()); 318 } 319 break; 320 } 321 } 322 scheduler = Executors.newScheduledThreadPool(1, new ThreadFactory() { 323 @Override 324 public Thread newThread(Runnable r) { 325 Thread schedulerThread = new Thread(r); 326 schedulerThread.setName("ActiveMQ Journal Scheduled executor"); 327 schedulerThread.setDaemon(true); 328 return schedulerThread; 329 } 330 }); 331 332 // init current write file 333 if (dataFiles.isEmpty()) { 334 nextDataFileId = 1; 335 rotateWriteFile(); 336 } else { 337 currentDataFile.set(dataFiles.getTail()); 338 nextDataFileId = currentDataFile.get().dataFileId + 1; 339 } 340 341 if( lastAppendLocation.get()==null ) { 342 DataFile df = dataFiles.getTail(); 343 lastAppendLocation.set(recoveryCheck(df)); 344 } 345 346 // ensure we don't report unused space of last journal file in size metric 347 int lastFileLength = dataFiles.getTail().getLength(); 348 if (totalLength.get() > lastFileLength && lastAppendLocation.get().getOffset() > 0) { 349 totalLength.addAndGet(lastAppendLocation.get().getOffset() - lastFileLength); 350 } 351 352 cleanupTask = scheduler.scheduleAtFixedRate(new Runnable() { 353 @Override 354 public void run() { 355 cleanup(); 356 } 357 }, cleanupInterval, cleanupInterval, TimeUnit.MILLISECONDS); 358 359 long end = System.currentTimeMillis(); 360 LOG.trace("Startup took: "+(end-start)+" ms"); 361 } 362 363 private ByteBuffer allocateDirectBuffer(int size) { 364 ByteBuffer buffer = ByteBuffer.allocateDirect(size); 365 buffer.put(EOF_RECORD); 366 return buffer; 367 } 368 369 public void preallocateEntireJournalDataFile(RecoverableRandomAccessFile file) { 370 371 if (PreallocationScope.NONE != preallocationScope) { 372 373 try { 374 if (PreallocationStrategy.OS_KERNEL_COPY == preallocationStrategy) { 375 doPreallocationKernelCopy(file); 376 } else if (PreallocationStrategy.ZEROS == preallocationStrategy) { 377 doPreallocationZeros(file); 378 } else if (PreallocationStrategy.CHUNKED_ZEROS == preallocationStrategy) { 379 doPreallocationChunkedZeros(file); 380 } else { 381 doPreallocationSparseFile(file); 382 } 383 } catch (Throwable continueWithNoPrealloc) { 384 // error on preallocation is non fatal, and we don't want to leak the journal handle 385 LOG.error("cound not preallocate journal data file", continueWithNoPrealloc); 386 } 387 } 388 } 389 390 private void doPreallocationSparseFile(RecoverableRandomAccessFile file) { 391 final ByteBuffer journalEof = ByteBuffer.wrap(EOF_RECORD); 392 try { 393 FileChannel channel = file.getChannel(); 394 channel.position(0); 395 channel.write(journalEof); 396 channel.position(maxFileLength - 5); 397 journalEof.rewind(); 398 channel.write(journalEof); 399 channel.force(false); 400 channel.position(0); 401 } catch (ClosedByInterruptException ignored) { 402 LOG.trace("Could not preallocate journal file with sparse file", ignored); 403 } catch (IOException e) { 404 LOG.error("Could not preallocate journal file with sparse file", e); 405 } 406 } 407 408 private void doPreallocationZeros(RecoverableRandomAccessFile file) { 409 preAllocateDirectBuffer.rewind(); 410 try { 411 FileChannel channel = file.getChannel(); 412 channel.write(preAllocateDirectBuffer); 413 channel.force(false); 414 channel.position(0); 415 } catch (ClosedByInterruptException ignored) { 416 LOG.trace("Could not preallocate journal file with zeros", ignored); 417 } catch (IOException e) { 418 LOG.error("Could not preallocate journal file with zeros", e); 419 } 420 } 421 422 private void doPreallocationKernelCopy(RecoverableRandomAccessFile file) { 423 try (RandomAccessFile templateRaf = new RandomAccessFile(osKernelCopyTemplateFile, "rw");){ 424 templateRaf.getChannel().transferTo(0, getMaxFileLength(), file.getChannel()); 425 } catch (ClosedByInterruptException ignored) { 426 LOG.trace("Could not preallocate journal file with kernel copy", ignored); 427 } catch (FileNotFoundException e) { 428 LOG.error("Could not find the template file on disk at " + osKernelCopyTemplateFile.getAbsolutePath(), e); 429 } catch (IOException e) { 430 LOG.error("Could not transfer the template file to journal, transferFile=" + osKernelCopyTemplateFile.getAbsolutePath(), e); 431 } 432 } 433 434 private File createJournalTemplateFile() { 435 String fileName = "db-log.template"; 436 File rc = new File(directory, fileName); 437 try (RandomAccessFile templateRaf = new RandomAccessFile(rc, "rw");) { 438 templateRaf.getChannel().write(ByteBuffer.wrap(EOF_RECORD)); 439 templateRaf.setLength(maxFileLength); 440 templateRaf.getChannel().force(true); 441 } catch (FileNotFoundException e) { 442 LOG.error("Could not find the template file on disk at " + osKernelCopyTemplateFile.getAbsolutePath(), e); 443 } catch (IOException e) { 444 LOG.error("Could not transfer the template file to journal, transferFile=" + osKernelCopyTemplateFile.getAbsolutePath(), e); 445 } 446 return rc; 447 } 448 449 private void doPreallocationChunkedZeros(RecoverableRandomAccessFile file) { 450 preAllocateDirectBuffer.limit(preAllocateDirectBuffer.capacity()); 451 preAllocateDirectBuffer.rewind(); 452 try { 453 FileChannel channel = file.getChannel(); 454 455 int remLen = maxFileLength; 456 while (remLen > 0) { 457 if (remLen < preAllocateDirectBuffer.remaining()) { 458 preAllocateDirectBuffer.limit(remLen); 459 } 460 int writeLen = channel.write(preAllocateDirectBuffer); 461 remLen -= writeLen; 462 preAllocateDirectBuffer.rewind(); 463 } 464 465 channel.force(false); 466 channel.position(0); 467 } catch (ClosedByInterruptException ignored) { 468 LOG.trace("Could not preallocate journal file with zeros", ignored); 469 } catch (IOException e) { 470 LOG.error("Could not preallocate journal file with zeros! Will continue without preallocation", e); 471 } 472 } 473 474 private static byte[] bytes(String string) { 475 try { 476 return string.getBytes("UTF-8"); 477 } catch (UnsupportedEncodingException e) { 478 throw new RuntimeException(e); 479 } 480 } 481 482 public boolean isUnusedPreallocated(DataFile dataFile) throws IOException { 483 if (preallocationScope == PreallocationScope.ENTIRE_JOURNAL_ASYNC) { 484 DataFileAccessor reader = accessorPool.openDataFileAccessor(dataFile); 485 try { 486 byte[] firstFewBytes = new byte[BATCH_CONTROL_RECORD_HEADER.length]; 487 reader.readFully(0, firstFewBytes); 488 ByteSequence bs = new ByteSequence(firstFewBytes); 489 return bs.startsWith(EOF_RECORD); 490 } catch (Exception ignored) { 491 } finally { 492 accessorPool.closeDataFileAccessor(reader); 493 } 494 } 495 return false; 496 } 497 498 protected Location recoveryCheck(DataFile dataFile) throws IOException { 499 Location location = new Location(); 500 location.setDataFileId(dataFile.getDataFileId()); 501 location.setOffset(0); 502 503 DataFileAccessor reader = accessorPool.openDataFileAccessor(dataFile); 504 try { 505 RandomAccessFile randomAccessFile = reader.getRaf().getRaf(); 506 randomAccessFile.seek(0); 507 final long totalFileLength = randomAccessFile.length(); 508 byte[] data = new byte[getWriteBatchSize()]; 509 ByteSequence bs = new ByteSequence(data, 0, randomAccessFile.read(data)); 510 511 while (true) { 512 int size = checkBatchRecord(bs, randomAccessFile); 513 if (size >= 0 && location.getOffset() + BATCH_CONTROL_RECORD_SIZE + size <= totalFileLength) { 514 if (size == 0) { 515 // eof batch record 516 break; 517 } 518 location.setOffset(location.getOffset() + BATCH_CONTROL_RECORD_SIZE + size); 519 } else { 520 521 // Perhaps it's just some corruption... scan through the 522 // file to find the next valid batch record. We 523 // may have subsequent valid batch records. 524 if (findNextBatchRecord(bs, randomAccessFile) >= 0) { 525 int nextOffset = Math.toIntExact(randomAccessFile.getFilePointer() - bs.remaining()); 526 Sequence sequence = new Sequence(location.getOffset(), nextOffset - 1); 527 LOG.warn("Corrupt journal records found in '{}' between offsets: {}", dataFile.getFile(), sequence); 528 dataFile.corruptedBlocks.add(sequence); 529 location.setOffset(nextOffset); 530 } else { 531 break; 532 } 533 } 534 } 535 536 } catch (IOException e) { 537 } finally { 538 accessorPool.closeDataFileAccessor(reader); 539 } 540 541 int existingLen = dataFile.getLength(); 542 dataFile.setLength(location.getOffset()); 543 if (existingLen > dataFile.getLength()) { 544 totalLength.addAndGet(dataFile.getLength() - existingLen); 545 } 546 547 if (!dataFile.corruptedBlocks.isEmpty()) { 548 // Is the end of the data file corrupted? 549 if (dataFile.corruptedBlocks.getTail().getLast() + 1 == location.getOffset()) { 550 dataFile.setLength((int) dataFile.corruptedBlocks.removeLastSequence().getFirst()); 551 } 552 } 553 554 return location; 555 } 556 557 private int findNextBatchRecord(ByteSequence bs, RandomAccessFile reader) throws IOException { 558 final ByteSequence header = new ByteSequence(BATCH_CONTROL_RECORD_HEADER); 559 int pos = 0; 560 while (true) { 561 pos = bs.indexOf(header, 0); 562 if (pos >= 0) { 563 bs.setOffset(bs.offset + pos); 564 return pos; 565 } else { 566 // need to load the next data chunck in.. 567 if (bs.length != bs.data.length) { 568 // If we had a short read then we were at EOF 569 return -1; 570 } 571 bs.setOffset(bs.length - BATCH_CONTROL_RECORD_HEADER.length); 572 bs.reset(); 573 bs.setLength(bs.length + reader.read(bs.data, bs.length, bs.data.length - BATCH_CONTROL_RECORD_HEADER.length)); 574 } 575 } 576 } 577 578 private int checkBatchRecord(ByteSequence bs, RandomAccessFile reader) throws IOException { 579 ensureAvailable(bs, reader, EOF_RECORD.length); 580 if (bs.startsWith(EOF_RECORD)) { 581 return 0; // eof 582 } 583 ensureAvailable(bs, reader, BATCH_CONTROL_RECORD_SIZE); 584 try (DataByteArrayInputStream controlIs = new DataByteArrayInputStream(bs)) { 585 586 // Assert that it's a batch record. 587 for (int i = 0; i < BATCH_CONTROL_RECORD_HEADER.length; i++) { 588 if (controlIs.readByte() != BATCH_CONTROL_RECORD_HEADER[i]) { 589 return -1; 590 } 591 } 592 593 int size = controlIs.readInt(); 594 if (size < 0 || size > Integer.MAX_VALUE - (BATCH_CONTROL_RECORD_SIZE + EOF_RECORD.length)) { 595 return -2; 596 } 597 598 long expectedChecksum = controlIs.readLong(); 599 Checksum checksum = null; 600 if (isChecksum() && expectedChecksum > 0) { 601 checksum = new Adler32(); 602 } 603 604 // revert to bs to consume data 605 bs.setOffset(controlIs.position()); 606 int toRead = size; 607 while (toRead > 0) { 608 if (bs.remaining() >= toRead) { 609 if (checksum != null) { 610 checksum.update(bs.getData(), bs.getOffset(), toRead); 611 } 612 bs.setOffset(bs.offset + toRead); 613 toRead = 0; 614 } else { 615 if (bs.length != bs.data.length) { 616 // buffer exhausted 617 return -3; 618 } 619 620 toRead -= bs.remaining(); 621 if (checksum != null) { 622 checksum.update(bs.getData(), bs.getOffset(), bs.remaining()); 623 } 624 bs.setLength(reader.read(bs.data)); 625 bs.setOffset(0); 626 } 627 } 628 if (checksum != null && expectedChecksum != checksum.getValue()) { 629 return -4; 630 } 631 632 return size; 633 } 634 } 635 636 private void ensureAvailable(ByteSequence bs, RandomAccessFile reader, int required) throws IOException { 637 if (bs.remaining() < required) { 638 bs.reset(); 639 int read = reader.read(bs.data, bs.length, bs.data.length - bs.length); 640 if (read < 0) { 641 if (bs.remaining() == 0) { 642 throw new EOFException("request for " + required + " bytes reached EOF"); 643 } 644 } 645 bs.setLength(bs.length + read); 646 } 647 } 648 649 void addToTotalLength(int size) { 650 totalLength.addAndGet(size); 651 } 652 653 public long length() { 654 return totalLength.get(); 655 } 656 657 private void rotateWriteFile() throws IOException { 658 synchronized (dataFileIdLock) { 659 DataFile dataFile = nextDataFile; 660 if (dataFile == null) { 661 dataFile = newDataFile(); 662 } 663 synchronized (currentDataFile) { 664 fileMap.put(dataFile.getDataFileId(), dataFile); 665 fileByFileMap.put(dataFile.getFile(), dataFile); 666 dataFiles.addLast(dataFile); 667 currentDataFile.set(dataFile); 668 } 669 nextDataFile = null; 670 } 671 if (PreallocationScope.ENTIRE_JOURNAL_ASYNC == preallocationScope) { 672 preAllocateNextDataFileFuture = scheduler.submit(preAllocateNextDataFileTask); 673 } 674 } 675 676 private Runnable preAllocateNextDataFileTask = new Runnable() { 677 @Override 678 public void run() { 679 if (nextDataFile == null) { 680 synchronized (dataFileIdLock){ 681 try { 682 nextDataFile = newDataFile(); 683 } catch (IOException e) { 684 LOG.warn("Failed to proactively allocate data file", e); 685 } 686 } 687 } 688 } 689 }; 690 691 private volatile Future preAllocateNextDataFileFuture; 692 693 private DataFile newDataFile() throws IOException { 694 int nextNum = nextDataFileId++; 695 File file = getFile(nextNum); 696 DataFile nextWriteFile = new DataFile(file, nextNum); 697 preallocateEntireJournalDataFile(nextWriteFile.appendRandomAccessFile()); 698 return nextWriteFile; 699 } 700 701 702 public DataFile reserveDataFile() { 703 synchronized (dataFileIdLock) { 704 int nextNum = nextDataFileId++; 705 File file = getFile(nextNum); 706 DataFile reservedDataFile = new DataFile(file, nextNum); 707 synchronized (currentDataFile) { 708 fileMap.put(reservedDataFile.getDataFileId(), reservedDataFile); 709 fileByFileMap.put(file, reservedDataFile); 710 if (dataFiles.isEmpty()) { 711 dataFiles.addLast(reservedDataFile); 712 } else { 713 dataFiles.getTail().linkBefore(reservedDataFile); 714 } 715 } 716 return reservedDataFile; 717 } 718 } 719 720 public File getFile(int nextNum) { 721 String fileName = filePrefix + nextNum + fileSuffix; 722 File file = new File(directory, fileName); 723 return file; 724 } 725 726 DataFile getDataFile(Location item) throws IOException { 727 Integer key = Integer.valueOf(item.getDataFileId()); 728 DataFile dataFile = null; 729 synchronized (currentDataFile) { 730 dataFile = fileMap.get(key); 731 } 732 if (dataFile == null) { 733 LOG.error("Looking for key " + key + " but not found in fileMap: " + fileMap); 734 throw new IOException("Could not locate data file " + getFile(item.getDataFileId())); 735 } 736 return dataFile; 737 } 738 739 public void close() throws IOException { 740 synchronized (this) { 741 if (!started) { 742 return; 743 } 744 cleanupTask.cancel(true); 745 if (preAllocateNextDataFileFuture != null) { 746 preAllocateNextDataFileFuture.cancel(true); 747 } 748 ThreadPoolUtils.shutdownGraceful(scheduler, 4000); 749 accessorPool.close(); 750 } 751 // the appender can be calling back to to the journal blocking a close AMQ-5620 752 appender.close(); 753 synchronized (currentDataFile) { 754 fileMap.clear(); 755 fileByFileMap.clear(); 756 dataFiles.clear(); 757 lastAppendLocation.set(null); 758 started = false; 759 } 760 } 761 762 public synchronized void cleanup() { 763 if (accessorPool != null) { 764 accessorPool.disposeUnused(); 765 } 766 } 767 768 public synchronized boolean delete() throws IOException { 769 770 // Close all open file handles... 771 appender.close(); 772 accessorPool.close(); 773 774 boolean result = true; 775 for (Iterator<DataFile> i = fileMap.values().iterator(); i.hasNext();) { 776 DataFile dataFile = i.next(); 777 result &= dataFile.delete(); 778 } 779 780 if (preAllocateNextDataFileFuture != null) { 781 preAllocateNextDataFileFuture.cancel(true); 782 } 783 synchronized (dataFileIdLock) { 784 if (nextDataFile != null) { 785 nextDataFile.delete(); 786 nextDataFile = null; 787 } 788 } 789 790 totalLength.set(0); 791 synchronized (currentDataFile) { 792 fileMap.clear(); 793 fileByFileMap.clear(); 794 lastAppendLocation.set(null); 795 dataFiles = new LinkedNodeList<DataFile>(); 796 } 797 // reopen open file handles... 798 accessorPool = new DataFileAccessorPool(this); 799 appender = new DataFileAppender(this); 800 return result; 801 } 802 803 public void removeDataFiles(Set<Integer> files) throws IOException { 804 for (Integer key : files) { 805 // Can't remove the data file (or subsequent files) that is currently being written to. 806 if (key >= lastAppendLocation.get().getDataFileId()) { 807 continue; 808 } 809 DataFile dataFile = null; 810 synchronized (currentDataFile) { 811 dataFile = fileMap.remove(key); 812 if (dataFile != null) { 813 fileByFileMap.remove(dataFile.getFile()); 814 dataFile.unlink(); 815 } 816 } 817 if (dataFile != null) { 818 forceRemoveDataFile(dataFile); 819 } 820 } 821 } 822 823 private void forceRemoveDataFile(DataFile dataFile) throws IOException { 824 accessorPool.disposeDataFileAccessors(dataFile); 825 totalLength.addAndGet(-dataFile.getLength()); 826 if (archiveDataLogs) { 827 File directoryArchive = getDirectoryArchive(); 828 if (directoryArchive.exists()) { 829 LOG.debug("Archive directory exists: {}", directoryArchive); 830 } else { 831 if (directoryArchive.isAbsolute()) 832 if (LOG.isDebugEnabled()) { 833 LOG.debug("Archive directory [{}] does not exist - creating it now", 834 directoryArchive.getAbsolutePath()); 835 } 836 IOHelper.mkdirs(directoryArchive); 837 } 838 LOG.debug("Moving data file {} to {} ", dataFile, directoryArchive.getCanonicalPath()); 839 dataFile.move(directoryArchive); 840 LOG.debug("Successfully moved data file"); 841 } else { 842 LOG.debug("Deleting data file: {}", dataFile); 843 if (dataFile.delete()) { 844 LOG.debug("Discarded data file: {}", dataFile); 845 } else { 846 LOG.warn("Failed to discard data file : {}", dataFile.getFile()); 847 } 848 } 849 if (dataFileRemovedListener != null) { 850 dataFileRemovedListener.fileRemoved(dataFile); 851 } 852 } 853 854 /** 855 * @return the maxFileLength 856 */ 857 public int getMaxFileLength() { 858 return maxFileLength; 859 } 860 861 /** 862 * @param maxFileLength the maxFileLength to set 863 */ 864 public void setMaxFileLength(int maxFileLength) { 865 this.maxFileLength = maxFileLength; 866 } 867 868 @Override 869 public String toString() { 870 return directory.toString(); 871 } 872 873 public Location getNextLocation(Location location) throws IOException, IllegalStateException { 874 return getNextLocation(location, null); 875 } 876 877 public Location getNextLocation(Location location, Location limit) throws IOException, IllegalStateException { 878 Location cur = null; 879 while (true) { 880 if (cur == null) { 881 if (location == null) { 882 DataFile head = null; 883 synchronized (currentDataFile) { 884 head = dataFiles.getHead(); 885 } 886 if (head == null) { 887 return null; 888 } 889 cur = new Location(); 890 cur.setDataFileId(head.getDataFileId()); 891 cur.setOffset(0); 892 } else { 893 // Set to the next offset.. 894 if (location.getSize() == -1) { 895 cur = new Location(location); 896 } else { 897 cur = new Location(location); 898 cur.setOffset(location.getOffset() + location.getSize()); 899 } 900 } 901 } else { 902 cur.setOffset(cur.getOffset() + cur.getSize()); 903 } 904 905 DataFile dataFile = getDataFile(cur); 906 907 // Did it go into the next file?? 908 if (dataFile.getLength() <= cur.getOffset()) { 909 synchronized (currentDataFile) { 910 dataFile = dataFile.getNext(); 911 } 912 if (dataFile == null) { 913 return null; 914 } else { 915 cur.setDataFileId(dataFile.getDataFileId().intValue()); 916 cur.setOffset(0); 917 if (limit != null && cur.compareTo(limit) >= 0) { 918 LOG.trace("reached limit: {} at: {}", limit, cur); 919 return null; 920 } 921 } 922 } 923 924 // Load in location size and type. 925 DataFileAccessor reader = accessorPool.openDataFileAccessor(dataFile); 926 try { 927 reader.readLocationDetails(cur); 928 } catch (EOFException eof) { 929 LOG.trace("EOF on next: " + location + ", cur: " + cur); 930 throw eof; 931 } finally { 932 accessorPool.closeDataFileAccessor(reader); 933 } 934 935 Sequence corruptedRange = dataFile.corruptedBlocks.get(cur.getOffset()); 936 if (corruptedRange != null) { 937 // skip corruption 938 cur.setSize((int) corruptedRange.range()); 939 } else if (cur.getSize() == EOF_INT && cur.getType() == EOF_EOT || 940 (cur.getType() == 0 && cur.getSize() == 0)) { 941 // eof - jump to next datafile 942 // EOF_INT and EOF_EOT replace 0,0 - we need to react to both for 943 // replay of existing journals 944 // possibly journal is larger than maxFileLength after config change 945 cur.setSize(EOF_RECORD.length); 946 cur.setOffset(Math.max(maxFileLength, dataFile.getLength())); 947 } else if (cur.getType() == USER_RECORD_TYPE) { 948 // Only return user records. 949 return cur; 950 } 951 } 952 } 953 954 public ByteSequence read(Location location) throws IOException, IllegalStateException { 955 DataFile dataFile = getDataFile(location); 956 DataFileAccessor reader = accessorPool.openDataFileAccessor(dataFile); 957 ByteSequence rc = null; 958 try { 959 rc = reader.readRecord(location); 960 } finally { 961 accessorPool.closeDataFileAccessor(reader); 962 } 963 return rc; 964 } 965 966 public Location write(ByteSequence data, boolean sync) throws IOException, IllegalStateException { 967 Location loc = appender.storeItem(data, Location.USER_TYPE, sync); 968 return loc; 969 } 970 971 public Location write(ByteSequence data, Runnable onComplete) throws IOException, IllegalStateException { 972 Location loc = appender.storeItem(data, Location.USER_TYPE, onComplete); 973 return loc; 974 } 975 976 public void update(Location location, ByteSequence data, boolean sync) throws IOException { 977 DataFile dataFile = getDataFile(location); 978 DataFileAccessor updater = accessorPool.openDataFileAccessor(dataFile); 979 try { 980 updater.updateRecord(location, data, sync); 981 } finally { 982 accessorPool.closeDataFileAccessor(updater); 983 } 984 } 985 986 public PreallocationStrategy getPreallocationStrategy() { 987 return preallocationStrategy; 988 } 989 990 public void setPreallocationStrategy(PreallocationStrategy preallocationStrategy) { 991 this.preallocationStrategy = preallocationStrategy; 992 } 993 994 public PreallocationScope getPreallocationScope() { 995 return preallocationScope; 996 } 997 998 public void setPreallocationScope(PreallocationScope preallocationScope) { 999 this.preallocationScope = preallocationScope; 1000 } 1001 1002 public File getDirectory() { 1003 return directory; 1004 } 1005 1006 public void setDirectory(File directory) { 1007 this.directory = directory; 1008 } 1009 1010 public String getFilePrefix() { 1011 return filePrefix; 1012 } 1013 1014 public void setFilePrefix(String filePrefix) { 1015 this.filePrefix = filePrefix; 1016 } 1017 1018 public Map<WriteKey, WriteCommand> getInflightWrites() { 1019 return inflightWrites; 1020 } 1021 1022 public Location getLastAppendLocation() { 1023 return lastAppendLocation.get(); 1024 } 1025 1026 public void setLastAppendLocation(Location lastSyncedLocation) { 1027 this.lastAppendLocation.set(lastSyncedLocation); 1028 } 1029 1030 public File getDirectoryArchive() { 1031 if (!directoryArchiveOverridden && (directoryArchive == null)) { 1032 // create the directoryArchive relative to the journal location 1033 directoryArchive = new File(directory.getAbsolutePath() + 1034 File.separator + DEFAULT_ARCHIVE_DIRECTORY); 1035 } 1036 return directoryArchive; 1037 } 1038 1039 public void setDirectoryArchive(File directoryArchive) { 1040 directoryArchiveOverridden = true; 1041 this.directoryArchive = directoryArchive; 1042 } 1043 1044 public boolean isArchiveDataLogs() { 1045 return archiveDataLogs; 1046 } 1047 1048 public void setArchiveDataLogs(boolean archiveDataLogs) { 1049 this.archiveDataLogs = archiveDataLogs; 1050 } 1051 1052 public DataFile getDataFileById(int dataFileId) { 1053 synchronized (currentDataFile) { 1054 return fileMap.get(Integer.valueOf(dataFileId)); 1055 } 1056 } 1057 1058 public DataFile getCurrentDataFile(int capacity) throws IOException { 1059 //First just acquire the currentDataFile lock and return if no rotation needed 1060 synchronized (currentDataFile) { 1061 if (currentDataFile.get().getLength() + capacity < maxFileLength) { 1062 return currentDataFile.get(); 1063 } 1064 } 1065 1066 //AMQ-6545 - if rotation needed, acquire dataFileIdLock first to prevent deadlocks 1067 //then re-check if rotation is needed 1068 synchronized (dataFileIdLock) { 1069 synchronized (currentDataFile) { 1070 if (currentDataFile.get().getLength() + capacity >= maxFileLength) { 1071 rotateWriteFile(); 1072 } 1073 return currentDataFile.get(); 1074 } 1075 } 1076 } 1077 1078 public Integer getCurrentDataFileId() { 1079 synchronized (currentDataFile) { 1080 return currentDataFile.get().getDataFileId(); 1081 } 1082 } 1083 1084 /** 1085 * Get a set of files - only valid after start() 1086 * 1087 * @return files currently being used 1088 */ 1089 public Set<File> getFiles() { 1090 synchronized (currentDataFile) { 1091 return fileByFileMap.keySet(); 1092 } 1093 } 1094 1095 public Map<Integer, DataFile> getFileMap() { 1096 synchronized (currentDataFile) { 1097 return new TreeMap<Integer, DataFile>(fileMap); 1098 } 1099 } 1100 1101 public long getDiskSize() { 1102 return totalLength.get(); 1103 } 1104 1105 public void setReplicationTarget(ReplicationTarget replicationTarget) { 1106 this.replicationTarget = replicationTarget; 1107 } 1108 1109 public ReplicationTarget getReplicationTarget() { 1110 return replicationTarget; 1111 } 1112 1113 public String getFileSuffix() { 1114 return fileSuffix; 1115 } 1116 1117 public void setFileSuffix(String fileSuffix) { 1118 this.fileSuffix = fileSuffix; 1119 } 1120 1121 public boolean isChecksum() { 1122 return checksum; 1123 } 1124 1125 public void setChecksum(boolean checksumWrites) { 1126 this.checksum = checksumWrites; 1127 } 1128 1129 public boolean isCheckForCorruptionOnStartup() { 1130 return checkForCorruptionOnStartup; 1131 } 1132 1133 public void setCheckForCorruptionOnStartup(boolean checkForCorruptionOnStartup) { 1134 this.checkForCorruptionOnStartup = checkForCorruptionOnStartup; 1135 } 1136 1137 public void setWriteBatchSize(int writeBatchSize) { 1138 this.writeBatchSize = writeBatchSize; 1139 } 1140 1141 public int getWriteBatchSize() { 1142 return writeBatchSize; 1143 } 1144 1145 public void setSizeAccumulator(AtomicLong storeSizeAccumulator) { 1146 this.totalLength = storeSizeAccumulator; 1147 } 1148 1149 public void setEnableAsyncDiskSync(boolean val) { 1150 this.enableAsyncDiskSync = val; 1151 } 1152 1153 public boolean isEnableAsyncDiskSync() { 1154 return enableAsyncDiskSync; 1155 } 1156 1157 public JournalDiskSyncStrategy getJournalDiskSyncStrategy() { 1158 return journalDiskSyncStrategy; 1159 } 1160 1161 public void setJournalDiskSyncStrategy(JournalDiskSyncStrategy journalDiskSyncStrategy) { 1162 this.journalDiskSyncStrategy = journalDiskSyncStrategy; 1163 } 1164 1165 public boolean isJournalDiskSyncPeriodic() { 1166 return JournalDiskSyncStrategy.PERIODIC.equals(journalDiskSyncStrategy); 1167 } 1168 1169 public void setDataFileRemovedListener(DataFileRemovedListener dataFileRemovedListener) { 1170 this.dataFileRemovedListener = dataFileRemovedListener; 1171 } 1172 1173 public static class WriteCommand extends LinkedNode<WriteCommand> { 1174 public final Location location; 1175 public final ByteSequence data; 1176 final boolean sync; 1177 public final Runnable onComplete; 1178 1179 public WriteCommand(Location location, ByteSequence data, boolean sync) { 1180 this.location = location; 1181 this.data = data; 1182 this.sync = sync; 1183 this.onComplete = null; 1184 } 1185 1186 public WriteCommand(Location location, ByteSequence data, Runnable onComplete) { 1187 this.location = location; 1188 this.data = data; 1189 this.onComplete = onComplete; 1190 this.sync = false; 1191 } 1192 } 1193 1194 public static class WriteKey { 1195 private final int file; 1196 private final long offset; 1197 private final int hash; 1198 1199 public WriteKey(Location item) { 1200 file = item.getDataFileId(); 1201 offset = item.getOffset(); 1202 // TODO: see if we can build a better hash 1203 hash = (int)(file ^ offset); 1204 } 1205 1206 @Override 1207 public int hashCode() { 1208 return hash; 1209 } 1210 1211 @Override 1212 public boolean equals(Object obj) { 1213 if (obj instanceof WriteKey) { 1214 WriteKey di = (WriteKey)obj; 1215 return di.file == file && di.offset == offset; 1216 } 1217 return false; 1218 } 1219 } 1220}