/* * Copyright Terracotta, 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 org.ehcache.transactions.xa.internal; import org.ehcache.core.spi.store.StoreAccessException; import org.ehcache.core.spi.time.TimeSource; import org.ehcache.core.spi.store.Store; import org.ehcache.core.spi.store.Store.RemoveStatus; import org.ehcache.core.spi.store.Store.ReplaceStatus; import org.ehcache.transactions.xa.internal.commands.Command; import org.ehcache.transactions.xa.internal.commands.StoreEvictCommand; import org.ehcache.transactions.xa.internal.commands.StorePutCommand; import org.ehcache.transactions.xa.internal.commands.StoreRemoveCommand; import org.ehcache.transactions.xa.internal.journal.Journal; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * Context holder of an in-flight XA transaction. Modifications to the {@link XAStore} are registered in an instance * of this class in the form of {@link Command}s and can then be applied to the {@link Store} backing the {@link XAStore} * in the form of {@link SoftLock}s. * * @author Ludovic Orban */ public class XATransactionContext<K, V> { private static final Logger LOGGER = LoggerFactory.getLogger(XATransactionContext.class); private final ConcurrentHashMap<K, Command<V>> commands = new ConcurrentHashMap<K, Command<V>>(); private final TransactionId transactionId; private final Store<K, SoftLock<V>> underlyingStore; private final Journal<K> journal; private final TimeSource timeSource; private final long timeoutTimestamp; XATransactionContext(TransactionId transactionId, Store<K, SoftLock<V>> underlyingStore, Journal<K> journal, TimeSource timeSource, long timeoutTimestamp) { this.transactionId = transactionId; this.underlyingStore = underlyingStore; this.journal = journal; this.timeSource = timeSource; this.timeoutTimestamp = timeoutTimestamp; } public boolean hasTimedOut() { return timeSource.getTimeMillis() >= timeoutTimestamp; } public TransactionId getTransactionId() { return transactionId; } public boolean addCommand(K key, Command<V> command) { if (commands.get(key) instanceof StoreEvictCommand) { // once a mapping is marked as evict, that's the only thing that can happen return false; } commands.put(key, command); return true; } public void removeCommand(K key) { commands.remove(key); } public Map<K, XAValueHolder<V>> newValueHolders() { Map<K, XAValueHolder<V>> puts = new HashMap<K, XAValueHolder<V>>(); for (Map.Entry<K, Command<V>> entry : commands.entrySet()) { Command<V> command = entry.getValue(); if (command instanceof StorePutCommand) { puts.put(entry.getKey(), entry.getValue().getNewValueHolder()); } } return puts; } public boolean touched(K key) { return commands.containsKey(key); } public boolean removed(K key) { return commands.get(key) instanceof StoreRemoveCommand; } public boolean updated(K key) { return commands.get(key) instanceof StorePutCommand; } public boolean evicted(K key) { return commands.get(key) instanceof StoreEvictCommand; } public V oldValueOf(K key) { Command<V> command = commands.get(key); return command != null ? command.getOldValue() : null; } public XAValueHolder<V> newValueHolderOf(K key) { Command<V> command = commands.get(key); return command != null ? command.getNewValueHolder() : null; } public V newValueOf(K key) { Command<V> command = commands.get(key); XAValueHolder<V> valueHolder = command == null ? null : command.getNewValueHolder(); return valueHolder == null ? null : valueHolder.value(); } public int prepare() throws StoreAccessException, IllegalStateException, TransactionTimeoutException { try { if (hasTimedOut()) { throw new TransactionTimeoutException(); } if (journal.isInDoubt(transactionId)) { throw new IllegalStateException("Cannot prepare transaction that is not in-flight : " + transactionId); } journal.saveInDoubt(transactionId, commands.keySet()); for (Map.Entry<K, Command<V>> entry : commands.entrySet()) { if (entry.getValue() instanceof StoreEvictCommand) { evictFromUnderlyingStore(entry.getKey()); continue; } V oldValue = entry.getValue().getOldValue(); SoftLock<V> oldSoftLock = oldValue == null ? null : new SoftLock<V>(null, oldValue, null); SoftLock<V> newSoftLock = new SoftLock<V>(transactionId, oldValue, entry.getValue().getNewValueHolder()); if (oldSoftLock != null) { boolean replaced = replaceInUnderlyingStore(entry.getKey(), oldSoftLock, newSoftLock); if (!replaced) { LOGGER.debug("prepare failed replace of softlock (concurrent modification?)"); evictFromUnderlyingStore(entry.getKey()); } } else { Store.ValueHolder<SoftLock<V>> existing = putIfAbsentInUnderlyingStore(entry, newSoftLock); if (existing != null) { LOGGER.debug("prepare failed putIfAbsent of softlock (concurrent modification?)"); evictFromUnderlyingStore(entry.getKey()); } } } if (commands.isEmpty()) { journal.saveRolledBack(transactionId, false); } return commands.size(); } finally { commands.clear(); } } /** * @throws IllegalStateException if the transaction ID is unknown * @throws IllegalArgumentException if the transaction ID has not been prepared */ public void commit(boolean recovering) throws StoreAccessException, IllegalStateException, IllegalArgumentException { if (!journal.isInDoubt(transactionId)) { if (recovering) { throw new IllegalStateException("Cannot commit unknown transaction : " + transactionId); } else { throw new IllegalArgumentException("Cannot commit transaction that has not been prepared : " + transactionId); } } Collection<K> keys = journal.getInDoubtKeys(transactionId); for (K key : keys) { SoftLock<V> preparedSoftLock = getFromUnderlyingStore(key); XAValueHolder<V> newValueHolder = preparedSoftLock == null ? null : preparedSoftLock.getNewValueHolder(); SoftLock<V> definitiveSoftLock = newValueHolder == null ? null : new SoftLock<V>(null, newValueHolder.value(), null); if (preparedSoftLock != null) { if (preparedSoftLock.getTransactionId() != null && !preparedSoftLock.getTransactionId().equals(transactionId)) { LOGGER.debug("commit skipping prepared softlock with non-matching TX ID (concurrent modification?)"); evictFromUnderlyingStore(key); continue; } if (definitiveSoftLock != null) { boolean replaced = replaceInUnderlyingStore(key, preparedSoftLock, definitiveSoftLock); if (!replaced) { LOGGER.debug("commit failed replace of softlock (concurrent modification?)"); evictFromUnderlyingStore(key); } } else { boolean removed = removeFromUnderlyingStore(key, preparedSoftLock); if (!removed) { LOGGER.debug("commit failed remove of softlock (concurrent modification?)"); evictFromUnderlyingStore(key); } } } else { LOGGER.debug("commit skipping evicted prepared softlock"); } } journal.saveCommitted(transactionId, false); } public void commitInOnePhase() throws StoreAccessException, IllegalStateException, TransactionTimeoutException { if (journal.isInDoubt(transactionId)) { throw new IllegalStateException("Cannot commit-one-phase transaction that has been prepared : " + transactionId); } int prepared = prepare(); if (prepared > 0) { commit(false); } } /** * @throws IllegalStateException if the transaction ID is unknown */ public void rollback(boolean recovering) throws StoreAccessException, IllegalStateException { boolean inDoubt = journal.isInDoubt(transactionId); if (inDoubt) { // phase 2 rollback Collection<K> keys = journal.getInDoubtKeys(transactionId); for (K key : keys) { SoftLock<V> preparedSoftLock = getFromUnderlyingStore(key); V oldValue = preparedSoftLock == null ? null : preparedSoftLock.getOldValue(); SoftLock<V> definitiveSoftLock = oldValue == null ? null : new SoftLock<V>(null, oldValue, null); if (preparedSoftLock != null) { if (preparedSoftLock.getTransactionId() != null && !preparedSoftLock.getTransactionId().equals(transactionId)) { LOGGER.debug("rollback skipping prepared softlock with non-matching TX ID (concurrent modification?)"); evictFromUnderlyingStore(key); continue; } if (definitiveSoftLock != null) { boolean replaced = replaceInUnderlyingStore(key, preparedSoftLock, definitiveSoftLock); if (!replaced) { LOGGER.debug("rollback failed replace of softlock (concurrent modification?)"); evictFromUnderlyingStore(key); } } else { boolean removed = removeFromUnderlyingStore(key, preparedSoftLock); if (!removed) { LOGGER.debug("rollback failed remove of softlock (concurrent modification?)"); evictFromUnderlyingStore(key); } } } else { LOGGER.debug("rollback skipping evicted prepared softlock"); } } journal.saveRolledBack(transactionId, false); } else if (recovering) { throw new IllegalStateException("Cannot rollback unknown transaction : " + transactionId); } else { // phase 1 rollback commands.clear(); } } private boolean removeFromUnderlyingStore(K key, SoftLock<V> preparedSoftLock) throws StoreAccessException { if (underlyingStore.remove(key, preparedSoftLock).equals(RemoveStatus.REMOVED)) { return true; } return false; } private boolean replaceInUnderlyingStore(K key, SoftLock<V> preparedSoftLock, SoftLock<V> definitiveSoftLock) throws StoreAccessException { if (underlyingStore.replace(key, preparedSoftLock, definitiveSoftLock).equals(ReplaceStatus.HIT)) { return true; } return false; } private Store.ValueHolder<SoftLock<V>> putIfAbsentInUnderlyingStore(Map.Entry<K, Command<V>> entry, SoftLock<V> newSoftLock) throws StoreAccessException { return underlyingStore.putIfAbsent(entry.getKey(), newSoftLock); } private SoftLock<V> getFromUnderlyingStore(K key) throws StoreAccessException { Store.ValueHolder<SoftLock<V>> softLockValueHolder = underlyingStore.get(key); return softLockValueHolder == null ? null : softLockValueHolder.value(); } private void evictFromUnderlyingStore(K key) throws StoreAccessException { underlyingStore.remove(key); } static class TransactionTimeoutException extends RuntimeException { } }