org.ehcache.transactions.xa.internal.XATransactionContext Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ehcache-transactions Show documentation
Show all versions of ehcache-transactions Show documentation
The transactions module of Ehcache 3
/*
* 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.impl.internal.concurrent.ConcurrentHashMap;
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;
/**
* 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 {
private static final Logger LOGGER = LoggerFactory.getLogger(XATransactionContext.class);
private final ConcurrentHashMap> commands = new ConcurrentHashMap>();
private final TransactionId transactionId;
private final Store> underlyingStore;
private final Journal journal;
private final TimeSource timeSource;
private final long timeoutTimestamp;
XATransactionContext(TransactionId transactionId, Store> underlyingStore, Journal 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 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> newValueHolders() {
Map> puts = new HashMap>();
for (Map.Entry> entry : commands.entrySet()) {
Command 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 command = commands.get(key);
return command != null ? command.getOldValue() : null;
}
public XAValueHolder newValueHolderOf(K key) {
Command command = commands.get(key);
return command != null ? command.getNewValueHolder() : null;
}
public V newValueOf(K key) {
Command command = commands.get(key);
XAValueHolder 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> entry : commands.entrySet()) {
if (entry.getValue() instanceof StoreEvictCommand) {
evictFromUnderlyingStore(entry.getKey());
continue;
}
V oldValue = entry.getValue().getOldValue();
SoftLock oldSoftLock = oldValue == null ? null : new SoftLock(null, oldValue, null);
SoftLock newSoftLock = new SoftLock(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> 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 keys = journal.getInDoubtKeys(transactionId);
for (K key : keys) {
SoftLock preparedSoftLock = getFromUnderlyingStore(key);
XAValueHolder newValueHolder = preparedSoftLock == null ? null : preparedSoftLock.getNewValueHolder();
SoftLock definitiveSoftLock = newValueHolder == null ? null : new SoftLock(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 keys = journal.getInDoubtKeys(transactionId);
for (K key : keys) {
SoftLock preparedSoftLock = getFromUnderlyingStore(key);
V oldValue = preparedSoftLock == null ? null : preparedSoftLock.getOldValue();
SoftLock definitiveSoftLock = oldValue == null ? null : new SoftLock(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 preparedSoftLock) throws StoreAccessException {
if (underlyingStore.remove(key, preparedSoftLock).equals(RemoveStatus.REMOVED)) {
return true;
}
return false;
}
private boolean replaceInUnderlyingStore(K key, SoftLock preparedSoftLock, SoftLock definitiveSoftLock) throws StoreAccessException {
if (underlyingStore.replace(key, preparedSoftLock, definitiveSoftLock).equals(ReplaceStatus.HIT)) {
return true;
}
return false;
}
private Store.ValueHolder> putIfAbsentInUnderlyingStore(Map.Entry> entry, SoftLock newSoftLock) throws StoreAccessException {
return underlyingStore.putIfAbsent(entry.getKey(), newSoftLock);
}
private SoftLock getFromUnderlyingStore(K key) throws StoreAccessException {
Store.ValueHolder> 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 {
}
}