/** * diqube: Distributed Query Base. * * Copyright (C) 2015 Bastian Gloeckle * * This file is part of diqube. * * diqube is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.diqube.metadata; import java.lang.Thread.UncaughtExceptionHandler; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.function.Function; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import javax.inject.Inject; import org.diqube.consensus.ConsensusClient; import org.diqube.consensus.ConsensusClient.ClosableProvider; import org.diqube.consensus.ConsensusClient.ConsensusClusterUnavailableException; import org.diqube.consensus.internal.DiqubeCatalystSerializer; import org.diqube.context.AutoInstatiate; import org.diqube.context.InjectOptional; import org.diqube.metadata.consensus.TableMetadataStateMachine; import org.diqube.metadata.consensus.TableMetadataStateMachine.CompareAndSetTableMetadata; import org.diqube.metadata.consensus.TableMetadataStateMachine.GetTableMetadata; import org.diqube.metadata.consensus.TableMetadataStateMachine.RecomputeTableMetadata; import org.diqube.metadata.consensus.TableMetadataStateMachineImplementation; import org.diqube.metadata.create.TableMetadataRecomputeRequestListener; import org.diqube.metadata.util.CurrentFlattenedTableNameUtil; import org.diqube.metadata.util.CurrentFlattenedTableNameUtil.FlattenIdentificationImpossibleException; import org.diqube.name.FlattenedTableNameUtil; import org.diqube.threads.ExecutorManager; import org.diqube.thrift.base.thrift.AuthorizationException; import org.diqube.thrift.base.thrift.TableMetadata; import org.diqube.util.Pair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Manages table metadata across the cluster using an internal consensus state machine. * * <p> * This class manages calling the {@link TableMetadataRecomputeRequestListener}s in a separate thread pool. * * @author Bastian Gloeckle */ @AutoInstatiate public class DefaultTableMetadataManager implements TableMetadataManager { private static final Logger logger = LoggerFactory.getLogger(DefaultTableMetadataManager.class); @Inject private ConsensusClient consensusClient; @Inject private TableMetadataStateMachineImplementation tableMetadataStateMachineImplementation; @InjectOptional private List<TableMetadataRecomputeRequestListener> recomputeListeners; @InjectOptional private ExecutorManager executorManager; @Inject private DiqubeCatalystSerializer diqubeCatalystSerializer; @Inject private CurrentFlattenedTableNameUtil currentFlattenedTableNameUtil; @Inject private FlattenedTableNameUtil flattenedTableNameUtil; private ExecutorService recomputeExecutor; @PostConstruct public void initialize() { tableMetadataStateMachineImplementation.setRecomputeConsumer(tableName -> requestLocalRecomputation(tableName)); recomputeExecutor = executorManager.newCachedThreadPoolWithMax("table-metadata-recompute-%d", new UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable e) { logger.error("Uncaught exception while recomputing table metadata. This node should be restarted.", e); } }, 1); } @PreDestroy public void cleanup() { if (recomputeExecutor != null) recomputeExecutor.shutdownNow(); } @Override public TableMetadata getCurrentTableMetadata(String tableName) throws AuthorizationException { if (flattenedTableNameUtil.isFlattenedTableName(tableName) && !flattenedTableNameUtil.isFullFlattenedTableName(tableName)) { try { tableName = currentFlattenedTableNameUtil.enhanceIncompleteFlattenedTableNameWithNewestFlattenId(tableName); } catch (FlattenIdentificationImpossibleException e) { logger.warn("Cannot get newest flatten ID for table '{}' of which metadata should've been loaded", tableName, e); // cannot identify any metadata. return null; } } Pair<TableMetadata, Long> current = null; try (ClosableProvider<TableMetadataStateMachine> p = consensusClient.getStateMachineClient(TableMetadataStateMachine.class)) { current = p.getClient().getTableMetadata(GetTableMetadata.local(tableName)); } catch (ConsensusClusterUnavailableException e) { return null; } return (current != null) ? current.getLeft() : null; } @Override public void adjustTableMetadata(String tableName, Function<TableMetadata, TableMetadata> adjustFunction) { try (ClosableProvider<TableMetadataStateMachine> p = consensusClient.getStateMachineClient(TableMetadataStateMachine.class)) { boolean success = false; while (!success) { Pair<TableMetadata, Long> currentMetadataPair = p.getClient().getTableMetadata(GetTableMetadata.local(tableName)); TableMetadata newMetadata; long previousVersion; if (currentMetadataPair == null) { newMetadata = adjustFunction.apply(null); previousVersion = Long.MIN_VALUE; } else { newMetadata = adjustFunction.apply(currentMetadataPair.getLeft()); previousVersion = currentMetadataPair.getRight(); } try { diqubeCatalystSerializer.validateSerializationObject(newMetadata); } catch (IllegalArgumentException e) { logger.warn("Cannot publish metadata for table {} because metadata cannot be accepted by consensus cluster.", tableName, e); return; } success = p.getClient().compareAndSetTableMetadata(CompareAndSetTableMetadata.local(previousVersion, newMetadata)); } logger.debug("Sent the current metadata of the local '{}' table to the cluster.", tableName); } catch (ConsensusClusterUnavailableException e) { logger.warn("Could not publish metadata becuase consensus cluster is not available", e); } } @Override public void startRecomputingTableMetadata(String tableName) { try (ClosableProvider<TableMetadataStateMachine> p = consensusClient.getStateMachineClient(TableMetadataStateMachine.class)) { p.getClient().recomputeTableMetadata(RecomputeTableMetadata.local(tableName)); logger.debug("Informing cluster that metadata for table '{}' needs to be recomputed.", tableName); } catch (ConsensusClusterUnavailableException e) { throw new IllegalStateException("Consensus cluster not available", e); } } /** * A request to recompute metadata has arrived us from another cluster node (perhaps even we ourselves started that!). */ private void requestLocalRecomputation(String tableName) { if (recomputeListeners != null) { // execute listeners on different thread, because this method is effectively called by the state machine thread of // the consensus implementation - we must not block it. In addition, we expect the listeners to recompute and send // the results again -> another interaction with consensus, and this cannot happen while still working on applying // a commit to the local state machine. for (TableMetadataRecomputeRequestListener listener : recomputeListeners) { recomputeExecutor.execute(() -> listener.tableMetadataRecomputeRequestReceived(tableName)); } } } }