/* * Copyright (c) 2008-2017, Hazelcast, Inc. All Rights Reserved. * * 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.hazelcast.transaction.impl.xa; import com.hazelcast.config.GroupConfig; import com.hazelcast.core.HazelcastInstanceNotActiveException; import com.hazelcast.core.Member; import com.hazelcast.core.MemberLeftException; import com.hazelcast.internal.cluster.ClusterService; import com.hazelcast.logging.ILogger; import com.hazelcast.nio.Address; import com.hazelcast.nio.serialization.Data; import com.hazelcast.spi.AbstractDistributedObject; import com.hazelcast.spi.InternalCompletableFuture; import com.hazelcast.spi.NodeEngine; import com.hazelcast.spi.OperationService; import com.hazelcast.spi.exception.TargetNotMemberException; import com.hazelcast.spi.impl.SerializableList; import com.hazelcast.spi.partition.IPartitionService; import com.hazelcast.transaction.HazelcastXAResource; import com.hazelcast.transaction.TransactionContext; import com.hazelcast.transaction.TransactionOptions; import com.hazelcast.transaction.impl.Transaction; import com.hazelcast.transaction.impl.xa.operations.ClearRemoteTransactionOperation; import com.hazelcast.transaction.impl.xa.operations.CollectRemoteTransactionsOperation; import com.hazelcast.transaction.impl.xa.operations.FinalizeRemoteTransactionOperation; import com.hazelcast.util.ExceptionUtil; import javax.transaction.xa.XAException; import javax.transaction.xa.XAResource; import javax.transaction.xa.Xid; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicInteger; import static com.hazelcast.transaction.impl.xa.XAService.SERVICE_NAME; import static java.util.concurrent.TimeUnit.MILLISECONDS; /** * Server side XaResource implementation */ public final class XAResourceImpl extends AbstractDistributedObject<XAService> implements HazelcastXAResource { private static final int DEFAULT_TIMEOUT_SECONDS = (int) MILLISECONDS.toSeconds(TransactionOptions.DEFAULT_TIMEOUT_MILLIS); private final ConcurrentMap<Long, TransactionContext> threadContextMap = new ConcurrentHashMap<Long, TransactionContext>(); private final ConcurrentMap<Xid, List<TransactionContext>> xidContextMap = new ConcurrentHashMap<Xid, List<TransactionContext>>(); private final String groupName; private final AtomicInteger timeoutInSeconds = new AtomicInteger(DEFAULT_TIMEOUT_SECONDS); private final ILogger logger; public XAResourceImpl(NodeEngine nodeEngine, XAService service) { super(nodeEngine, service); GroupConfig groupConfig = nodeEngine.getConfig().getGroupConfig(); groupName = groupConfig.getName(); logger = nodeEngine.getLogger(getClass()); } @Override public void start(Xid xid, int flags) throws XAException { long threadId = currentThreadId(); TransactionContext threadContext = threadContextMap.get(currentThreadId()); switch (flags) { case TMNOFLAGS: List<TransactionContext> contexts = new CopyOnWriteArrayList<TransactionContext>(); List<TransactionContext> currentContexts = xidContextMap.putIfAbsent(xid, contexts); if (currentContexts != null) { throw new XAException("There is already TransactionContexts for the given xid: " + xid); } TransactionContext context = createTransactionContext(xid); contexts.add(context); threadContextMap.put(threadId, context); break; case TMRESUME: case TMJOIN: List<TransactionContext> contextList = xidContextMap.get(xid); if (contextList == null) { throw new XAException("There is no TransactionContexts for the given xid: " + xid); } if (threadContext == null) { threadContext = createTransactionContext(xid); threadContextMap.put(threadId, threadContext); contextList.add(threadContext); } break; default: throw new XAException("Unknown flag! " + flags); } } private TransactionContext createTransactionContext(Xid xid) { XAService xaService = getService(); TransactionContext context = xaService.newXATransactionContext(xid, null, timeoutInSeconds.get(), false); getTransaction(context).begin(); return context; } @Override public void end(Xid xid, int flags) throws XAException { long threadId = currentThreadId(); TransactionContext threadContext = threadContextMap.remove(threadId); if (threadContext == null && logger.isFinestEnabled()) { logger.finest("There is no TransactionContext for the current thread: " + threadId); } List<TransactionContext> contexts = xidContextMap.get(xid); if (contexts == null && logger.isFinestEnabled()) { logger.finest("There is no TransactionContexts for the given xid: " + xid); } } @Override public int prepare(Xid xid) throws XAException { List<TransactionContext> contexts = xidContextMap.get(xid); if (contexts == null) { throw new XAException("There is no TransactionContexts for the given xid: " + xid); } for (TransactionContext context : contexts) { Transaction transaction = getTransaction(context); transaction.prepare(); } return XA_OK; } @Override public void commit(Xid xid, boolean onePhase) throws XAException { List<TransactionContext> contexts = xidContextMap.remove(xid); if (contexts == null && onePhase) { throw new XAException("There is no TransactionContexts for the given xid: " + xid); } if (contexts == null) { finalizeTransactionRemotely(xid, true); return; } for (TransactionContext context : contexts) { Transaction transaction = getTransaction(context); if (onePhase) { transaction.prepare(); } transaction.commit(); } clearRemoteTransactions(xid); } @Override public void rollback(Xid xid) throws XAException { List<TransactionContext> contexts = xidContextMap.remove(xid); if (contexts == null) { finalizeTransactionRemotely(xid, false); return; } for (TransactionContext context : contexts) { getTransaction(context).rollback(); } clearRemoteTransactions(xid); } private void finalizeTransactionRemotely(Xid xid, boolean isCommit) throws XAException { NodeEngine nodeEngine = getNodeEngine(); IPartitionService partitionService = nodeEngine.getPartitionService(); OperationService operationService = nodeEngine.getOperationService(); SerializableXID serializableXID = new SerializableXID(xid.getFormatId(), xid.getGlobalTransactionId(), xid.getBranchQualifier()); Data xidData = nodeEngine.toData(serializableXID); int partitionId = partitionService.getPartitionId(xidData); FinalizeRemoteTransactionOperation operation = new FinalizeRemoteTransactionOperation(xidData, isCommit); InternalCompletableFuture<Integer> future = operationService.invokeOnPartition(SERVICE_NAME, operation, partitionId); Integer errorCode; try { errorCode = future.get(); } catch (Exception e) { throw ExceptionUtil.rethrow(e); } if (errorCode != null) { throw new XAException(errorCode); } } private void clearRemoteTransactions(Xid xid) { NodeEngine nodeEngine = getNodeEngine(); IPartitionService partitionService = nodeEngine.getPartitionService(); OperationService operationService = nodeEngine.getOperationService(); SerializableXID serializableXID = new SerializableXID(xid.getFormatId(), xid.getGlobalTransactionId(), xid.getBranchQualifier()); Data xidData = nodeEngine.toData(serializableXID); int partitionId = partitionService.getPartitionId(xidData); ClearRemoteTransactionOperation operation = new ClearRemoteTransactionOperation(xidData); operationService.invokeOnPartition(SERVICE_NAME, operation, partitionId); } @Override public void forget(Xid xid) throws XAException { List<TransactionContext> contexts = xidContextMap.remove(xid); if (contexts == null) { throw new XAException("No context with the given xid: " + xid); } clearRemoteTransactions(xid); } @Override public boolean isSameRM(XAResource xaResource) throws XAException { if (this == xaResource) { return true; } if (xaResource instanceof XAResourceImpl) { XAResourceImpl otherXaResource = (XAResourceImpl) xaResource; return groupName.equals(otherXaResource.groupName); } return xaResource.isSameRM(this); } @Override public Xid[] recover(int flag) throws XAException { NodeEngine nodeEngine = getNodeEngine(); XAService xaService = getService(); OperationService operationService = nodeEngine.getOperationService(); ClusterService clusterService = nodeEngine.getClusterService(); Collection<Member> memberList = clusterService.getMembers(); List<Future<SerializableList>> futureList = new ArrayList<Future<SerializableList>>(); for (Member member : memberList) { if (member.localMember()) { continue; } CollectRemoteTransactionsOperation op = new CollectRemoteTransactionsOperation(); Address address = member.getAddress(); InternalCompletableFuture<SerializableList> future = operationService.invokeOnTarget(SERVICE_NAME, op, address); futureList.add(future); } HashSet<SerializableXID> xids = new HashSet<SerializableXID>(); xids.addAll(xaService.getPreparedXids()); for (Future<SerializableList> future : futureList) { try { SerializableList xidSet = future.get(); for (Data xidData : xidSet) { SerializableXID xid = nodeEngine.toObject(xidData); xids.add(xid); } } catch (InterruptedException e) { throw new XAException(XAException.XAER_RMERR); } catch (MemberLeftException e) { logger.warning("Member left while recovering", e); } catch (ExecutionException e) { Throwable cause = e.getCause(); if (cause instanceof HazelcastInstanceNotActiveException || cause instanceof TargetNotMemberException) { logger.warning("Member left while recovering", e); } else { throw new XAException(XAException.XAER_RMERR); } } } return xids.toArray(new SerializableXID[xids.size()]); } @Override public int getTransactionTimeout() throws XAException { return timeoutInSeconds.get(); } @Override public boolean setTransactionTimeout(int seconds) throws XAException { timeoutInSeconds.set(seconds == 0 ? DEFAULT_TIMEOUT_SECONDS : seconds); return true; } @Override public String getServiceName() { return SERVICE_NAME; } @Override public String getName() { return SERVICE_NAME; } @Override public TransactionContext getTransactionContext() { long threadId = Thread.currentThread().getId(); TransactionContext transactionContext = threadContextMap.get(threadId); if (transactionContext == null) { throw new IllegalStateException("No TransactionContext associated with current thread: " + threadId); } return transactionContext; } public String getGroupName() { return groupName; } private Transaction getTransaction(TransactionContext context) { return ((XATransactionContextImpl) context).getTransaction(); } private long currentThreadId() { return Thread.currentThread().getId(); } @Override public String toString() { return "HazelcastXaResource {" + groupName + '}'; } }