/*
* 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;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel.MapMode;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.locks.StampedLock;
import javax.annotation.concurrent.ThreadSafe;
import com.cinchapi.concourse.server.io.FileSystem;
import com.cinchapi.concourse.util.Integers;
import com.cinchapi.concourse.util.LongBitSet;
import com.google.common.collect.Lists;
/**
* The {@link Inventory} is a persistent collection of longs that represents a
* listing of all the records that exist within an environment (e.g. have data
* or a history of data).
*
* @author Jeff Nelson
*/
@ThreadSafe
public class Inventory {
/**
* Return a new {@link Inventory} that is persisted to the
* {@code backingStore} in calls to {@link #sync()}.
*
* @param backingStore
* @return the Inventory
*/
public static Inventory create(String backingStore) {
return new Inventory(backingStore);
}
/**
* The amount of memory and disk space allocated each time we need to extend
* the backing store. This value is rounded up to the nearest power of two
* for the most efficient I/O.
*/
private static final int MEMORY_MAPPING_SIZE = 8 * 20000;
/**
* The location where the inventory is stored on disk.
*/
private final String backingStore;
/**
* The bitset that contains the read-efficient version of the data in the
* inventory.
*/
private final LongBitSet bitSet;
/**
* A memory mapped buffer that is used to handle writes to the backing
* store.
*/
private MappedByteBuffer content;
/**
* A collection of dirty writes that have not been synced to disk yet.
*/
protected final transient List<Long> dirty = Lists
.newArrayListWithExpectedSize(1); // visible for testing
/**
* Concurrency control
*/
private final StampedLock lock = new StampedLock();
/**
* Construct a new instance. If the {@code backingStore} has data then it
* will be read into memory.
*
* @param backingStore
*/
private Inventory(String backingStore) {
this.backingStore = backingStore;
this.bitSet = LongBitSet.create();
this.content = FileSystem.map(backingStore, MapMode.READ_ONLY, 0,
FileSystem.getFileSize(backingStore));
while (content.position() < content.capacity()) {
long record = content.getLong();
if(record == 0 && content.getLong() == 0) { // if there is a null
// (0) record, check to
// see if the next one
// is also null which
// will tell us we've
// read too far
content.position(content.position() - 16);
break;
}
else {
bitSet.set(record);
}
}
map0(content.position(), MEMORY_MAPPING_SIZE);
}
/**
* Add an {@code record} to the inventory.
*
* @param record
*/
public void add(long record) {
long stamp = lock.writeLock();
try {
if(bitSet.set(record)) {
dirty.add(record);
}
}
finally {
lock.unlockWrite(stamp);
}
}
/**
* Return {@code true} if the {@code record} exists within the inventory.
*
* @param record
* @return {@code true} if the record is contained
*/
public boolean contains(long record) {
long stamp = lock.tryOptimisticRead();
boolean result = bitSet.get(record);
if(lock.validate(stamp)) {
return result;
}
else {
stamp = lock.readLock();
try {
return bitSet.get(record);
}
finally {
lock.unlockRead(stamp);
}
}
}
/**
* Return {@code Set<Long>} if records that ever had data exist.
*
* @return {@code Set<Long>}
*/
public Set<Long> getAll() {
return (Set<Long>) bitSet.toIterable();
}
/**
* Perform an fsync and flush any dirty writes to disk.
*/
public void sync() {
long stamp = lock.writeLock();
try {
if(!dirty.isEmpty()) {
Iterator<Long> it = dirty.iterator();
while (it.hasNext()) {
if(content.remaining() < 8) {
map0(content.position(), content.position() * 2);
}
content.putLong(it.next());
}
content.force();
dirty.clear();
}
}
finally {
lock.unlockWrite(stamp);
}
}
/**
* Map the {@link #backingStore} in memory for the specified
* {@code position} for at least {@code length} bytes.
*
* @param position
* @param length
*/
private void map0(long position, int length) {
FileSystem.unmap(content);
content = FileSystem.map(backingStore, MapMode.READ_WRITE, position,
Integers.nextPowerOfTwo(length));
}
}