org.janusgraph.diskstorage.locking.consistentkey.ExpectedValueCheckingTransaction.java Source code

Java tutorial

Introduction

Here is the source code for org.janusgraph.diskstorage.locking.consistentkey.ExpectedValueCheckingTransaction.java

Source

// 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);
        }
    }
}