/*
* 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.io.File;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import javax.annotation.concurrent.ThreadSafe;
import com.cinchapi.concourse.annotate.Authorized;
import com.cinchapi.concourse.annotate.DoNotInvoke;
import com.cinchapi.concourse.annotate.Restricted;
import com.cinchapi.concourse.server.GlobalState;
import com.cinchapi.concourse.server.concurrent.LockService;
import com.cinchapi.concourse.server.concurrent.PriorityReadWriteLock;
import com.cinchapi.concourse.server.concurrent.RangeLockService;
import com.cinchapi.concourse.server.concurrent.RangeToken;
import com.cinchapi.concourse.server.concurrent.RangeTokens;
import com.cinchapi.concourse.server.concurrent.Token;
import com.cinchapi.concourse.server.io.FileSystem;
import com.cinchapi.concourse.server.jmx.ManagedOperation;
import com.cinchapi.concourse.server.model.Text;
import com.cinchapi.concourse.server.model.Value;
import com.cinchapi.concourse.server.storage.db.Database;
import com.cinchapi.concourse.server.storage.temp.Buffer;
import com.cinchapi.concourse.server.storage.temp.Write;
import com.cinchapi.concourse.thrift.Operator;
import com.cinchapi.concourse.thrift.TObject;
import com.cinchapi.concourse.time.Time;
import com.cinchapi.concourse.util.Logger;
import com.cinchapi.concourse.util.Strings;
import com.google.common.base.MoreObjects;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.Maps;
import com.google.common.collect.Range;
import com.google.common.collect.RangeSet;
import com.google.common.collect.TreeRangeSet;
import static com.google.common.base.Preconditions.*;
/**
* The {@code Engine} schedules concurrent CRUD operations, manages ACID
* transactions, versions writes and indexes data.
* <p>
* The Engine is a {@link BufferedStore}. Writing to the {@link Database} is
* expensive because multiple index records must be deserialized, updated and
* flushed back to disk for each revision. By using a {@link Buffer}, the Engine
* can handle writes in a more efficient manner with minimal impact on Read
* performance. The buffering system provides full CD guarantees.
* </p>
*
* @author Jeff Nelson
*/
@ThreadSafe
public final class Engine extends BufferedStore implements
TransactionSupport,
AtomicSupport,
InventoryTracker {
//
// NOTES ON LOCKING:
// =================
// Even though the individual storage components (Block, Record, etc)
// handle their own locking, we must also grab "global" coordinating locks
// in the Engine
//
// 1) to account for the fact that an atomic operation may lock notions of
// things to create a virtual fence that ensures the atomicity of its
// reads and writes, and
//
// 2) BufferedStore operations query the buffer and destination separately
// and the individual locking protocols of those stores are not sufficient
// to prevent dropped data (i.e. while reading from the destination it is
// okay to continue writing to the buffer since we'll lock there when its
// time BUT, between the time that we unlock in the #destination and the
// time when we lock in the #buffer, it is possible for a transport from the
// #buffer to the #destination to occur in which case the data would be
// dropped since it wasn't read during the #destination query and won't be
// read during the #buffer scan).
//
// It is important to note that we DO NOT need to do any locking for
// historical reads because concurrent data writes cannot affect what is
// returned.
/**
* The id used to determine that the Buffer should be dumped in the
* {@link #dump(String)} method.
*/
public static final String BUFFER_DUMP_ID = "BUFFER";
/**
* The number of milliseconds we allow between writes before pausing the
* {@link BufferTransportThread}. If the amount of time between writes is
* less than this value then we assume we are streaming writes, which means
* it is more efficient for the BufferTransportThread to busy-wait than
* block.
*/
protected static final int BUFFER_TRANSPORT_THREAD_ALLOWABLE_INACTIVITY_THRESHOLD_IN_MILLISECONDS = 1000; // visible
// for
// testing
/**
* The frequency with which we check to see if the
* {@link BufferTransportThread} has hung/stalled.
*/
protected static int BUFFER_TRANSPORT_THREAD_HUNG_DETECTION_FREQUENCY_IN_MILLISECONDS = 10000; // visible
// for
// testing
/**
* The number of milliseconds we allow the {@link BufferTransportThread} to
* sleep without waking up (e.g. being in the TIMED_WAITING) state before we
* assume that the thread has hung/stalled and we try to rescue it.
*/
protected static int BUFFER_TRANSPORT_THREAD_HUNG_DETECTION_THRESOLD_IN_MILLISECONDS = 5000; // visible
// for
// testing
/**
* A flag to indicate that the {@link BufferTransportThrread} has appeared
* to be hung at some point during the current runtime.
*/
protected final AtomicBoolean bufferTransportThreadHasEverAppearedHung = new AtomicBoolean(
false); // visible for testing
/**
* A flag to indicate that the {@link BufferTransportThread} has ever been
* successfully restarted after appearing to be hung during the current
* runtime.
*/
protected final AtomicBoolean bufferTransportThreadHasEverBeenRestarted = new AtomicBoolean(
false); // visible for testing
/**
* A flag to indicate that the {@link BufferTransportThread} has, at least
* once, gone into "paused" mode where it blocks during inactivity instead
* of busy waiting.
*/
protected final AtomicBoolean bufferTransportThreadHasEverPaused = new AtomicBoolean(
false); // visible for testing
/**
* If this value is > 0, then we will sleep for this amount instead of what
* the buffer suggests. This is mainly used for testing.
*/
protected int bufferTransportThreadSleepInMs = 0; // visible for testing
/**
* The inventory contains a collection of all the records that have ever
* been created. The Engine and its Buffer share access to this inventory so
* that the Buffer can update it whenever a new record is written. The
* Engine uses the inventory to make some reads (i.e. verify) more
* efficient.
*/
protected final Inventory inventory; // visible for testing
/**
* The location where transaction backups are stored.
*/
protected final String transactionStore; // exposed for Transaction backup
/**
* The thread that is responsible for transporting buffer content in the
* background.
*/
private final Thread bufferTransportThread; // NOTE: Having a dedicated
// thread that sleeps is faster
// than using an
// ExecutorService.
/**
* A flag that indicates whether the {@link BufferTransportThread} is
* actively doing work at the moment. This flag is necessary so we don't
* interrupt the thread if it appears to be hung when it is actually just
* busy doing a lot of work.
*/
private final AtomicBoolean bufferTransportThreadIsDoingWork = new AtomicBoolean(
false);
/**
* A flag that indicates that the {@link BufferTransportThread} is currently
* paused due to inactivity (e.g. no writes).
*/
private final AtomicBoolean bufferTransportThreadIsPaused = new AtomicBoolean(
false);
/**
* The timestamp when the {@link BufferTransportThread} last awoke from
* sleep. We use this to help monitor and detect whether the thread has
* stalled/hung.
*/
private final AtomicLong bufferTransportThreadLastWakeUp = new AtomicLong(
Time.now());
/**
* The environment that is associated with this {@link Engine}.
*/
private final String environment;
/**
* A collection of listeners that should be notified of a version change for
* a given range token.
*/
private final Cache<VersionChangeListener, Map<Text, RangeSet<Value>>> rangeVersionChangeListeners = CacheBuilder
.newBuilder().weakKeys().build();
/**
* A flag to indicate if the Engine is running or not.
*/
private volatile boolean running = false;
/**
* A {@link Timer} that is used to schedule some regular tasks.
*/
private final Timer scheduler = new Timer(true);
/**
* A lock that prevents the Engine from causing the Buffer to transport
* Writes to the Database while a buffered read is occurring. Even though
* the Buffer has a transportLock, we must also maintain one at the Engine
* level to prevent the appearance of dropped writes where data is
* transported from the Buffer to the Database after the Database context is
* captured and sent to the Buffer to finish the buffered reading.
*/
private final ReentrantReadWriteLock transportLock = PriorityReadWriteLock
.prioritizeReads();
/**
* A collection of listeners that should be notified of a version change for
* a given token.
*/
private final ConcurrentMap<Token, WeakHashMap<VersionChangeListener, Boolean>> versionChangeListeners = new ConcurrentHashMap<Token, WeakHashMap<VersionChangeListener, Boolean>>();
/**
* Construct an Engine that is made up of a {@link Buffer} and
* {@link Database} in the default locations.
*
*/
public Engine() {
this(new Buffer(), new Database(), GlobalState.DEFAULT_ENVIRONMENT);
}
/**
* Construct an Engine that is made up of a {@link Buffer} and
* {@link Database} that are both backed by {@code bufferStore} and
* {@code dbStore} respectively.
*
* @param bufferStore
* @param dbStore
*/
public Engine(String bufferStore, String dbStore) {
this(bufferStore, dbStore, GlobalState.DEFAULT_ENVIRONMENT);
}
/**
* Construct an Engine that is made up of a {@link Buffer} and
* {@link Database} that are both backed by {@code bufferStore} and
* {@code dbStore} respectively} and are associated with {@code environment}
* .
*
* @param bufferStore
* @param dbStore
* @param environment
*/
public Engine(String bufferStore, String dbStore, String environment) {
this(new Buffer(bufferStore), new Database(dbStore), environment);
}
/**
* Construct an Engine that is made up of {@code buffer} and
* {@code database}.
*
* @param buffer
* @param database
* @param environment
*/
@Authorized
private Engine(Buffer buffer, Database database, String environment) {
super(buffer, database, LockService.create(), RangeLockService.create());
this.environment = environment;
this.bufferTransportThread = new BufferTransportThread();
this.transactionStore = buffer.getBackingStore() + File.separator
+ "txn"; /* (authorized) */
this.inventory = Inventory.create(buffer.getBackingStore()
+ File.separator + "meta" + File.separator + "inventory");
buffer.setInventory(inventory);
buffer.setThreadNamePrefix(environment + "-buffer");
buffer.setEnvironment(environment);
}
@Override
@DoNotInvoke
public void accept(Write write) {
accept(write, true);
}
/**
* <p>
* The Engine is the destination for Transaction commits, which means that
* this method will accept Writes from Transactions and create new Writes
* within the Engine BufferedStore (e.g. a new Write will be created in the
* Buffer and eventually transported to the Database). Creating a new Write
* does associate a new timestamp with the transactional data, but this is
* the desired behaviour because data from a Transaction should always have
* a post commit timestamp.
* </p>
* <p>
* It is also worth calling out the fact that this method does not have any
* locks to prevent multiple transactions from concurrently invoking meaning
* that two transactions that don't have any overlapping reads/writes
* (meaning they don't touch any of the same data) can commit at the same
* time and its possible that their writes will be interleaved since calls
* to this method don't globally lock. This is <em>okay</em> and does not
* violate ACID because the <strong>observed</strong> state of the system
* will be the same as if the transactions transported all their Writes
* serially.
* </p>
*/
@Override
@DoNotInvoke
public void accept(Write write, boolean sync) {
checkArgument(write.getType() != Action.COMPARE);
String key = write.getKey().toString();
TObject value = write.getValue().getTObject();
long record = write.getRecord().longValue();
boolean accepted = write.getType() == Action.ADD ? addUnsafe(key,
value, record, sync) : removeUnsafe(key, value, record, sync);
if(!accepted) {
Logger.warn("Write {} was rejected by the Engine "
+ "because it was previously accepted "
+ "but not offset. This implies that a "
+ "premature shutdown occurred and the parent"
+ "Transaction is attempting to restore "
+ "itself from backup and finish committing.", write);
}
else {
Logger.debug("'{}' was accepted by the Engine", write);
}
}
@Override
public Set<Long> getAllRecords() {
return inventory.getAll();
}
@Override
public boolean add(String key, TObject value, long record) {
Token sharedToken = Token.wrap(record);
Token writeToken = Token.wrap(key, record);
RangeToken rangeToken = RangeToken.forWriting(Text.wrap(key),
Value.wrap(value));
Lock shared = lockService.getWriteLock(sharedToken);
Lock write = lockService.getWriteLock(writeToken);
Lock range = rangeLockService.getWriteLock(rangeToken);
shared.lock();
write.lock();
range.lock();
try {
return addUnsafe(key, value, record, true, sharedToken, writeToken,
rangeToken);
}
finally {
shared.unlock();
write.unlock();
range.unlock();
}
}
@Override
@Restricted
public void addVersionChangeListener(Token token,
VersionChangeListener listener) {
if(token instanceof RangeToken) {
Iterable<Range<Value>> ranges = RangeTokens
.convertToRange((RangeToken) token);
for (Range<Value> range : ranges) {
Map<Text, RangeSet<Value>> map = rangeVersionChangeListeners
.getIfPresent(listener);
if(map == null) {
map = Maps.newHashMap();
rangeVersionChangeListeners.put(listener, map);
}
RangeSet<Value> set = map.get(((RangeToken) token).getKey());
if(set == null) {
set = TreeRangeSet.create();
map.put(((RangeToken) token).getKey(), set);
}
set.add(range);
}
}
else {
WeakHashMap<VersionChangeListener, Boolean> existing = versionChangeListeners
.get(token);
if(existing == null) {
WeakHashMap<VersionChangeListener, Boolean> created = new WeakHashMap<VersionChangeListener, Boolean>();
existing = versionChangeListeners.putIfAbsent(token, created);
existing = MoreObjects.firstNonNull(existing, created);
}
synchronized (existing) {
existing.put(listener, Boolean.TRUE);
}
}
}
@Override
public Map<Long, String> audit(long record) {
transportLock.readLock().lock();
Lock read = lockService.getReadLock(record);
read.lock();
try {
return super.audit(record);
}
finally {
read.unlock();
transportLock.readLock().unlock();
}
}
@Override
public Map<Long, String> audit(String key, long record) {
transportLock.readLock().lock();
Lock read = lockService.getReadLock(key, record);
read.lock();
try {
return super.audit(key, record);
}
finally {
read.unlock();
transportLock.readLock().unlock();
}
}
@Override
public Map<Long, String> auditUnsafe(long record) {
transportLock.readLock().lock();
try {
return super.audit(record);
}
finally {
transportLock.readLock().unlock();
}
}
@Override
public Map<Long, String> auditUnsafe(String key, long record) {
transportLock.readLock().lock();
try {
return super.audit(key, record);
}
finally {
transportLock.readLock().unlock();
}
}
@Override
public Map<TObject, Set<Long>> browse(String key) {
transportLock.readLock().lock();
Lock range = rangeLockService.getReadLock(Text.wrapCached(key),
Operator.BETWEEN, Value.NEGATIVE_INFINITY,
Value.POSITIVE_INFINITY);
range.lock();
try {
return super.browse(key);
}
finally {
range.unlock();
transportLock.readLock().unlock();
}
}
@Override
public Map<TObject, Set<Long>> browse(String key, long timestamp) {
transportLock.readLock().lock();
try {
return super.browse(key, timestamp);
}
finally {
transportLock.readLock().unlock();
}
}
@Override
public Map<String, Set<TObject>> browseUnsafe(long record) {
transportLock.readLock().lock();
try {
return super.select(record);
}
finally {
transportLock.readLock().unlock();
}
}
@Override
public Map<TObject, Set<Long>> browseUnsafe(String key) {
transportLock.readLock().lock();
try {
return super.browse(key);
}
finally {
transportLock.readLock().unlock();
}
}
@Override
public Map<Long, Set<TObject>> chronologize(String key, long record,
long start, long end) {
transportLock.readLock().lock();
Lock read = lockService.getReadLock(record);
read.lock();
try {
return super.chronologize(key, record, start, end);
}
finally {
read.unlock();
transportLock.readLock().unlock();
}
}
@Override
public Map<Long, Set<TObject>> chronologizeUnsafe(String key, long record,
long start, long end) {
transportLock.readLock().lock();
try {
return super.chronologize(key, record, start, end);
}
finally {
transportLock.readLock().unlock();
}
}
@Override
public boolean contains(long record) {
return inventory.contains(record);
}
@Override
public Map<Long, Set<TObject>> doExploreUnsafe(String key,
Operator operator, TObject... values) {
transportLock.readLock().lock();
try {
return super.doExplore(key, operator, values);
}
finally {
transportLock.readLock().unlock();
}
}
/**
* Public interface for the {@link Database#dump(String)} method.
*
* @param id
* @return the block dumps
*/
@ManagedOperation
public String dump(String id) {
if(id.equalsIgnoreCase(BUFFER_DUMP_ID)) {
return ((Buffer) buffer).dump();
}
return ((Database) destination).dump(id);
}
/**
* Public interface for the {@link Database#getDumpList()} method.
*
* @return the dump list
*/
@ManagedOperation
public String getDumpList() {
List<String> ids = ((Database) destination).getDumpList();
ids.add("BUFFER");
ListIterator<String> it = ids.listIterator(ids.size());
StringBuilder sb = new StringBuilder();
while (it.hasPrevious()) {
sb.append(Math.abs(it.previousIndex() - ids.size()));
sb.append(") ");
sb.append(it.previous());
sb.append(System.getProperty("line.separator"));
}
return sb.toString();
}
@Override
public Inventory getInventory() {
return inventory;
}
@Override
@Restricted
public void notifyVersionChange(Token token) {
if(token instanceof RangeToken) {
Iterable<Range<Value>> ranges = RangeTokens
.convertToRange((RangeToken) token);
for (Entry<VersionChangeListener, Map<Text, RangeSet<Value>>> entry : rangeVersionChangeListeners
.asMap().entrySet()) {
VersionChangeListener listener = entry.getKey();
RangeSet<Value> set = entry.getValue().get(
((RangeToken) token).getKey());
for (Range<Value> range : ranges) {
if(set != null && !set.subRangeSet(range).isEmpty()) {
listener.onVersionChange(token);
}
}
}
}
else {
WeakHashMap<VersionChangeListener, Boolean> existing = versionChangeListeners
.get(token);
if(existing != null) {
synchronized (existing) {
Iterator<VersionChangeListener> it = existing.keySet()
.iterator();
while (it.hasNext()) {
VersionChangeListener listener = it.next();
listener.onVersionChange(token);
it.remove();
}
}
}
}
}
@Override
public boolean remove(String key, TObject value, long record) {
Token sharedToken = Token.wrap(record);
Token writeToken = Token.wrap(key, record);
RangeToken rangeToken = RangeToken.forWriting(Text.wrap(key),
Value.wrap(value));
Lock shared = lockService.getWriteLock(sharedToken);
Lock write = lockService.getWriteLock(writeToken);
Lock range = rangeLockService.getWriteLock(rangeToken);
shared.lock();
write.lock();
range.lock();
try {
return removeUnsafe(key, value, record, true, sharedToken,
writeToken, rangeToken);
}
finally {
shared.unlock();
write.unlock();
range.unlock();
}
}
@Override
@Restricted
public void removeVersionChangeListener(Token token,
VersionChangeListener listener) {
// NOTE: Since we use weak references listeners, we don't have to do
// manual cleanup because the GC will take care of it.
}
@Override
public Set<Long> search(String key, String query) {
// NOTE: Range locking for a search query requires too much overhead, so
// we must be willing to live with the fact that a search query may
// provide inconsistent results if a match is added while the read is
// processing.
transportLock.readLock().lock();
try {
return super.search(key, query);
}
finally {
transportLock.readLock().unlock();
}
}
@Override
public Map<String, Set<TObject>> select(long record) {
transportLock.readLock().lock();
Lock read = lockService.getReadLock(record);
read.lock();
try {
return super.select(record);
}
finally {
read.unlock();
transportLock.readLock().unlock();
}
}
@Override
public Map<String, Set<TObject>> select(long record, long timestamp) {
transportLock.readLock().lock();
try {
return super.select(record, timestamp);
}
finally {
transportLock.readLock().unlock();
}
}
@Override
public Set<TObject> select(String key, long record) {
transportLock.readLock().lock();
Lock read = lockService.getReadLock(key, record);
read.lock();
try {
return super.select(key, record);
}
finally {
read.unlock();
transportLock.readLock().unlock();
}
}
@Override
public Set<TObject> select(String key, long record, long timestamp) {
transportLock.readLock().lock();
try {
return super.select(key, record, timestamp);
}
finally {
transportLock.readLock().unlock();
}
}
@Override
public Set<TObject> selectUnsafe(String key, long record) {
transportLock.readLock().lock();
try {
return super.select(key, record);
}
finally {
transportLock.readLock().unlock();
}
}
@Override
public void set(String key, TObject value, long record) {
Token sharedToken = Token.wrap(record);
Token writeToken = Token.wrap(key, record);
RangeToken rangeToken = RangeToken.forWriting(Text.wrap(key),
Value.wrap(value));
Lock shared = lockService.getWriteLock(sharedToken);
Lock write = lockService.getWriteLock(writeToken);
Lock range = rangeLockService.getWriteLock(rangeToken);
shared.lock();
write.lock();
range.lock();
try {
super.set(key, value, record);
notifyVersionChange(writeToken);
notifyVersionChange(sharedToken);
notifyVersionChange(rangeToken);
}
finally {
shared.unlock();
write.unlock();
range.unlock();
}
}
@Override
public void start() {
if(!running) {
Logger.info("Starting the '{}' Engine...", environment);
running = true;
destination.start();
buffer.start();
doTransactionRecovery();
scheduler.scheduleAtFixedRate(
new TimerTask() {
@Override
public void run() {
if(!bufferTransportThreadIsDoingWork.get()
&& !bufferTransportThreadIsPaused.get()
&& bufferTransportThreadLastWakeUp.get() != 0
&& TimeUnit.MILLISECONDS
.convert(
Time.now()
- bufferTransportThreadLastWakeUp
.get(),
TimeUnit.MICROSECONDS) > BUFFER_TRANSPORT_THREAD_HUNG_DETECTION_THRESOLD_IN_MILLISECONDS) {
bufferTransportThreadHasEverAppearedHung
.set(true);
bufferTransportThread.interrupt();
}
}
},
BUFFER_TRANSPORT_THREAD_HUNG_DETECTION_FREQUENCY_IN_MILLISECONDS,
BUFFER_TRANSPORT_THREAD_HUNG_DETECTION_FREQUENCY_IN_MILLISECONDS);
bufferTransportThread.start();
}
}
@Override
public AtomicOperation startAtomicOperation() {
return AtomicOperation.start(this);
}
@Override
public Transaction startTransaction() {
return Transaction.start(this);
}
@Override
public void stop() {
if(running) {
running = false;
scheduler.cancel();
buffer.stop();
bufferTransportThread.interrupt();
destination.stop();
lockService.shutdown();
rangeLockService.shutdown();
}
}
@Override
public void sync() {
buffer.sync();
}
@Override
public boolean verify(String key, TObject value, long record) {
transportLock.readLock().lock();
Lock read = lockService.getReadLock(key, record);
read.lock();
try {
return inventory.contains(record) ? super
.verify(key, value, record) : false;
}
finally {
read.unlock();
transportLock.readLock().unlock();
}
}
@Override
public boolean verify(String key, TObject value, long record, long timestamp) {
transportLock.readLock().lock();
try {
return inventory.contains(record) ? super.verify(key, value,
record, timestamp) : false;
}
finally {
transportLock.readLock().unlock();
}
}
@Override
public boolean verifyUnsafe(String key, TObject value, long record) {
transportLock.readLock().lock();
try {
return inventory.contains(record) ? super
.verify(key, value, record) : false;
}
finally {
transportLock.readLock().unlock();
}
}
@Override
protected Map<Long, Set<TObject>> doExplore(long timestamp, String key,
Operator operator, TObject... values) {
transportLock.readLock().lock();
try {
return super.doExplore(timestamp, key, operator, values);
}
finally {
transportLock.readLock().unlock();
}
}
@Override
protected Map<Long, Set<TObject>> doExplore(String key, Operator operator,
TObject... values) {
transportLock.readLock().lock();
Lock range = rangeLockService.getReadLock(key, operator, values);
range.lock();
try {
return super.doExplore(key, operator, values);
}
finally {
range.unlock();
transportLock.readLock().unlock();
}
}
@Override
protected boolean verify(Write write, boolean lock) {
return inventory.contains(write.getRecord().longValue()) ? super
.verify(write, lock) : false;
}
/**
* Add {@code key} as {@code value} to {@code record} WITHOUT grabbing any
* locks. This method is ONLY appropriate to call from the
* {@link #accept(Write)} method that processes transaction commits since,
* in that case, the appropriate locks have already been grabbed.
*
* @param key
* @param value
* @param record
* @return {@code true} if the add was successful
*/
private boolean addUnsafe(String key, TObject value, long record,
boolean sync) {
return addUnsafe(key, value, record, sync, Token.wrap(record),
Token.wrap(key, record),
RangeToken.forWriting(Text.wrap(key), Value.wrap(value)));
}
/**
* Add {@code key} as {@code value} to {@code record} WITHOUT grabbing any
* locks. This method is ONLY appropriate to call from the
* {@link #accept(Write)} method that processes transaction commits since,
* in that case, the appropriate locks have already been grabbed.
*
* @param key
* @param value
* @param record
* @param sync
* @param shared - {@link LockToken} for record
* @param write - {@link LockToken} for key in record
* @param range - {@link RangeToken} for writing value to key
* @return {@code true} if the add was successful
*/
private boolean addUnsafe(String key, TObject value, long record,
boolean sync, Token shared, Token write, RangeToken range) {
if(super.add(key, value, record, sync, sync, false)) {
notifyVersionChange(write);
notifyVersionChange(shared);
notifyVersionChange(range);
return true;
}
return false;
}
/**
* Restore any transactions that did not finish committing prior to the
* previous shutdown.
*/
private void doTransactionRecovery() {
FileSystem.mkdirs(transactionStore);
for (File file : new File(transactionStore).listFiles()) {
Transaction.recover(this, file.getAbsolutePath());
Logger.info("Restored Transaction from {}", file.getName());
}
}
/**
* Return the number of milliseconds that have elapsed since the last time
* the {@link BufferTransportThread} successfully transported data.
*
* @return the idle time
*/
private long getBufferTransportThreadIdleTimeInMs() {
return TimeUnit.MILLISECONDS.convert(
Time.now() - ((Buffer) buffer).getTimeOfLastTransport(),
TimeUnit.MICROSECONDS);
}
/**
* Remove {@code key} as {@code value} from {@code record} WITHOUT grabbing
* any locks. This method is ONLY appropriate to call from the
* {@link #accept(Write)} method that processes transaction commits since,
* in that case, the appropriate locks have already been grabbed.
*
* @param key
* @param value
* @param record
* @return {@code true} if the add was successful
*/
private boolean removeUnsafe(String key, TObject value, long record,
boolean sync) {
return removeUnsafe(key, value, record, sync, Token.wrap(record),
Token.wrap(key, record),
RangeToken.forWriting(Text.wrap(key), Value.wrap(value)));
}
/**
* Remove {@code key} as {@code value} from {@code record} WITHOUT grabbing
* any locks. This method is ONLY appropriate to call from the
* {@link #accept(Write)} method that processes transaction commits since,
* in that case, the appropriate locks have already been grabbed.
*
* @param key
* @param value
* @param record
* @param sync
* @param shared - {@link LockToken} for record
* @param write - {@link LockToken} for key in record
* @param range - {@link RangeToken} for writing value to key
* @return {@code true} if the remove was successful
*/
private boolean removeUnsafe(String key, TObject value, long record,
boolean sync, Token shared, Token write, RangeToken range) {
if(super.remove(key, value, record, sync, sync, false)) {
notifyVersionChange(write);
notifyVersionChange(shared);
notifyVersionChange(range);
return true;
}
return false;
}
/**
* A thread that is responsible for transporting content from
* {@link #buffer} to {@link #destination}.
*
* @author Jeff Nelson
*/
private class BufferTransportThread extends Thread {
/**
* Construct a new instance.
*/
public BufferTransportThread() {
super(Strings.joinSimple("BufferTransport [", environment, "]"));
setDaemon(true);
setPriority(MIN_PRIORITY);
setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
Logger.error("Uncaught exception in {}:", t.getName(), e);
}
});
}
@Override
public void run() {
while (running) {
if(Thread.interrupted()) { // the thread has been
// interrupted from the Engine
// stopping
break;
}
if(getBufferTransportThreadIdleTimeInMs() > BUFFER_TRANSPORT_THREAD_ALLOWABLE_INACTIVITY_THRESHOLD_IN_MILLISECONDS) {
// If there have been no transports within the last second
// then make this thread block until the buffer is
// transportable so that we do not waste CPU cycles
// busy waiting unnecessarily.
bufferTransportThreadHasEverPaused.set(true);
bufferTransportThreadIsPaused.set(true);
Logger.debug(
"Paused the background data transport thread because "
+ "it has been inactive for at least {} milliseconds",
BUFFER_TRANSPORT_THREAD_ALLOWABLE_INACTIVITY_THRESHOLD_IN_MILLISECONDS);
buffer.waitUntilTransportable();
if(Thread.interrupted()) { // the thread has been
// interrupted from the Engine
// stopping
break;
}
}
doTransport();
try {
// NOTE: This thread needs to sleep for a small amount of
// time to avoid thrashing
int sleep = bufferTransportThreadSleepInMs > 0 ? bufferTransportThreadSleepInMs
: buffer.getDesiredTransportSleepTimeInMs();
Thread.sleep(sleep);
bufferTransportThreadLastWakeUp.set(Time.now());
}
catch (InterruptedException e) {
if(getBufferTransportThreadIdleTimeInMs() > BUFFER_TRANSPORT_THREAD_HUNG_DETECTION_THRESOLD_IN_MILLISECONDS) {
Logger.warn(
"The data transport thread been sleeping for over "
+ "{} milliseconds even though there is work to do. "
+ "An attempt has been made to restart the stalled "
+ "process.",
BUFFER_TRANSPORT_THREAD_HUNG_DETECTION_THRESOLD_IN_MILLISECONDS);
bufferTransportThreadHasEverBeenRestarted.set(true);
}
else {
Thread.currentThread().interrupt();
}
}
}
}
/**
* Tell the Buffer to transport data and prevent deadlock in the event
* of failure.
*/
private void doTransport() {
if(transportLock.writeLock().tryLock()) {
try {
bufferTransportThreadIsPaused.compareAndSet(true, false);
bufferTransportThreadIsDoingWork.set(true);
buffer.transport(destination);
bufferTransportThreadLastWakeUp.set(Time.now());
bufferTransportThreadIsDoingWork.set(false);
}
finally {
transportLock.writeLock().unlock();
}
}
}
}
}