/* * 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 static com.google.common.base.Preconditions.checkArgument; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.channels.FileChannel.MapMode; import java.util.Iterator; import java.util.Map; import java.util.Set; import com.cinchapi.concourse.annotate.Restricted; import com.cinchapi.concourse.server.concurrent.LockService; import com.cinchapi.concourse.server.concurrent.RangeLockService; import com.cinchapi.concourse.server.concurrent.Token; import com.cinchapi.concourse.server.io.ByteableCollections; import com.cinchapi.concourse.server.io.FileSystem; import com.cinchapi.concourse.server.storage.temp.Queue; 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.ByteBuffers; import com.cinchapi.concourse.util.Logger; import com.google.common.base.Throwables; import com.google.common.collect.HashMultimap; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; /** * An {@link AtomicOperation} that performs backups prior to commit to make sure * that it is durable in the event of crash, power loss or failure. * * @author Jeff Nelson */ public final class Transaction extends AtomicOperation implements AtomicSupport { // NOTE: Because Transaction's rely on JIT locking, the unsafe methods call // the safe counterparts in the super class (AtomicOperation) because those // have logic to tell the BufferedStore class to perform unsafe reads. /** * Return the Transaction for {@code destination} that is backed up to * {@code file}. This method will finish committing the transaction before * returning. * * @param destination * @param file * @return The restored Transaction */ public static void recover(Engine destination, String file) { try { Transaction transaction = new Transaction(destination, FileSystem.map(file, MapMode.READ_ONLY, 0, FileSystem.getFileSize(file))); transaction.invokeSuperDoCommit(true); // recovering transaction // must always syncAndVerify // to prevent possible data // duplication FileSystem.deleteFile(file); } catch (Exception e) { Logger.warn("Attempted to recover a transaction from {}, " + "but the data is corrupted. This indicates that " + "Concourse Server shutdown before the transaction " + "could properly commit, so none of the data " + "in the transaction has persisted.", file); Logger.debug("Transaction backup in {} is corrupt because " + "of {}", file, e); FileSystem.deleteFile(file); } } /** * Return a new Transaction with {@code engine} as the eventual destination. * * @param engine * @return the new Transaction */ public static Transaction start(Engine engine) { return new Transaction(engine); } /** * The Transaction "manages" the version change listeners for each of its * Atomic Operations. Since the Transaction is registered with the Engine * for version change notifications for each action of each of its atomic * operations, the Transaction must intercept any notifications that would * affect an atomic operation that has not committed. */ private Multimap<AtomicOperation, Token> managedVersionChangeListeners = HashMultimap .create(); /** * The unique Transaction id. */ private final String id; /** * Construct a new instance. * * @param destination */ private Transaction(Engine destination) { super(new Queue(INITIAL_CAPACITY), destination); this.id = Long.toString(Time.now()); } /** * Construct a new instance. * * @param destination * @param bytes */ private Transaction(Engine destination, ByteBuffer bytes) { this(destination); deserialize(bytes); open.set(false); } @Override public void abort() { super.abort(); Logger.info("Aborted Transaction {}", this); } @Override public void accept(Write write) { // Accept writes from an AtomicOperation and put them in this // Transaction's buffer. checkArgument(write.getType() != Action.COMPARE); String key = write.getKey().toString(); TObject value = write.getValue().getTObject(); long record = write.getRecord().longValue(); if(write.getType() == Action.ADD) { add(key, value, record); } else { remove(key, value, record); } } @Override public void accept(Write write, boolean sync) { accept(write); } @Override @Restricted public void addVersionChangeListener(Token token, VersionChangeListener listener) { // The Transaction is added as a version change listener for each of its // atomic operation reads/writes by virtue of the fact that the atomic // operations (via BufferedStore) call the analogous read/write methods // in the Transaction, which registers the Transaction with // the Engine as a version change listener. managedVersionChangeListeners.put((AtomicOperation) listener, token); } @Override public Map<Long, String> auditUnsafe(long record) { return audit(record); } @Override public Map<Long, String> auditUnsafe(String key, long record) { return audit(key, record); } @Override public Map<Long, Set<TObject>> chronologizeUnsafe(String key, long record, long start, long end) { return chronologize(key, record, start, end); } @Override public Map<String, Set<TObject>> browseUnsafe(long record) { return select(record); } @Override public Map<TObject, Set<Long>> browseUnsafe(String key) { return browse(key); } @Override public Map<Long, Set<TObject>> doExploreUnsafe(String key, Operator operator, TObject... values) { return doExplore(key, operator, values); } @Override public Set<TObject> selectUnsafe(String key, long record) { return select(key, record); } @Override @Restricted public void notifyVersionChange(Token token) {} @Override public void onVersionChange(Token token) { // We override this method to handle the case where an atomic operation // started from this transaction must fail because of a version change, // but that failure should not cause the transaction itself to fail // (i.e. calling verifyAndSwap from a transaction and a version change // causes that particular operation to fail prior to commit. The logic // in this method will simply cause the invocation of verifyAndSwap to // return false while this transaction would stay alive. boolean callSuper = true; for (AtomicOperation operation : managedVersionChangeListeners.keySet()) { for (Token tok : managedVersionChangeListeners.get(operation)) { if(tok.equals(token)) { operation.onVersionChange(tok); managedVersionChangeListeners.remove(operation, tok); callSuper = false; break; } } } if(callSuper) { super.onVersionChange(token); } } @Override @Restricted public void removeVersionChangeListener(Token token, VersionChangeListener listener) {} @Override public AtomicOperation startAtomicOperation() { checkState(); AtomicOperation operation = AtomicOperation.start(this); operation.lockService = LockService.noOp(); operation.rangeLockService = RangeLockService.noOp(); return operation; } @Override public void sync() {/* no-op */} @Override public String toString() { return id; } @Override public boolean verifyUnsafe(String key, TObject value, long record) { return verify(key, value, record); } /** * Deserialize the content of this Transaction from {@code bytes}. * * @param bytes */ private void deserialize(ByteBuffer bytes) { locks = Maps.newHashMap(); Iterator<ByteBuffer> it = ByteableCollections.iterator(ByteBuffers .slice(bytes, bytes.getInt())); while (it.hasNext()) { LockDescription lock = LockDescription.fromByteBuffer(it.next(), lockService, rangeLockService); locks.put(lock.getToken(), lock); } it = ByteableCollections.iterator(bytes); while (it.hasNext()) { Write write = Write.fromByteBuffer(it.next()); buffer.insert(write); } } /** * Invoke {@link #doCommit()} that is defined in the super class. This * method should only be called when it is desirable to doCommit without * performing a backup (i.e. when restoring from a backup in a static * method). * * @param syncAndVerify a flag that is passed onto the * {@link AtomicOperation#doCommit(boolean)} method */ private void invokeSuperDoCommit(boolean syncAndVerify) { super.doCommit(syncAndVerify); Logger.info("Finalized commit for Transaction {}", this); } /** * Serialize the Transaction to a ByteBuffer. * <ol> * <li><strong>lockSize</strong> - position 0</li> * <li><strong>locks</strong> - position 4</li> * <li><strong>writes</strong> - position 4 + lockSize</li> * </ol> * * @return the ByteBuffer representation */ private ByteBuffer serialize() { ByteBuffer _locks = ByteableCollections.toByteBuffer(locks.values()); ByteBuffer _writes = ByteableCollections.toByteBuffer(((Queue) buffer) .getWrites()); ByteBuffer bytes = ByteBuffer.allocate(4 + _locks.capacity() + _writes.capacity()); bytes.putInt(_locks.capacity()); bytes.put(_locks); bytes.put(_writes); bytes.rewind(); return bytes; } @Override protected void checkState() throws AtomicStateException { try { super.checkState(); } catch (AtomicStateException e) { throw new TransactionStateException(); } } @Override protected void doCommit() { if(isReadOnly()) { invokeSuperDoCommit(false); } else { String file = ((Engine) destination).transactionStore + File.separator + id + ".txn"; FileChannel channel = FileSystem.getFileChannel(file); try { channel.write(serialize()); channel.force(true); Logger.info("Created backup for transaction {} at '{}'", this, file); invokeSuperDoCommit(false); FileSystem.deleteFile(file); } catch (IOException e) { throw Throwables.propagate(e); } finally { FileSystem.closeFileChannel(channel); } } } /** * Perform cleanup for the atomic {@code operation} that was birthed from * this transaction and has successfully committed. * * @param operation an AtomicOperation, birthed from this Transaction, * that has committed successfully */ protected void onCommit(AtomicOperation operation) { managedVersionChangeListeners.removeAll(operation); } }