/** * Copyright 2016 LinkedIn Corp. All rights reserved. * * Licensed 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. */ package com.github.ambry.store; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentNavigableMap; import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.atomic.AtomicInteger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; class JournalEntry { private final Offset offset; private final StoreKey key; JournalEntry(Offset offset, StoreKey key) { this.offset = offset; this.key = key; } Offset getOffset() { return offset; } StoreKey getKey() { return key; } } /** * An in memory journal used to track the most recent blobs for a store. */ class Journal { private final ConcurrentSkipListMap<Offset, StoreKey> journal; private final ConcurrentHashMap<StoreKey, Long> recentCrcs; private final int maxEntriesToJournal; private final int maxEntriesToReturn; private final AtomicInteger currentNumberOfEntries; private final String dataDir; private final Logger logger = LoggerFactory.getLogger(getClass()); /** * The journal that holds the most recent entries in a store sorted by offset of the blob on disk * @param maxEntriesToJournal The max number of entries to journal. The oldest entry will be removed from * the journal after the size is reached. * @param maxEntriesToReturn The max number of entries to return from the journal when queried for entries. */ Journal(String dataDir, int maxEntriesToJournal, int maxEntriesToReturn) { journal = new ConcurrentSkipListMap<>(); recentCrcs = new ConcurrentHashMap<>(); this.maxEntriesToJournal = maxEntriesToJournal; this.maxEntriesToReturn = maxEntriesToReturn; this.currentNumberOfEntries = new AtomicInteger(0); this.dataDir = dataDir; } /** * Adds an entry into the journal with the given {@link Offset}, {@link StoreKey}, and crc. * @param offset The {@link Offset} that the key pertains to. * @param key The key that the entry in the journal refers to. * @param crc The crc of the object. This may be null if crc is not available. */ void addEntry(Offset offset, StoreKey key, Long crc) { if (key == null || offset == null) { throw new IllegalArgumentException("Invalid arguments passed to add to the journal"); } if (maxEntriesToJournal > 0) { if (currentNumberOfEntries.get() == maxEntriesToJournal) { Map.Entry<Offset, StoreKey> earliestEntry = journal.firstEntry(); journal.remove(earliestEntry.getKey()); recentCrcs.remove(earliestEntry.getValue()); currentNumberOfEntries.decrementAndGet(); } journal.put(offset, key); if (crc != null) { recentCrcs.put(key, crc); } logger.trace("Journal : " + dataDir + " offset " + offset + " key " + key); currentNumberOfEntries.incrementAndGet(); logger.trace("Journal : " + dataDir + " number of entries " + currentNumberOfEntries.get()); } } /** * Adds an entry into the journal with the given {@link Offset}, {@link StoreKey}, and a null crc. * @param offset The {@link Offset} that the key pertains to. * @param key The key that the entry in the journal refers to. */ void addEntry(Offset offset, StoreKey key) { addEntry(offset, key, null); } /** * Gets all the entries from the journal starting at the provided offset and till the maxEntriesToReturn or the * end of the journal is reached. * @param offset The {@link Offset} from where the journal needs to return entries. * @param inclusive if {@code true}, the returned entries (if not {@code null}), contain the entry at {@code offset}. * @return The entries in the journal starting from offset. If the offset is outside the range of the journal, * it returns null. */ List<JournalEntry> getEntriesSince(Offset offset, boolean inclusive) { // To prevent synchronizing the addEntry method, we first get all the entries from the journal that are greater // than offset. Once we have all the required entries, we finally check if the offset is actually present // in the journal. If the offset is not present we return null, else we return the entries we got in the first step. // The offset may not be present in the journal as it could be removed. Map.Entry<Offset, StoreKey> first = journal.firstEntry(); Map.Entry<Offset, StoreKey> last = journal.lastEntry(); // check if the journal contains the offset. if (first == null || offset.compareTo(first.getKey()) < 0 || last == null || offset.compareTo(last.getKey()) > 0 || !journal.containsKey(offset)) { return null; } ConcurrentNavigableMap<Offset, StoreKey> subsetMap = journal.tailMap(offset, true); int entriesToReturn = Math.min(subsetMap.size(), maxEntriesToReturn); List<JournalEntry> journalEntries = new ArrayList<JournalEntry>(entriesToReturn); int entriesAdded = 0; for (Map.Entry<Offset, StoreKey> entry : subsetMap.entrySet()) { if (inclusive || !entry.getKey().equals(offset)) { journalEntries.add(new JournalEntry(entry.getKey(), entry.getValue())); entriesAdded++; if (entriesAdded == entriesToReturn) { break; } } } // Ensure that the offset was not pushed out of the journal. first = journal.firstEntry(); if (first == null || offset.compareTo(first.getKey()) < 0) { return null; } logger.trace("Journal : " + dataDir + " entries returned " + journalEntries.size()); return journalEntries; } /** * @return the first/smallest offset in the journal or {@code null} if no such entry exists. */ Offset getFirstOffset() { Map.Entry<Offset, StoreKey> first = journal.firstEntry(); return first == null ? null : first.getKey(); } /** * @return the last/greatest offset in the journal or {@code null} if no such entry exists. */ Offset getLastOffset() { Map.Entry<Offset, StoreKey> last = journal.lastEntry(); return last == null ? null : last.getKey(); } /** * Returns the crc associated with this key in the journal if there is one; else returns null. * @param key the key for which the crc is to be obtained. * @return the crc associated with this key in the journal if there is one; else returns null. */ Long getCrcOfKey(StoreKey key) { return recentCrcs.get(key); } }