/*
* Copyright 2014 WANdisco
*
* WANdisco 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 c5db.log;
import c5db.LogConstants;
import c5db.interfaces.log.SequentialEntry;
import c5db.interfaces.log.SequentialEntryCodec;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.channels.Channels;
import java.util.NavigableMap;
import java.util.TreeMap;
import static c5db.log.LogPersistenceService.BytePersistence;
import static c5db.log.LogPersistenceService.PersistenceNavigator;
import static c5db.log.LogPersistenceService.PersistenceReader;
import static c5db.log.SequentialLog.LogEntryNotFound;
/**
* PersistenceNavigator using only in-memory structures, not itself persisting any data it
* has been issued by notifyLogging(). This class keeps an internal Navigable map from entry sequence
* number to byte position. The strategy used is: when notifyLogging is called, if the entry
* sequence number is at least k greater than the greatest entry sequence number already stored,
* then store it. k is a configurable parameter, maxEntrySeek. Also, if requested to get the
* address of a specific entry, and that address is not already stored, store it once it is
* found.
*/
public class InMemoryPersistenceNavigator<E extends SequentialEntry> implements PersistenceNavigator {
private final BytePersistence persistence;
private final SequentialEntryCodec<E> codec;
private final NavigableMap<Long, Long> index = new TreeMap<>();
private final long fileOffset;
private int maxEntrySeek = LogConstants.LOG_NAVIGATOR_DEFAULT_MAX_ENTRY_SEEK;
public InMemoryPersistenceNavigator(BytePersistence persistence, SequentialEntryCodec<E> codec) {
this(persistence, codec, 0);
}
public InMemoryPersistenceNavigator(BytePersistence persistence, SequentialEntryCodec<E> codec, long offset) {
this.persistence = persistence;
this.codec = codec;
this.fileOffset = offset;
// Logic is simplified if the index NavigableMap is guaranteed to have at least one entry.
index.put(0L, 0L);
}
public void setMaxEntrySeek(int numberOfEntries) {
if (numberOfEntries < 1) {
throw new IllegalArgumentException("InMemoryPersistenceNavigator#setMaxEntrySeek");
}
maxEntrySeek = numberOfEntries;
}
@Override
public void notifyLogging(long seqNum, long byteAddress) throws IOException {
maybeAddToIndex(seqNum, byteAddress);
}
@Override
public void addToIndex(long seqNum, long address) {
index.put(seqNum, address);
}
@Override
public void notifyTruncation(long seqNum) throws IOException {
if (seqNum <= 0) {
throw new IllegalArgumentException("InMemoryPersistenceNavigator#notifyTruncation");
}
truncateIndex(seqNum);
}
@Override
public long getAddressOfEntry(long seqNum) throws IOException, LogEntryNotFound {
if (index.containsKey(seqNum)) {
return index.get(seqNum);
} else {
try (PersistenceReader reader = getReaderAtSeqNum(seqNum)) {
return reader.position();
}
}
}
@Override
public InputStream getStreamAtSeqNum(long seqNum) throws IOException, LogEntryNotFound {
return Channels.newInputStream(getReaderAtSeqNum(seqNum));
}
@Override
public InputStream getStreamAtFirstEntry() throws IOException {
PersistenceReader reader = persistence.getReader();
reader.position(fileOffset);
return Channels.newInputStream(reader);
}
@Override
public InputStream getStreamAtLastEntry() throws IOException {
long lastEntrySeqNum = lastIndexedSeqNum();
long lastEntryAddress = index.get(lastEntrySeqNum);
PersistenceReader reader = persistence.getReader();
reader.position(lastEntryAddress);
InputStream inputStream = Channels.newInputStream(reader);
try {
//noinspection InfiniteLoopStatement
while (true) {
long entryStartAddress = reader.position();
lastEntrySeqNum = codec.skipEntryAndReturnSeqNum(inputStream);
lastEntryAddress = entryStartAddress;
}
} catch (EOFException ignore) {
}
reader.position(lastEntryAddress);
addToIndex(lastEntrySeqNum, lastEntryAddress);
return inputStream;
}
private PersistenceReader getReaderAtSeqNum(long seqNum) throws IOException, LogEntryNotFound {
PersistenceReader reader = persistence.getReader();
if (index.containsKey(seqNum)) {
reader.position(index.get(seqNum));
return reader;
}
reader.position(nearestAddressTo(seqNum));
InputStream inputStream = Channels.newInputStream(reader);
try {
while (true) {
long entryStartAddress = reader.position();
long entrySeqNum = codec.skipEntryAndReturnSeqNum(inputStream);
if (seqNum == entrySeqNum) {
reader.position(entryStartAddress);
addToIndex(seqNum, entryStartAddress);
return reader;
}
}
} catch (EOFException e) {
throw new LogEntryNotFound("EOF reached before finding requested seqNum (" + seqNum + ")");
}
}
/**
* @return The greatest seqNum in the index, or 0 if no seqNum has ever been added to the index.
*/
private long lastIndexedSeqNum() {
return index.lastKey();
}
private void maybeAddToIndex(long seqNum, long address) {
if (seqNum - lastIndexedSeqNum() >= maxEntrySeek) {
index.put(seqNum, address);
}
}
private long nearestAddressTo(long seqNum) {
return index.floorEntry(seqNum).getValue();
}
private void truncateIndex(long seqNum) {
index.tailMap(seqNum, true).clear();
}
}