io.atomix.core.impl.CoreTransactionService Maven / Gradle / Ivy
/*
* Copyright 2017-present Open Networking Foundation
*
* 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 io.atomix.core.impl;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableSet;
import io.atomix.cluster.ClusterMembershipEvent;
import io.atomix.cluster.ClusterMembershipEventListener;
import io.atomix.cluster.MemberId;
import io.atomix.core.iterator.AsyncIterator;
import io.atomix.core.map.AsyncAtomicMap;
import io.atomix.core.map.AtomicMapConfig;
import io.atomix.core.map.AtomicMapType;
import io.atomix.core.transaction.ManagedTransactionService;
import io.atomix.core.transaction.ParticipantInfo;
import io.atomix.core.transaction.TransactionException;
import io.atomix.core.transaction.TransactionId;
import io.atomix.core.transaction.TransactionService;
import io.atomix.core.transaction.TransactionState;
import io.atomix.core.transaction.Transactional;
import io.atomix.primitive.DistributedPrimitive;
import io.atomix.primitive.PrimitiveBuilder;
import io.atomix.primitive.PrimitiveManagementService;
import io.atomix.primitive.PrimitiveType;
import io.atomix.primitive.partition.PartitionGroup;
import io.atomix.primitive.protocol.PrimitiveProtocol;
import io.atomix.primitive.protocol.ProxyCompatibleBuilder;
import io.atomix.primitive.protocol.ProxyProtocol;
import io.atomix.utils.concurrent.Futures;
import io.atomix.utils.serializer.Namespace;
import io.atomix.utils.serializer.Namespaces;
import io.atomix.utils.serializer.Serializer;
import io.atomix.utils.time.Versioned;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
/**
* Core transaction service.
*/
public class CoreTransactionService implements ManagedTransactionService {
private static final Logger LOGGER = LoggerFactory.getLogger(CoreTransactionService.class);
private static final Serializer SERIALIZER = Serializer.using(Namespace.builder()
.register(Namespaces.BASIC)
.register(MemberId.class)
.register(TransactionId.class)
.register(TransactionState.class)
.register(ParticipantInfo.class)
.register(TransactionInfo.class)
.build());
private final PrimitiveManagementService managementService;
private final MemberId localMemberId;
private final ClusterMembershipEventListener clusterEventListener = this::onMembershipChange;
private volatile AsyncAtomicMap transactions;
private final AtomicBoolean started = new AtomicBoolean();
public CoreTransactionService(PrimitiveManagementService managementService) {
this.managementService = checkNotNull(managementService);
this.localMemberId = managementService.getMembershipService().getLocalMember().id();
}
@Override
public Set getActiveTransactions() {
checkState(isRunning());
return transactions.sync().keySet();
}
@Override
public TransactionState getTransactionState(TransactionId transactionId) {
checkState(isRunning());
TransactionInfo info = Versioned.valueOrNull(transactions.get(transactionId).join());
return info != null ? info.state : null;
}
@Override
public CompletableFuture begin() {
checkState(isRunning());
TransactionId transactionId = TransactionId.from(UUID.randomUUID().toString());
TransactionInfo info = new TransactionInfo(localMemberId, TransactionState.ACTIVE, ImmutableSet.of());
return transactions.put(transactionId, info).thenApply(v -> transactionId);
}
@Override
public CompletableFuture preparing(TransactionId transactionId, Set participants) {
checkState(isRunning());
return transactions.compute(transactionId, (id, info) -> {
if (info == null) {
return null;
} else if (info.state == TransactionState.ACTIVE && info.coordinator.equals(localMemberId)) {
return new TransactionInfo(info.coordinator, TransactionState.PREPARING, participants);
} else {
return info;
}
}).thenCompose(value -> {
if (value == null || value.value() == null) {
return Futures.exceptionalFuture(new TransactionException("Unknown transaction " + transactionId));
} else if (value.value().state != TransactionState.PREPARING) {
return Futures.exceptionalFuture(new TransactionException("Concurrent transaction modification " + transactionId));
} else if (!value.value().coordinator.equals(localMemberId)) {
return Futures.exceptionalFuture(new TransactionException("Transaction " + transactionId + " recovered by another member"));
}
return Futures.completedFuture(null);
});
}
@Override
public CompletableFuture committing(TransactionId transactionId) {
checkState(isRunning());
return transactions.compute(transactionId, (id, info) -> {
if (info == null) {
return null;
} else if (info.state == TransactionState.PREPARING && info.coordinator.equals(localMemberId)) {
return new TransactionInfo(info.coordinator, TransactionState.COMMITTING, info.participants);
} else {
return info;
}
}).thenCompose(value -> {
if (value == null || value.value() == null) {
return Futures.exceptionalFuture(new TransactionException("Unknown transaction " + transactionId));
} else if (value.value().state != TransactionState.COMMITTING) {
return Futures.exceptionalFuture(new TransactionException("Concurrent transaction modification " + transactionId));
} else if (!value.value().coordinator.equals(localMemberId)) {
return Futures.exceptionalFuture(new TransactionException("Transaction " + transactionId + " recovered by another member"));
}
return Futures.completedFuture(null);
});
}
@Override
public CompletableFuture aborting(TransactionId transactionId) {
checkState(isRunning());
return transactions.compute(transactionId, (id, info) -> {
if (info == null) {
return null;
} else if (info.state == TransactionState.PREPARING && info.coordinator.equals(localMemberId)) {
return new TransactionInfo(info.coordinator, TransactionState.ROLLING_BACK, info.participants);
} else {
return info;
}
}).thenCompose(value -> {
if (value == null || value.value() == null) {
return Futures.exceptionalFuture(new TransactionException("Unknown transaction " + transactionId));
} else if (value.value().state != TransactionState.ROLLING_BACK) {
return Futures.exceptionalFuture(new TransactionException("Concurrent transaction modification " + transactionId));
} else if (!value.value().coordinator.equals(localMemberId)) {
return Futures.exceptionalFuture(new TransactionException("Transaction " + transactionId + " recovered by another member"));
}
return Futures.completedFuture(null);
});
}
@Override
public CompletableFuture complete(TransactionId transactionId) {
checkState(isRunning());
return transactions.remove(transactionId).thenApply(v -> null);
}
/**
* Handles a cluster membership change event.
*/
private void onMembershipChange(ClusterMembershipEvent event) {
if (event.type() == ClusterMembershipEvent.Type.MEMBER_REMOVED) {
recoverTransactions(transactions.entrySet().iterator(), event.subject().id());
}
}
/**
* Recursively recovers transactions using the given iterator.
*
* @param iterator the asynchronous iterator from which to recover transactions
* @param memberId the transaction member ID
*/
private void recoverTransactions(AsyncIterator>> iterator, MemberId memberId) {
iterator.next().thenAccept(entry -> {
if (entry.getValue().value().coordinator.equals(memberId)) {
recoverTransaction(entry.getKey(), entry.getValue().value());
}
recoverTransactions(iterator, memberId);
});
}
/**
* Recovers and completes the given transaction.
*
* @param transactionId the transaction identifier
* @param transactionInfo the transaction info
*/
private void recoverTransaction(TransactionId transactionId, TransactionInfo transactionInfo) {
switch (transactionInfo.state) {
case PREPARING:
completePreparingTransaction(transactionId);
break;
case COMMITTING:
completeCommittingTransaction(transactionId);
break;
case ROLLING_BACK:
completeRollingBackTransaction(transactionId);
break;
default:
break;
}
}
/**
* Completes a transaction in the {@link TransactionState#PREPARING} state.
*
* @param transactionId the transaction identifier for the transaction to complete
*/
private void completePreparingTransaction(TransactionId transactionId) {
// Change ownership of the transaction to the local node and set its state to ROLLING_BACK and then roll it back.
completeTransaction(
transactionId,
TransactionState.PREPARING,
info -> new TransactionInfo(localMemberId, TransactionState.ROLLING_BACK, info.participants),
info -> info.state == TransactionState.ROLLING_BACK,
(id, transactional) -> transactional.rollback(id))
.whenComplete((result, error) -> {
if (error != null) {
error = Throwables.getRootCause(error);
if (error instanceof TransactionException) {
LOGGER.warn("Failed to complete transaction", error);
} else {
LOGGER.warn("Failed to roll back transaction " + transactionId);
}
}
});
}
/**
* Completes a transaction in the {@link TransactionState#PREPARING} state.
*
* @param transactionId the transaction identifier for the transaction to complete
*/
private void completeCommittingTransaction(TransactionId transactionId) {
// Change ownership of the transaction to the local node and then commit it.
completeTransaction(
transactionId,
TransactionState.COMMITTING,
info -> new TransactionInfo(localMemberId, TransactionState.COMMITTING, info.participants),
info -> info.state == TransactionState.COMMITTING && info.coordinator.equals(localMemberId),
(id, transactional) -> transactional.commit(id))
.whenComplete((result, error) -> {
if (error != null) {
error = Throwables.getRootCause(error);
if (error instanceof TransactionException) {
LOGGER.warn("Failed to complete transaction", error);
} else {
LOGGER.warn("Failed to commit transaction " + transactionId);
}
}
});
}
/**
* Completes a transaction in the {@link TransactionState#PREPARING} state.
*
* @param transactionId the transaction identifier for the transaction to complete
*/
private void completeRollingBackTransaction(TransactionId transactionId) {
// Change ownership of the transaction to the local node and then roll it back.
completeTransaction(
transactionId,
TransactionState.ROLLING_BACK,
info -> new TransactionInfo(localMemberId, TransactionState.ROLLING_BACK, info.participants),
info -> info.state == TransactionState.ROLLING_BACK && info.coordinator.equals(localMemberId),
(id, transactional) -> transactional.rollback(id))
.whenComplete((result, error) -> {
if (error != null) {
error = Throwables.getRootCause(error);
if (error instanceof TransactionException) {
LOGGER.warn("Failed to complete transaction", error);
} else {
LOGGER.warn("Failed to roll back transaction " + transactionId);
}
}
});
}
/**
* Completes a transaction by modifying the transaction state to change ownership to this member and then completing
* the transaction based on the existing transaction state.
*/
@SuppressWarnings("unchecked")
private CompletableFuture completeTransaction(
TransactionId transactionId,
TransactionState expectState,
Function updateFunction,
Predicate updatedPredicate,
BiFunction, CompletableFuture> completionFunction) {
return transactions.compute(transactionId, (id, info) -> {
if (info == null) {
return null;
} else if (info.state == expectState) {
return updateFunction.apply(info);
} else {
return info;
}
}).thenCompose(value -> {
if (value != null && updatedPredicate.test(value.value())) {
return Futures.allOf(value.value().participants.stream()
.map(participantInfo -> completeParticipant(participantInfo, info -> completionFunction.apply(transactionId, info))))
.thenApply(v -> null);
}
return Futures.exceptionalFuture(new TransactionException("Failed to acquire transaction lock"));
});
}
/**
* Completes an individual participant in a transaction by loading the primitive by type/protocol/partition group and
* applying the given completion function to it.
*/
@SuppressWarnings("unchecked")
private CompletableFuture completeParticipant(
ParticipantInfo participantInfo,
Function, CompletableFuture> completionFunction) {
// Look up the primitive type for the participant. If the primitive type is not found, return an exception.
PrimitiveType primitiveType = managementService.getPrimitiveTypeRegistry().getPrimitiveType(participantInfo.type());
if (primitiveType == null) {
return Futures.exceptionalFuture(new TransactionException("Failed to locate primitive type " + participantInfo.type() + " for participant " + participantInfo.name()));
}
// Look up the protocol type for the participant.
PrimitiveProtocol.Type protocolType = managementService.getProtocolTypeRegistry().getProtocolType(participantInfo.protocol());
if (protocolType == null) {
return Futures.exceptionalFuture(new TransactionException("Failed to locate protocol type for participant " + participantInfo.name()));
}
// Look up the partition group in which the primitive is stored.
PartitionGroup partitionGroup;
if (participantInfo.group() == null) {
partitionGroup = managementService.getPartitionService().getPartitionGroup(protocolType);
} else {
partitionGroup = managementService.getPartitionService().getPartitionGroup(participantInfo.group());
}
// If the partition group is not found, return an exception.
if (partitionGroup == null) {
return Futures.exceptionalFuture(new TransactionException("Failed to locate partition group for participant " + participantInfo.name()));
}
PrimitiveBuilder builder = primitiveType.newBuilder(participantInfo.name(), primitiveType.newConfig(), managementService);
((ProxyCompatibleBuilder) builder).withProtocol(partitionGroup.newProtocol());
DistributedPrimitive primitive = builder.build();
return completionFunction.apply((Transactional>) primitive);
}
@Override
@SuppressWarnings("unchecked")
public CompletableFuture start() {
PrimitiveProtocol protocol = managementService.getPartitionService().getSystemPartitionGroup().newProtocol();
return AtomicMapType.instance()
.newBuilder("atomix-transactions", new AtomicMapConfig(), managementService)
.withSerializer(SERIALIZER)
.withProtocol((ProxyProtocol) protocol)
.withCacheEnabled()
.buildAsync()
.thenApply(transactions -> {
this.transactions = transactions.async();
managementService.getMembershipService().addListener(clusterEventListener);
LOGGER.info("Started");
started.set(true);
return this;
});
}
@Override
public boolean isRunning() {
return started.get();
}
@Override
public CompletableFuture stop() {
if (started.compareAndSet(true, false)) {
managementService.getMembershipService().removeListener(clusterEventListener);
return transactions.close().exceptionally(e -> null);
}
return CompletableFuture.completedFuture(null);
}
/**
* Transaction info.
*/
private static class TransactionInfo {
private final MemberId coordinator;
private final TransactionState state;
private final Set participants;
TransactionInfo(MemberId coordinator, TransactionState state, Set participants) {
this.coordinator = coordinator;
this.state = state;
this.participants = participants;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy