/*
* Copyright (c) 2013-2017 Cinchapi Inc.
*
* 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.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.cinchapi.concourse.server.storage.db;
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.util.Iterator;
import java.util.Map;
import com.cinchapi.concourse.server.io.Byteable;
import com.cinchapi.concourse.server.io.ByteableCollections;
import com.cinchapi.concourse.server.io.Composite;
import com.cinchapi.concourse.server.io.FileSystem;
import com.cinchapi.concourse.server.io.Syncable;
import com.cinchapi.concourse.util.ByteBuffers;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.collect.Maps;
/**
* A reference that stores the start and end position for sequences of bytes
* that relate to a key that is described by one or more {@link Byteable}
* objects. A BlockIndex is associated with each {@link Block} to determine
* where to look on disk for a particular {@code locator} or {@code locator}/
* {@code key} pair.
*
* @author Jeff Nelson
*/
public class BlockIndex implements Byteable, Syncable {
// CON-256: The BlockIndex does not need to perform locking for concurrency
// control since writes only happen from a single thread (the
// BufferTransportThread requesting a sync on the parent Block) and reads
// only happen when the parent Block has been synced and the BlockIndex is
// no longer mutable.
/**
* Return a newly created BlockIndex.
*
* @param file
* @param expectedInsertions
* @return the BlockIndex
*/
public static BlockIndex create(String file, int expectedInsertions) {
return new BlockIndex(file, expectedInsertions);
}
/**
* Return the BlockIndex that is stored in {@code file}.
*
* @param file
* @return the BlockIndex
*/
public static BlockIndex open(String file) {
return new BlockIndex(file);
}
/**
* Represents an entry that has not been recorded.
*/
public static final int NO_ENTRY = -1;
/**
* The entries contained in the index.
*/
private Map<Composite, Entry> entries;
/**
* The file where the BlockIndex is stored during an diskSync.
*/
private final String file;
/**
* A flag that indicates if this index is mutable. An index is no longer
* mutable after it has been synced.
*/
private boolean mutable;
/**
* The running size of the index in bytes.
*/
private transient int size = 0;
/**
* A {@link SoftReference} to the entries contained in the index that is
* used to reduce memory overhead.
*/
private SoftReference<Map<Composite, Entry>> softEntries;
/**
* Lazily construct an existing instance from the data in {@code file}.
*
* @param file
*/
public BlockIndex(String file) {
this.file = file;
this.mutable = false;
this.entries = null;
this.softEntries = null;
}
/**
* Construct a new instance.
*
* @param expectedInsertions
*/
private BlockIndex(String file, int expectedInsertions) {
this.file = file;
this.entries = Maps.newHashMapWithExpectedSize(expectedInsertions);
this.softEntries = null;
this.mutable = true;
}
@Override
public ByteBuffer getBytes() {
Preconditions.checkState(mutable);
ByteBuffer bytes = ByteBuffer.allocate(size());
copyTo(bytes);
bytes.rewind();
return bytes;
}
/**
* Return the end position for {@code byteables} if it exists, otherwise
* return {@link #NO_ENTRY}.
*
* @param byteables
* @return the end position
*/
public int getEnd(Byteable... byteables) {
Composite composite = Composite.create(byteables);
Entry entry = entries().get(composite);
if(entry != null) {
return entry.getEnd();
}
else {
return NO_ENTRY;
}
}
/**
* Return the start position for {@code byteables} if it exists, otherwise
* return {@code #NO_ENTRY}.
*
* @param byteables
* @return the start position
*/
public int getStart(Byteable... byteables) {
Composite composite = Composite.create(byteables);
Entry entry = entries().get(composite);
if(entry != null) {
return entry.getStart();
}
else {
return NO_ENTRY;
}
}
/**
* Record the end position for the {@code byteables}.
*
* @param end
* @param byteables
*/
public void putEnd(int end, Byteable... byteables) {
Preconditions.checkArgument(end >= 0,
"Cannot have negative index. Tried to put %s", end);
Preconditions.checkState(mutable);
Composite composite = Composite.create(byteables);
Entry entry = entries().get(composite);
Preconditions.checkState(entry != null,
"Cannot set the end position before setting "
+ "the start position. Tried to put %s", end);
entry.setEnd(end);
}
/**
* Record the start position for the {@code byteables}.
*
* @param start
* @param byteables
*/
public void putStart(int start, Byteable... byteables) {
Preconditions.checkArgument(start >= 0,
"Cannot have negative index. Tried to put %s", start);
Preconditions.checkState(mutable);
Composite composite = Composite.create(byteables);
Entry entry = entries().get(composite);
if(entry == null) {
entry = new Entry(composite);
entries.put(composite, entry);
size += entry.size() + 4;
}
entry.setStart(start);
}
@Override
public int size() {
return size;
}
@Override
public void sync() {
Preconditions.checkState(mutable);
FileChannel channel = FileSystem.getFileChannel(file);
try {
channel.write(getBytes());
channel.force(true);
softEntries = new SoftReference<Map<Composite, Entry>>(entries);
mutable = false;
entries = null;
}
catch (IOException e) {
throw Throwables.propagate(e);
}
finally {
FileSystem.closeFileChannel(channel); // CON-162
}
}
@Override
public void copyTo(ByteBuffer buffer) {
Preconditions.checkState(mutable);
for (Entry entry : entries.values()) {
buffer.putInt(entry.size());
entry.copyTo(buffer);
}
}
/**
* Return {@code true} if this index is considered <em>loaded</em> meaning
* all of its entries are available in memory.
*
* @return {@code true} if the entries are loaded
*/
protected boolean isLoaded() { // visible for testing
return mutable || (softEntries != null && softEntries.get() != null);
}
/**
* Return the entries in this index. This method will lazily load the
* entries on demand if they do not currently exist in memory.
*
* @return the entries
*/
private synchronized Map<Composite, Entry> entries() {
if(mutable && entries != null) {
return entries;
}
else if(!mutable && (softEntries == null || softEntries.get() == null)) { // do
// lazy
// load
ByteBuffer bytes = FileSystem.map(file, MapMode.READ_ONLY, 0,
FileSystem.getFileSize(file));
Iterator<ByteBuffer> it = ByteableCollections.iterator(bytes);
Map<Composite, Entry> entries = Maps
.newHashMapWithExpectedSize(bytes.capacity()
/ Entry.CONSTANT_SIZE);
while (it.hasNext()) {
Entry entry = new Entry(it.next());
entries.put(entry.getKey(), entry);
}
softEntries = new SoftReference<Map<Composite, Entry>>(entries);
return softEntries.get();
}
else if(!mutable && softEntries.get() != null) {
return softEntries.get();
}
else {
// "If i'm really an engineer thats worth a damn, we won't ever get
// to this point" -jnelson
throw new IllegalStateException();
}
}
/**
* Represents a single entry in the Index.
*
* @author Jeff Nelson
*/
private final class Entry implements Byteable {
private static final int CONSTANT_SIZE = 8; // start(4), end(4)
private int end = NO_ENTRY;
private final Composite key;
private int start = NO_ENTRY;
/**
* Construct an instance that represents an existing Entry from a
* ByteBuffer. This constructor is public so as to comply with the
* {@link Byteable} interface. Calling this constructor directly is not
* recommend. Use {@link #fromByteBuffer(ByteBuffer)} instead to take
* advantage of reference caching.
*
* @param bytes
*/
public Entry(ByteBuffer bytes) {
this.start = bytes.getInt();
this.end = bytes.getInt();
this.key = Composite.fromByteBuffer(ByteBuffers.get(bytes,
bytes.remaining()));
}
/**
* Construct a new instance.
*
* @param key
*/
public Entry(Composite key) {
this.key = key;
}
@Override
public ByteBuffer getBytes() {
ByteBuffer bytes = ByteBuffer.allocate(size());
copyTo(bytes);
bytes.rewind();
return bytes;
}
/**
* Return the end position.
*
* @return the end
*/
public int getEnd() {
return end;
}
/**
* Return the entry key
*
* @return the key
*/
public Composite getKey() {
return key;
}
/**
* Return the start position.
*
* @return the start
*/
public int getStart() {
return start;
}
/**
* Set the end position.
*
* @param end the end to set
*/
public void setEnd(int end) {
this.end = end;
}
/**
* Set the start position.
*
* @param start the start to set
*/
public void setStart(int start) {
this.start = start;
}
@Override
public int size() {
return CONSTANT_SIZE + key.size();
}
@Override
public void copyTo(ByteBuffer buffer) {
buffer.putInt(start);
buffer.putInt(end);
key.copyTo(buffer);
}
}
}