/** * 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.queries; import java.util.ArrayList; import java.util.Collection; import java.util.Deque; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.NavigableMap; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentNavigableMap; import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.stream.Collectors; import org.diqube.config.Config; import org.diqube.config.ConfigKey; import org.diqube.context.AutoInstatiate; import org.diqube.context.InjectOptional; import org.diqube.function.IntermediaryResult; import org.diqube.listeners.providers.OurNodeAddressStringProvider; import org.diqube.queries.QueryUuid.QueryUuidThreadState; import org.diqube.util.Pair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * All queries being executed are registered here. This class will be informed about any asynchronous exceptions that * are thrown inside the {@link ExecutorService}s of a query and will inform a specific {@link QueryExceptionHandler} * that can be installed for each query. * * <p> * All queries that will be executed should be (1) registered in this class and (2) all {@link Executor}s that will * execute anything for the query should be created by the ExecutorManager (diqube-threads), to make sure that these * threads have a valid {@link QueryUuidThreadState} set. * * @author Bastian Gloeckle */ @AutoInstatiate public class QueryRegistry { private static final Logger logger = LoggerFactory.getLogger(QueryRegistry.class); /** * Map from queryUuid/executionUuid pair to Pair of exception handler for that execution and a boolean indicating if * the queryUuid/executionUuid pair is the pair for the query master of that queryUuid. */ private ConcurrentNavigableMap<Pair<UUID, UUID>, Pair<QueryExceptionHandler, Boolean>> queryExecutionInformation = new ConcurrentSkipListMap<>(); private ConcurrentMap<UUID, Deque<QueryResultHandler>> resultHandlers = new ConcurrentHashMap<>(); private ConcurrentMap<UUID, Deque<QueryPercentHandler>> percentHandlers = new ConcurrentHashMap<>(); private ConcurrentMap<UUID, QueryStatsManager> queryStats = new ConcurrentHashMap<>(); private ConcurrentMap<UUID, Map<UUID, QueryStatsListener>> queryStatsListeners = new ConcurrentHashMap<>(); @Config(ConfigKey.OUR_HOST) private String ourHost; @Config(ConfigKey.PORT) private int ourPort; @InjectOptional private OurNodeAddressStringProvider ourNodeAddressProvider; /** * Register a query, its execution and its exception handler. Note that for the query * {@link #unregisterQueryExecution(UUID)} has to be called. For query UUID/execution UUID, see {@link QueryUuid} and * ExecutablePlan. */ public void registerQueryExecution(UUID queryUuid, UUID executionUuid, QueryExceptionHandler exceptionHandler, boolean isQueryMaster) { queryExecutionInformation.put(new Pair<>(queryUuid, executionUuid), new Pair<>(exceptionHandler, isQueryMaster)); } /** * Add a handler for percentages that were reported by remotes. * * Be sure to call {@link #removeRemotePercentHandler(UUID, QueryPercentHandler)}. */ public void addRemotePercentHandler(UUID queryUuid, QueryPercentHandler percentHandler) { synchronized (percentHandlers) { if (!percentHandlers.containsKey(queryUuid)) percentHandlers.put(queryUuid, new ConcurrentLinkedDeque<>()); percentHandlers.get(queryUuid).add(percentHandler); } } /** * Removes a {@link QueryPercentHandler} and releases any reserved resources. */ public void removeRemotePercentHandler(UUID queryUuid, QueryPercentHandler percentHandler) { if (percentHandlers.containsKey(queryUuid)) { synchronized (percentHandlers) { if (percentHandlers.containsKey(queryUuid)) { percentHandlers.get(queryUuid).remove(percentHandler); if (percentHandlers.get(queryUuid).isEmpty()) percentHandlers.remove(queryUuid); } } } } /** * Get the registered {@link QueryPercentHandler}s for a query or return an empty list. */ public List<QueryPercentHandler> getQueryPercentHandlers(UUID queryUuid) { Deque<QueryPercentHandler> handlers = percentHandlers.get(queryUuid); if (handlers == null) return new ArrayList<>(); return new ArrayList<>(handlers); } /** * Add a {@link QueryResultHandler} that will receive results of query remotes from a query execution. Please note * that the resultHandler needs to be {@link #removeQueryResultHandler(UUID, UUID, QueryResultHandler) unregistered} * again. This method only makes sense to be called on the query master. */ public void addQueryResultHandler(UUID queryUuid, QueryResultHandler resultHandler) { if (!resultHandlers.containsKey(queryUuid)) { synchronized (queryUuid) { if (!resultHandlers.containsKey(queryUuid)) { resultHandlers.put(queryUuid, new ConcurrentLinkedDeque<>()); } } } resultHandlers.get(queryUuid).add(resultHandler); } /** * Remove a specific resultHandler. */ public void removeQueryResultHandler(UUID queryUuid, QueryResultHandler resultHandler) { Deque<QueryResultHandler> deque = resultHandlers.get(queryUuid); if (deque == null) return; while (deque.remove(resultHandler)) ; if (deque.isEmpty()) { synchronized (queryUuid) { if (deque.isEmpty()) resultHandlers.remove(queryUuid); } } } /** * Get and return all {@link QueryResultHandler}s registered for a specific query. * * @return the result handlers. Could be empty. */ public Collection<QueryResultHandler> getQueryResultHandlers(UUID queryUuid) { Deque<QueryResultHandler> deque = resultHandlers.get(queryUuid); if (deque == null) return new ArrayList<>(); return new ArrayList<>(deque); } /** * Unregisters a query execution, all exceptions that will be thrown in one of the corresponding {@link Executor}s in * the future will not be passed on to the registered {@link QueryExceptionHandler} any more! */ public void unregisterQueryExecution(UUID queryUuid, UUID executionUuid) { logger.trace("Unregistering query {} execution {}", queryUuid, executionUuid); queryExecutionInformation.remove(new Pair<>(queryUuid, executionUuid)); queryStats.remove(executionUuid); if (queryStatsListeners.containsKey(queryUuid)) { synchronized (queryStatsListeners) { if (queryStatsListeners.containsKey(queryUuid)) { queryStatsListeners.get(queryUuid).remove(executionUuid); if (queryStatsListeners.get(queryUuid).isEmpty()) queryStatsListeners.remove(queryUuid); } } } } /** * Cleans up any resources that might be remaining for the given query. THis should only be called on the query * master: There might be a query remote being executed on the same node as the master and that node must not clear * any resources of the master! */ public void cleanupQueryFully(UUID queryUuid) { logger.trace("Cleaning up query {} fully.", queryUuid); // pair of queryUuid to executionUuid for the given query. Set<Pair<UUID, UUID>> queryExecutionPairs = new HashSet<>(); // exceptionHandlers is sorted. We pick a sub-map that will start with the Pair<UUID, UUID>s that contain our // queryUuid. As soon as we find another UUID as "left" we can stop traversing, because there will be no other pair // with our queryUuid. NavigableMap<Pair<UUID, UUID>, Pair<QueryExceptionHandler, Boolean>> searchMap = queryExecutionInformation.tailMap(new Pair<>(queryUuid, new UUID(Long.MIN_VALUE, Long.MIN_VALUE))); for (Pair<UUID, UUID> p : searchMap.keySet()) { if (!p.getLeft().equals(queryUuid)) break; queryExecutionPairs.add(p); } for (Pair<UUID, UUID> p : queryExecutionPairs) queryExecutionInformation.remove(p); Set<UUID> executionUuids = queryExecutionPairs.stream().map(p -> p.getRight()).collect(Collectors.toSet()); synchronized (percentHandlers) { percentHandlers.remove(queryUuid); } synchronized (queryStats) { for (UUID executionUuid : executionUuids) queryStats.remove(executionUuid); } synchronized (queryUuid) { resultHandlers.remove(queryUuid); } synchronized (queryStatsListeners) { queryStatsListeners.remove(queryUuid); } } /** * Tries to find the executionUuid of the query master of the given query. * * If it is not available locally, <code>null</code> will be returned. */ public UUID getMasterExecutionUuid(UUID queryUuid) { Pair<UUID, UUID> masterPair = null; NavigableMap<Pair<UUID, UUID>, Pair<QueryExceptionHandler, Boolean>> searchMap = queryExecutionInformation.tailMap(new Pair<>(queryUuid, new UUID(Long.MIN_VALUE, Long.MIN_VALUE))); for (Entry<Pair<UUID, UUID>, Pair<QueryExceptionHandler, Boolean>> e : searchMap.entrySet()) { if (!e.getKey().getLeft().equals(queryUuid)) break; if (e.getValue().getRight()) { masterPair = e.getKey(); break; } } if (masterPair == null) return null; return masterPair.getRight(); } /** * Call if an exception occurred while executing a given query. * * @return <code>true</code> when the exception was handled by an exception handler, <code>false</code> otherwise. */ public boolean handleException(UUID queryUuid, UUID executionUuid, Throwable t) { Pair<QueryExceptionHandler, Boolean> p = queryExecutionInformation.get(new Pair<>(queryUuid, executionUuid)); if (p == null) return false; p.getLeft().handleException(t); unregisterQueryExecution(queryUuid, executionUuid); return true; } /** * @return The currently active {@link QueryStats}, there is one created if not yet available. * @throws IllegalStateException * If current queryUuid or executionUuid cannot be found. */ public QueryStatsManager getOrCreateCurrentStatsManager() throws IllegalStateException { UUID queryUuid = QueryUuid.getCurrentQueryUuid(); UUID executionUuid = QueryUuid.getCurrentExecutionUuid(); if (queryUuid == null || executionUuid == null) throw new IllegalStateException("No current query and execution!"); return getOrCreateStatsManager(queryUuid, executionUuid); } /** * Gets, but does not create the current statistics * * @return the current {@link QueryStats} or <code>null</code>. * @throws IllegalStateException * If current executionUuid cannot be determined. */ public QueryStatsManager getCurrentStatsManager() throws IllegalStateException { UUID executionUuid = QueryUuid.getCurrentExecutionUuid(); if (executionUuid == null) throw new IllegalStateException("No current query and execution!"); return queryStats.get(executionUuid); } /** * Get or create a QueryStats object for the given query/execution. * * @return The active {@link QueryStats} for that query/execution, there is one created if not yet available. */ public QueryStatsManager getOrCreateStatsManager(UUID queryUuid, UUID executionUuid) { if (!queryStats.containsKey(executionUuid)) { synchronized (queryStats) { if (!queryStats.containsKey(executionUuid)) { String ourAddr; if (ourNodeAddressProvider != null) ourAddr = ourNodeAddressProvider.getOurNodeAddressAsString(); else ourAddr = ourHost + ":" + ourPort; queryStats.put(executionUuid, new QueryStatsManager(ourAddr)); } } } return queryStats.get(executionUuid); } /** * Add a listener which gets informed when query remotes inform about their query statistics on the given query UUID. */ public void addQueryStatsListener(UUID queryUuid, UUID executionUuid, QueryStatsListener listener) { if (!queryStatsListeners.containsKey(queryUuid)) { synchronized (queryStatsListeners) { if (!queryStatsListeners.containsKey(queryUuid)) queryStatsListeners.putIfAbsent(queryUuid, new ConcurrentHashMap<>()); } } queryStatsListeners.get(queryUuid).put(executionUuid, listener); } /** * As soon as a remote reported query statistics, call this method in order to inform everybody who is interested. */ public void remoteQueryStatsAvailable(UUID queryUuid, QueryStats stats) { Map<UUID, QueryStatsListener> listeners = queryStatsListeners.getOrDefault(queryUuid, new HashMap<>()); listeners.values().forEach(listener -> listener.queryStatistics(stats)); } /** * Handle and exception that occurred in a thread while executing a query. */ public static interface QueryExceptionHandler { /** * Handle the given exception. The query execution will automatically be unregistered in the {@link QueryRegistry}. */ public void handleException(Throwable t); } /** * Handles results that are received by the ClusterQueryService from query remotes. * * This needs to be implemented on the query master. */ public static interface QueryResultHandler { /** * A new {@link IntermediaryResult} is available from a remote. */ public void newIntermediaryAggregationResult(long groupId, String colName, IntermediaryResult oldIntermediaryResult, IntermediaryResult newIntermediaryResult); /** * New column values are available from a remote. */ public void newColumnValues(String colName, Map<Long, Object> values); /** * One remote reported that it is done processing the request. * * A single remote will only call this method after all other calls of that remote to other methods of this * interface have returned. */ public void oneRemoteDone(); /** * One remote reported that there was an exception processing the request. That one remote stopped processing * therefore. */ public void oneRemoteException(String msg); } /** * Listener that is called when new statistics have been reported by query remotes. */ public static interface QueryStatsListener { public void queryStatistics(QueryStats stats); } /** * Listener for data about how much of an executablePlan was calculated by remotes already. */ public static interface QueryPercentHandler { /** * A single remote reported a new percent-delta. */ public void newRemoteCompletionPercentDelta(short percentDeltaOfSingleRemote); } }