/*
* 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.facebook.presto.transaction;
import com.facebook.presto.connector.ConnectorId;
import com.facebook.presto.metadata.Catalog;
import com.facebook.presto.metadata.CatalogManager;
import com.facebook.presto.metadata.CatalogMetadata;
import com.facebook.presto.spi.PrestoException;
import com.facebook.presto.spi.connector.Connector;
import com.facebook.presto.spi.connector.ConnectorMetadata;
import com.facebook.presto.spi.connector.ConnectorTransactionHandle;
import com.facebook.presto.spi.transaction.IsolationLevel;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import io.airlift.concurrent.BoundedExecutor;
import io.airlift.concurrent.ExecutorServiceAdapter;
import io.airlift.log.Logger;
import io.airlift.units.Duration;
import org.joda.time.DateTime;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import static com.facebook.presto.spi.StandardErrorCode.AUTOCOMMIT_WRITE_CONFLICT;
import static com.facebook.presto.spi.StandardErrorCode.MULTI_CATALOG_WRITE_CONFLICT;
import static com.facebook.presto.spi.StandardErrorCode.NOT_FOUND;
import static com.facebook.presto.spi.StandardErrorCode.READ_ONLY_VIOLATION;
import static com.facebook.presto.spi.StandardErrorCode.TRANSACTION_ALREADY_ABORTED;
import static com.facebook.presto.spi.StandardErrorCode.UNKNOWN_TRANSACTION;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Verify.verify;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.util.concurrent.Futures.immediateFailedFuture;
import static com.google.common.util.concurrent.Futures.immediateFuture;
import static com.google.common.util.concurrent.Futures.nonCancellationPropagating;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static com.google.common.util.concurrent.MoreExecutors.listeningDecorator;
import static io.airlift.concurrent.MoreFutures.addExceptionCallback;
import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.stream.Collectors.toList;
@ThreadSafe
public class TransactionManager
{
private static final Logger log = Logger.get(TransactionManager.class);
public static final IsolationLevel DEFAULT_ISOLATION = IsolationLevel.READ_UNCOMMITTED;
public static final boolean DEFAULT_READ_ONLY = false;
private final Duration idleTimeout;
private final int maxFinishingConcurrency;
private final ConcurrentMap<TransactionId, TransactionMetadata> transactions = new ConcurrentHashMap<>();
private final CatalogManager catalogManager;
private final Executor finishingExecutor;
private TransactionManager(Duration idleTimeout, int maxFinishingConcurrency, CatalogManager catalogManager, Executor finishingExecutor)
{
this.catalogManager = catalogManager;
requireNonNull(idleTimeout, "idleTimeout is null");
checkArgument(maxFinishingConcurrency > 0, "maxFinishingConcurrency must be at least 1");
requireNonNull(finishingExecutor, "finishingExecutor is null");
this.idleTimeout = idleTimeout;
this.maxFinishingConcurrency = maxFinishingConcurrency;
this.finishingExecutor = finishingExecutor;
}
public static TransactionManager create(
TransactionManagerConfig config,
ScheduledExecutorService idleCheckExecutor,
CatalogManager catalogManager,
ExecutorService finishingExecutor)
{
TransactionManager transactionManager = new TransactionManager(config.getIdleTimeout(), config.getMaxFinishingConcurrency(), catalogManager, finishingExecutor);
transactionManager.scheduleIdleChecks(config.getIdleCheckInterval(), idleCheckExecutor);
return transactionManager;
}
public static TransactionManager createTestTransactionManager()
{
return createTestTransactionManager(new CatalogManager());
}
public static TransactionManager createTestTransactionManager(CatalogManager catalogManager)
{
// No idle checks needed
return new TransactionManager(new Duration(1, TimeUnit.DAYS), 1, catalogManager, directExecutor());
}
private void scheduleIdleChecks(Duration idleCheckInterval, ScheduledExecutorService idleCheckExecutor)
{
idleCheckExecutor.scheduleWithFixedDelay(() -> {
try {
cleanUpExpiredTransactions();
}
catch (Throwable t) {
log.error(t, "Unexpected exception while cleaning up expired transactions");
}
}, idleCheckInterval.toMillis(), idleCheckInterval.toMillis(), MILLISECONDS);
}
private synchronized void cleanUpExpiredTransactions()
{
Iterator<Entry<TransactionId, TransactionMetadata>> iterator = transactions.entrySet().iterator();
while (iterator.hasNext()) {
Entry<TransactionId, TransactionMetadata> entry = iterator.next();
if (entry.getValue().isExpired(idleTimeout)) {
iterator.remove();
log.info("Removing expired transaction: %s", entry.getKey());
entry.getValue().asyncAbort();
}
}
}
public TransactionInfo getTransactionInfo(TransactionId transactionId)
{
return getTransactionMetadata(transactionId).getTransactionInfo();
}
public List<TransactionInfo> getAllTransactionInfos()
{
return transactions.values().stream()
.map(TransactionMetadata::getTransactionInfo)
.collect(toImmutableList());
}
public TransactionId beginTransaction(boolean autoCommitContext)
{
return beginTransaction(DEFAULT_ISOLATION, DEFAULT_READ_ONLY, autoCommitContext);
}
public TransactionId beginTransaction(IsolationLevel isolationLevel, boolean readOnly, boolean autoCommitContext)
{
TransactionId transactionId = TransactionId.create();
BoundedExecutor executor = new BoundedExecutor(finishingExecutor, maxFinishingConcurrency);
TransactionMetadata transactionMetadata = new TransactionMetadata(transactionId, isolationLevel, readOnly, autoCommitContext, catalogManager, executor);
checkState(transactions.put(transactionId, transactionMetadata) == null, "Duplicate transaction ID: %s", transactionId);
return transactionId;
}
public Map<String, ConnectorId> getCatalogNames(TransactionId transactionId)
{
return getTransactionMetadata(transactionId).getCatalogNames();
}
public Optional<CatalogMetadata> getOptionalCatalogMetadata(TransactionId transactionId, String catalogName)
{
TransactionMetadata transactionMetadata = getTransactionMetadata(transactionId);
return transactionMetadata.getConnectorId(catalogName)
.map(transactionMetadata::getTransactionCatalogMetadata);
}
public CatalogMetadata getCatalogMetadata(TransactionId transactionId, ConnectorId connectorId)
{
return getTransactionMetadata(transactionId).getTransactionCatalogMetadata(connectorId);
}
public CatalogMetadata getCatalogMetadataForWrite(TransactionId transactionId, ConnectorId connectorId)
{
CatalogMetadata catalogMetadata = getCatalogMetadata(transactionId, connectorId);
checkConnectorWrite(transactionId, connectorId);
return catalogMetadata;
}
public CatalogMetadata getCatalogMetadataForWrite(TransactionId transactionId, String catalogName)
{
TransactionMetadata transactionMetadata = getTransactionMetadata(transactionId);
// there is no need to ask for a connector specific id since the overlay connectors are read only
ConnectorId connectorId = transactionMetadata.getConnectorId(catalogName)
.orElseThrow(() -> new PrestoException(NOT_FOUND, "Catalog does not exist: " + catalogName));
return getCatalogMetadataForWrite(transactionId, connectorId);
}
public ConnectorTransactionHandle getConnectorTransaction(TransactionId transactionId, ConnectorId connectorId)
{
return getCatalogMetadata(transactionId, connectorId).getTransactionHandleFor(connectorId);
}
private void checkConnectorWrite(TransactionId transactionId, ConnectorId connectorId)
{
getTransactionMetadata(transactionId).checkConnectorWrite(connectorId);
}
public void checkAndSetActive(TransactionId transactionId)
{
TransactionMetadata metadata = getTransactionMetadata(transactionId);
metadata.checkOpenTransaction();
metadata.setActive();
}
public void trySetActive(TransactionId transactionId)
{
tryGetTransactionMetadata(transactionId).ifPresent(TransactionMetadata::setActive);
}
public void trySetInactive(TransactionId transactionId)
{
tryGetTransactionMetadata(transactionId).ifPresent(TransactionMetadata::setInActive);
}
private TransactionMetadata getTransactionMetadata(TransactionId transactionId)
{
TransactionMetadata transactionMetadata = transactions.get(transactionId);
if (transactionMetadata == null) {
throw unknownTransactionError(transactionId);
}
return transactionMetadata;
}
private Optional<TransactionMetadata> tryGetTransactionMetadata(TransactionId transactionId)
{
return Optional.ofNullable(transactions.get(transactionId));
}
private ListenableFuture<TransactionMetadata> removeTransactionMetadataAsFuture(TransactionId transactionId)
{
TransactionMetadata transactionMetadata = transactions.remove(transactionId);
if (transactionMetadata == null) {
return immediateFailedFuture(unknownTransactionError(transactionId));
}
return immediateFuture(transactionMetadata);
}
private static PrestoException unknownTransactionError(TransactionId transactionId)
{
return new PrestoException(UNKNOWN_TRANSACTION, format("Unknown transaction ID: %s. Possibly expired? Commands ignored until end of transaction block", transactionId));
}
public ListenableFuture<?> asyncCommit(TransactionId transactionId)
{
return nonCancellationPropagating(Futures.transformAsync(removeTransactionMetadataAsFuture(transactionId), TransactionMetadata::asyncCommit));
}
public ListenableFuture<?> asyncAbort(TransactionId transactionId)
{
return nonCancellationPropagating(Futures.transformAsync(removeTransactionMetadataAsFuture(transactionId), TransactionMetadata::asyncAbort));
}
public void fail(TransactionId transactionId)
{
// Mark transaction as failed, but don't remove it.
tryGetTransactionMetadata(transactionId).ifPresent(TransactionMetadata::asyncAbort);
}
@ThreadSafe
private static class TransactionMetadata
{
private final DateTime createTime = DateTime.now();
private final CatalogManager catalogManager;
private final TransactionId transactionId;
private final IsolationLevel isolationLevel;
private final boolean readOnly;
private final boolean autoCommitContext;
@GuardedBy("this")
private final Map<ConnectorId, ConnectorTransactionMetadata> connectorIdToMetadata = new ConcurrentHashMap<>();
@GuardedBy("this")
private final AtomicReference<ConnectorId> writtenConnectorId = new AtomicReference<>();
private final ListeningExecutorService finishingExecutor;
private final AtomicReference<Boolean> completedSuccessfully = new AtomicReference<>();
private final AtomicReference<Long> idleStartTime = new AtomicReference<>();
@GuardedBy("this")
private final Map<String, Optional<Catalog>> catalogByName = new ConcurrentHashMap<>();
@GuardedBy("this")
private final Map<ConnectorId, Catalog> catalogsByConnectorId = new ConcurrentHashMap<>();
@GuardedBy("this")
private final Map<ConnectorId, CatalogMetadata> catalogMetadata = new ConcurrentHashMap<>();
public TransactionMetadata(
TransactionId transactionId,
IsolationLevel isolationLevel,
boolean readOnly,
boolean autoCommitContext,
CatalogManager catalogManager,
Executor finishingExecutor)
{
this.transactionId = requireNonNull(transactionId, "transactionId is null");
this.isolationLevel = requireNonNull(isolationLevel, "isolationLevel is null");
this.readOnly = readOnly;
this.autoCommitContext = autoCommitContext;
this.catalogManager = requireNonNull(catalogManager, "catalogManager is null");
this.finishingExecutor = listeningDecorator(ExecutorServiceAdapter.from(requireNonNull(finishingExecutor, "finishingExecutor is null")));
}
public void setActive()
{
idleStartTime.set(null);
}
public void setInActive()
{
idleStartTime.set(System.nanoTime());
}
public boolean isExpired(Duration idleTimeout)
{
Long idleStartTime = this.idleStartTime.get();
return idleStartTime != null && Duration.nanosSince(idleStartTime).compareTo(idleTimeout) > 0;
}
public void checkOpenTransaction()
{
Boolean completedStatus = this.completedSuccessfully.get();
if (completedStatus != null) {
if (completedStatus) {
// Should not happen normally
throw new IllegalStateException("Current transaction already committed");
}
else {
throw new PrestoException(TRANSACTION_ALREADY_ABORTED, "Current transaction is aborted, commands ignored until end of transaction block");
}
}
}
private synchronized Map<String, ConnectorId> getCatalogNames()
{
// todo if repeatable read, this must be recorded
Map<String, ConnectorId> catalogNames = new HashMap<>();
catalogByName.values().stream()
.filter(Optional::isPresent)
.map(Optional::get)
.forEach(catalog -> catalogNames.put(catalog.getCatalogName(), catalog.getConnectorId()));
catalogManager.getCatalogs().stream()
.forEach(catalog -> catalogNames.putIfAbsent(catalog.getCatalogName(), catalog.getConnectorId()));
return ImmutableMap.copyOf(catalogNames);
}
private synchronized Optional<ConnectorId> getConnectorId(String catalogName)
{
Optional<Catalog> catalog = catalogByName.get(catalogName);
if (catalog == null) {
catalog = catalogManager.getCatalog(catalogName);
catalogByName.put(catalogName, catalog);
if (catalog.isPresent()) {
registerCatalog(catalog.get());
}
}
return catalog.map(Catalog::getConnectorId);
}
private synchronized void registerCatalog(Catalog catalog)
{
catalogsByConnectorId.put(catalog.getConnectorId(), catalog);
catalogsByConnectorId.put(catalog.getInformationSchemaId(), catalog);
catalogsByConnectorId.put(catalog.getSystemTablesId(), catalog);
}
private synchronized CatalogMetadata getTransactionCatalogMetadata(ConnectorId connectorId)
{
checkOpenTransaction();
CatalogMetadata catalogMetadata = this.catalogMetadata.get(connectorId);
if (catalogMetadata == null) {
Catalog catalog = catalogsByConnectorId.get(connectorId);
verify(catalog != null, "Unknown connectorId: %s", connectorId);
ConnectorTransactionMetadata metadata = createConnectorTransactionMetadata(catalog.getConnectorId(), catalog);
ConnectorTransactionMetadata informationSchema = createConnectorTransactionMetadata(catalog.getInformationSchemaId(), catalog);
ConnectorTransactionMetadata systemTables = createConnectorTransactionMetadata(catalog.getSystemTablesId(), catalog);
catalogMetadata = new CatalogMetadata(
metadata.getConnectorId(), metadata.getConnectorMetadata(), metadata.getTransactionHandle(),
informationSchema.getConnectorId(), informationSchema.getConnectorMetadata(), informationSchema.getTransactionHandle(),
systemTables.getConnectorId(), systemTables.getConnectorMetadata(), systemTables.getTransactionHandle()
);
this.catalogMetadata.put(catalog.getConnectorId(), catalogMetadata);
this.catalogMetadata.put(catalog.getInformationSchemaId(), catalogMetadata);
this.catalogMetadata.put(catalog.getSystemTablesId(), catalogMetadata);
}
return catalogMetadata;
}
public synchronized ConnectorTransactionMetadata createConnectorTransactionMetadata(ConnectorId connectorId, Catalog catalog)
{
Connector connector = catalog.getConnector(connectorId);
ConnectorTransactionMetadata transactionMetadata = new ConnectorTransactionMetadata(connectorId, connector, beginTransaction(connector));
checkState(connectorIdToMetadata.put(connectorId, transactionMetadata) == null);
return transactionMetadata;
}
private ConnectorTransactionHandle beginTransaction(Connector connector)
{
if (connector instanceof InternalConnector) {
return ((InternalConnector) connector).beginTransaction(transactionId, isolationLevel, readOnly);
}
else {
return connector.beginTransaction(isolationLevel, readOnly);
}
}
public synchronized void checkConnectorWrite(ConnectorId connectorId)
{
checkOpenTransaction();
ConnectorTransactionMetadata transactionMetadata = connectorIdToMetadata.get(connectorId);
checkArgument(transactionMetadata != null, "Cannot record write for connector not part of transaction");
if (readOnly) {
throw new PrestoException(READ_ONLY_VIOLATION, "Cannot execute write in a read-only transaction");
}
if (!writtenConnectorId.compareAndSet(null, connectorId) && !writtenConnectorId.get().equals(connectorId)) {
throw new PrestoException(MULTI_CATALOG_WRITE_CONFLICT, "Multi-catalog writes not supported in a single transaction. Already wrote to catalog " + writtenConnectorId.get());
}
if (transactionMetadata.isSingleStatementWritesOnly() && !autoCommitContext) {
throw new PrestoException(AUTOCOMMIT_WRITE_CONFLICT, "Catalog " + connectorId + " only supports writes using autocommit");
}
}
public synchronized ListenableFuture<?> asyncCommit()
{
if (!completedSuccessfully.compareAndSet(null, true)) {
if (completedSuccessfully.get()) {
// Already done
return immediateFuture(null);
}
// Transaction already aborted
return immediateFailedFuture(new PrestoException(TRANSACTION_ALREADY_ABORTED, "Current transaction has already been aborted"));
}
ConnectorId writeConnectorId = this.writtenConnectorId.get();
if (writeConnectorId == null) {
ListenableFuture<?> future = Futures.allAsList(connectorIdToMetadata.values().stream()
.map(transactionMetadata -> finishingExecutor.submit(transactionMetadata::commit))
.collect(toList()));
addExceptionCallback(future, throwable -> {
abortInternal();
log.error(throwable, "Read-only connector should not throw exception on commit");
});
return nonCancellationPropagating(future);
}
Supplier<ListenableFuture<?>> commitReadOnlyConnectors = () -> {
ListenableFuture<? extends List<?>> future = Futures.allAsList(connectorIdToMetadata.entrySet().stream()
.filter(entry -> !entry.getKey().equals(writeConnectorId))
.map(Entry::getValue)
.map(transactionMetadata -> finishingExecutor.submit(transactionMetadata::commit))
.collect(toList()));
addExceptionCallback(future, throwable -> log.error(throwable, "Read-only connector should not throw exception on commit"));
return future;
};
ConnectorTransactionMetadata writeConnector = connectorIdToMetadata.get(writeConnectorId);
ListenableFuture<?> commitFuture = finishingExecutor.submit(writeConnector::commit);
ListenableFuture<?> readOnlyCommitFuture = Futures.transformAsync(commitFuture, ignored -> commitReadOnlyConnectors.get());
addExceptionCallback(readOnlyCommitFuture, this::abortInternal);
return nonCancellationPropagating(readOnlyCommitFuture);
}
public synchronized ListenableFuture<?> asyncAbort()
{
if (!completedSuccessfully.compareAndSet(null, false)) {
if (completedSuccessfully.get()) {
// Should not happen normally
return immediateFailedFuture(new IllegalStateException("Current transaction already committed"));
}
// Already done
return immediateFuture(null);
}
return abortInternal();
}
private synchronized ListenableFuture<?> abortInternal()
{
// the callbacks in statement performed on another thread so are safe
return nonCancellationPropagating(Futures.allAsList(connectorIdToMetadata.values().stream()
.map(connection -> finishingExecutor.submit(() -> safeAbort(connection)))
.collect(toList())));
}
private static void safeAbort(ConnectorTransactionMetadata connection)
{
try {
connection.abort();
}
catch (Exception e) {
log.error(e, "Connector threw exception on abort");
}
}
public TransactionInfo getTransactionInfo()
{
Duration idleTime = Optional.ofNullable(idleStartTime.get())
.map(Duration::nanosSince)
.orElse(new Duration(0, MILLISECONDS));
// dereferencing this field is safe because the field is atomic
@SuppressWarnings("FieldAccessNotGuarded") Optional<ConnectorId> writtenConnectorId = Optional.ofNullable(this.writtenConnectorId.get());
// copying the key set is safe here because the map is concurrent
@SuppressWarnings("FieldAccessNotGuarded") List<ConnectorId> connectorIds = ImmutableList.copyOf(connectorIdToMetadata.keySet());
return new TransactionInfo(transactionId, isolationLevel, readOnly, autoCommitContext, createTime, idleTime, connectorIds, writtenConnectorId);
}
private static class ConnectorTransactionMetadata
{
private final ConnectorId connectorId;
private final Connector connector;
private final ConnectorTransactionHandle transactionHandle;
private final ConnectorMetadata connectorMetadata;
private final AtomicBoolean finished = new AtomicBoolean();
public ConnectorTransactionMetadata(ConnectorId connectorId, Connector connector, ConnectorTransactionHandle transactionHandle)
{
this.connectorId = requireNonNull(connectorId, "connectorId is null");
this.connector = requireNonNull(connector, "connector is null");
this.transactionHandle = requireNonNull(transactionHandle, "transactionHandle is null");
this.connectorMetadata = connector.getMetadata(transactionHandle);
}
public ConnectorId getConnectorId()
{
return connectorId;
}
public boolean isSingleStatementWritesOnly()
{
return connector.isSingleStatementWritesOnly();
}
public synchronized ConnectorMetadata getConnectorMetadata()
{
checkState(!finished.get(), "Already finished");
return connectorMetadata;
}
public ConnectorTransactionHandle getTransactionHandle()
{
checkState(!finished.get(), "Already finished");
return transactionHandle;
}
public void commit()
{
if (finished.compareAndSet(false, true)) {
connector.commit(transactionHandle);
}
}
public void abort()
{
if (finished.compareAndSet(false, true)) {
connector.rollback(transactionHandle);
}
}
}
}
}