/* * 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.map.impl.query; import com.hazelcast.internal.cluster.ClusterService; import com.hazelcast.internal.serialization.InternalSerializationService; import com.hazelcast.logging.ILogger; import com.hazelcast.map.impl.LocalMapStatsProvider; import com.hazelcast.map.impl.MapContainer; import com.hazelcast.map.impl.MapServiceContext; import com.hazelcast.monitor.impl.LocalMapStatsImpl; import com.hazelcast.query.Predicate; import com.hazelcast.query.impl.QueryableEntry; import com.hazelcast.query.impl.predicates.QueryOptimizer; import com.hazelcast.spi.NodeEngine; import com.hazelcast.spi.OperationService; import java.util.Collection; import java.util.Collections; import java.util.concurrent.ExecutionException; /** * Runs query operations in the calling thread (thus blocking it) * <p> * Used by query operations only: QueryOperation & QueryPartitionOperation * Should not be used by proxies or any other query related objects. */ public class QueryRunner { protected final MapServiceContext mapServiceContext; protected final NodeEngine nodeEngine; protected final ILogger logger; protected final QueryResultSizeLimiter queryResultSizeLimiter; protected final InternalSerializationService serializationService; protected final QueryOptimizer queryOptimizer; protected final OperationService operationService; protected final ClusterService clusterService; protected final LocalMapStatsProvider localMapStatsProvider; protected final PartitionScanExecutor partitionScanExecutor; protected final ResultProcessorRegistry resultProcessorRegistry; public QueryRunner(MapServiceContext mapServiceContext, QueryOptimizer optimizer, PartitionScanExecutor partitionScanExecutor, ResultProcessorRegistry resultProcessorRegistry) { this.mapServiceContext = mapServiceContext; this.nodeEngine = mapServiceContext.getNodeEngine(); this.serializationService = (InternalSerializationService) nodeEngine.getSerializationService(); this.logger = nodeEngine.getLogger(getClass()); this.queryResultSizeLimiter = new QueryResultSizeLimiter(mapServiceContext, logger); this.queryOptimizer = optimizer; this.operationService = nodeEngine.getOperationService(); this.clusterService = nodeEngine.getClusterService(); this.localMapStatsProvider = mapServiceContext.getLocalMapStatsProvider(); this.partitionScanExecutor = partitionScanExecutor; this.resultProcessorRegistry = resultProcessorRegistry; } // full query = index query (if possible), then partition-scan query public Result runIndexOrPartitionScanQueryOnOwnedPartitions(Query query) throws ExecutionException, InterruptedException { int migrationStamp = getMigrationStamp(); Collection<Integer> initialPartitions = mapServiceContext.getOwnedPartitions(); MapContainer mapContainer = mapServiceContext.getMapContainer(query.getMapName()); // first we optimize the query Predicate predicate = queryOptimizer.optimize(query.getPredicate(), mapContainer.getIndexes()); // then we try to run using an index, but if that doesn't work, we'll try a full table scan // This would be the point where a query-plan should be added. It should determine f a full table scan // or an index should be used. Collection<QueryableEntry> entries = runUsingIndexSafely(predicate, mapContainer, migrationStamp); if (entries == null) { entries = runUsingPartitionScanSafely(query.getMapName(), predicate, initialPartitions, migrationStamp); } updateStatistics(mapContainer); if (entries != null) { // if results have been returned and partition state has not changed, set the partition IDs // so that caller is aware of partitions from which results were obtained. return populateTheResult(query, entries, initialPartitions); } else { // else: if fallback to full table scan also failed to return any results due to migrations, // then return empty result set without any partition IDs set (so that it is ignored by callers). return resultProcessorRegistry.get(query.getResultType()).populateResult(query, queryResultSizeLimiter.getNodeResultLimit(initialPartitions.size())); } } private Result populateTheResult(Query query, Collection<QueryableEntry> entries, Collection<Integer> initialPartitions) { ResultProcessor processor = resultProcessorRegistry.get(query.getResultType()); return processor.populateResult(query, queryResultSizeLimiter .getNodeResultLimit(initialPartitions.size()), entries, initialPartitions ); } private Collection<QueryableEntry> runUsingIndexSafely(Predicate predicate, MapContainer mapContainer, int migrationStamp) { // If a migration is in progress or migration ownership changes, // do not attempt to use an index as they may have not been created yet. if (!validateMigrationStamp(migrationStamp)) { return null; } Collection<QueryableEntry> entries = mapContainer.getIndexes().query(predicate); if (entries == null) { return null; } // If a migration is in progress or migration ownership changes, // do not attempt to use an index as they may have not been created yet. // This means migrations were executed and we may // return stale data, so we should rather return null and let the query run with a full table scan. // Also make sure there are no long migrations in flight which may have started after starting the query // but not completed yet. if (validateMigrationStamp(migrationStamp)) { return entries; } return null; } protected Collection<QueryableEntry> runUsingPartitionScanSafely(String name, Predicate predicate, Collection<Integer> partitions, int migrationStamp) throws InterruptedException, ExecutionException { Collection<QueryableEntry> entries; if (!validateMigrationStamp(migrationStamp)) { return null; } entries = partitionScanExecutor.execute(name, predicate, partitions); // If a migration is in progress or migration ownership changes, this means migrations were executed and we may // return stale data, so we should rather return null. // Also make sure there are no long migrations in flight which may have started after starting the query // but not completed yet. if (validateMigrationStamp(migrationStamp)) { return entries; } return null; } Result runPartitionScanQueryOnGivenOwnedPartition(Query query, int partitionId) throws ExecutionException, InterruptedException { MapContainer mapContainer = mapServiceContext.getMapContainer(query.getMapName()); Predicate predicate = queryOptimizer.optimize(query.getPredicate(), mapContainer.getIndexes()); Collection<QueryableEntry> entries = partitionScanExecutor.execute(query.getMapName(), predicate, Collections.singletonList(partitionId)); return populateTheResult(query, entries, Collections.singletonList(partitionId)); } private int getMigrationStamp() { return mapServiceContext.getService().getMigrationStamp(); } private boolean validateMigrationStamp(int migrationStamp) { return mapServiceContext.getService().validateMigrationStamp(migrationStamp); } private void updateStatistics(MapContainer mapContainer) { if (mapContainer.getMapConfig().isStatisticsEnabled()) { LocalMapStatsImpl localStats = localMapStatsProvider.getLocalMapStatsImpl(mapContainer.getName()); localStats.incrementOtherOperations(); } } }