// Copyright 2017 JanusGraph Authors
//
// 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 org.janusgraph.diskstorage.locking.consistentkey;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import org.janusgraph.diskstorage.*;
import org.janusgraph.diskstorage.keycolumnvalue.*;
import org.janusgraph.diskstorage.locking.LocalLockMediator;
import org.janusgraph.diskstorage.locking.Locker;
import org.janusgraph.diskstorage.locking.PermanentLockingException;
import org.janusgraph.diskstorage.util.BackendOperation;
import org.janusgraph.diskstorage.util.BufferUtil;
import org.janusgraph.diskstorage.util.KeyColumn;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
/**
* A {@link StoreTransaction} that supports locking via
* {@link LocalLockMediator} and writing and reading lock records in a
* {@link ExpectedValueCheckingStore}.
* <p/>
* <p/>
* <b>This class is not safe for concurrent use by multiple threads.
* Multithreaded access must be prevented or externally synchronized.</b>
*/
public class ExpectedValueCheckingTransaction implements StoreTransaction {
private static final Logger log = LoggerFactory.getLogger(ExpectedValueCheckingTransaction.class);
/**
* This variable starts false. It remains false during the
* locking stage of a transaction. It is set to true at the
* beginning of the first mutate/mutateMany call in a transaction
* (before performing any writes to the backing store).
*/
private boolean isMutationStarted;
/**
* Transaction for reading and writing locking-related metadata. Also used
* for reading expected values provided as arguments to
* {@link KeyColumnValueStore#acquireLock(StaticBuffer, StaticBuffer, StaticBuffer, StoreTransaction)}
*/
private final StoreTransaction strongConsistentTx;
/**
* Transaction for reading and writing client data. No guarantees about
* consistency strength.
*/
private final StoreTransaction inconsistentTx;
private final Duration maxReadTime;
private final Map<ExpectedValueCheckingStore, Map<KeyColumn, StaticBuffer>> expectedValuesByStore =
new HashMap<ExpectedValueCheckingStore, Map<KeyColumn, StaticBuffer>>();
public ExpectedValueCheckingTransaction(StoreTransaction inconsistentTx, StoreTransaction strongConsistentTx, Duration maxReadTime) {
this.inconsistentTx = inconsistentTx;
this.strongConsistentTx = strongConsistentTx;
this.maxReadTime = maxReadTime;
}
@Override
public void rollback() throws BackendException {
deleteAllLocks();
inconsistentTx.rollback();
strongConsistentTx.rollback();
}
@Override
public void commit() throws BackendException {
inconsistentTx.commit();
deleteAllLocks();
strongConsistentTx.commit();
}
/**
* Tells whether this transaction has been used in a
* {@link ExpectedValueCheckingStore#mutate(StaticBuffer, List, List, StoreTransaction)}
* call. When this returns true, the transaction is no longer allowed in
* calls to
* {@link ExpectedValueCheckingStore#acquireLock(StaticBuffer, StaticBuffer, StaticBuffer, StoreTransaction)}.
*
* @return False until
* {@link ExpectedValueCheckingStore#mutate(StaticBuffer, List, List, StoreTransaction)}
* is called on this transaction instance. Returns true forever
* after.
*/
public boolean isMutationStarted() {
return isMutationStarted;
}
@Override
public BaseTransactionConfig getConfiguration() {
return inconsistentTx.getConfiguration();
}
public StoreTransaction getInconsistentTx() {
return inconsistentTx;
}
public StoreTransaction getConsistentTx() {
return strongConsistentTx;
}
void storeExpectedValue(ExpectedValueCheckingStore store, KeyColumn lockID, StaticBuffer value) {
Preconditions.checkNotNull(store);
Preconditions.checkNotNull(lockID);
lockedOn(store);
Map<KeyColumn, StaticBuffer> m = expectedValuesByStore.get(store);
assert null != m;
if (m.containsKey(lockID)) {
log.debug("Multiple expected values for {}: keeping initial value {} and discarding later value {}",
new Object[]{lockID, m.get(lockID), value});
} else {
m.put(lockID, value);
log.debug("Store expected value for {}: {}", lockID, value);
}
}
/**
* If {@code !}{@link #isMutationStarted()}, check all locks and expected
* values, then mark the transaction as started.
* <p>
* If {@link #isMutationStarted()}, this does nothing.
*
* @throws org.janusgraph.diskstorage.BackendException
*
* @return true if this transaction holds at least one lock, false if the
* transaction holds no locks
*/
boolean prepareForMutations() throws BackendException {
if (!isMutationStarted()) {
checkAllLocks();
checkAllExpectedValues();
mutationStarted();
}
return !expectedValuesByStore.isEmpty();
}
/**
* Check all locks attempted by earlier
* {@link KeyColumnValueStore#acquireLock(StaticBuffer, StaticBuffer, StaticBuffer, StoreTransaction)}
* calls using this transaction.
*
* @throws org.janusgraph.diskstorage.BackendException
*/
void checkAllLocks() throws BackendException {
StoreTransaction lt = getConsistentTx();
for (ExpectedValueCheckingStore store : expectedValuesByStore.keySet()) {
Locker locker = store.getLocker();
// Ignore locks on stores without a locker
if (null == locker)
continue;
locker.checkLocks(lt);
}
}
/**
* Check that all expected values saved from earlier
* {@link KeyColumnValueStore#acquireLock(StaticBuffer, StaticBuffer, StaticBuffer, StoreTransaction)}
* calls using this transaction.
*
* @throws org.janusgraph.diskstorage.BackendException
*/
void checkAllExpectedValues() throws BackendException {
for (final ExpectedValueCheckingStore store : expectedValuesByStore.keySet()) {
final Map<KeyColumn, StaticBuffer> m = expectedValuesByStore.get(store);
for (final KeyColumn kc : m.keySet()) {
checkSingleExpectedValue(kc, m.get(kc), store);
}
}
}
/**
* Signals the transaction that it has been used in a call to
* {@link ExpectedValueCheckingStore#mutate(StaticBuffer, List, List, StoreTransaction)}
* . This transaction can't be used in subsequent calls to
* {@link ExpectedValueCheckingStore#acquireLock(StaticBuffer, StaticBuffer, StaticBuffer, StoreTransaction)}
* .
* <p/>
* Calling this method at the appropriate time is handled automatically by
* {@link ExpectedValueCheckingStore}. JanusGraph users don't need to call this
* method by hand.
*/
private void mutationStarted() {
isMutationStarted = true;
}
private void lockedOn(ExpectedValueCheckingStore store) {
Map<KeyColumn, StaticBuffer> m = expectedValuesByStore.get(store);
if (null == m) {
m = new HashMap<KeyColumn, StaticBuffer>();
expectedValuesByStore.put(store, m);
}
}
private void checkSingleExpectedValue(final KeyColumn kc,
final StaticBuffer ev, final ExpectedValueCheckingStore store) throws BackendException {
BackendOperation.executeDirect(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
checkSingleExpectedValueUnsafe(kc, ev, store);
return true;
}
@Override
public String toString() {
return "ExpectedValueChecking";
}
},maxReadTime);
}
private void checkSingleExpectedValueUnsafe(final KeyColumn kc,
final StaticBuffer ev, final ExpectedValueCheckingStore store) throws BackendException {
final StaticBuffer nextBuf = BufferUtil.nextBiggerBuffer(kc.getColumn());
KeySliceQuery ksq = new KeySliceQuery(kc.getKey(), kc.getColumn(), nextBuf);
// Call getSlice on the wrapped store using the quorum+ consistency tx
Iterable<Entry> actualEntries = store.getBackingStore().getSlice(ksq, strongConsistentTx);
if (null == actualEntries)
actualEntries = ImmutableList.<Entry>of();
/*
* Discard any columns which do not exactly match kc.getColumn().
*
* For example, it's possible that the slice returned columns which for
* which kc.getColumn() is a prefix.
*/
actualEntries = Iterables.filter(actualEntries, new Predicate<Entry>() {
@Override
public boolean apply(Entry input) {
if (!input.getColumn().equals(kc.getColumn())) {
log.debug("Dropping entry {} (only accepting column {})", input, kc.getColumn());
return false;
}
log.debug("Accepting entry {}", input);
return true;
}
});
// Extract values from remaining Entry instances
Iterable<StaticBuffer> actualVals = Iterables.transform(actualEntries, new Function<Entry, StaticBuffer>() {
@Override
public StaticBuffer apply(Entry e) {
StaticBuffer actualCol = e.getColumnAs(StaticBuffer.STATIC_FACTORY);
assert null != actualCol;
assert null != kc.getColumn();
assert 0 >= kc.getColumn().compareTo(actualCol);
assert 0 > actualCol.compareTo(nextBuf);
return e.getValueAs(StaticBuffer.STATIC_FACTORY);
}
});
final Iterable<StaticBuffer> expectedVals;
if (null == ev) {
expectedVals = ImmutableList.<StaticBuffer>of();
} else {
expectedVals = ImmutableList.<StaticBuffer>of(ev);
}
if (!Iterables.elementsEqual(expectedVals, actualVals)) {
throw new PermanentLockingException(
"Expected value mismatch for " + kc + ": expected="
+ expectedVals + " vs actual=" + actualVals + " (store=" + store.getName() + ")");
}
}
private void deleteAllLocks() throws BackendException {
for (ExpectedValueCheckingStore s : expectedValuesByStore.keySet()) {
s.deleteLocks(this);
}
}
}