/** * Copyright (c) Codice Foundation * <p> * This is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser * General Public License as published by the Free Software Foundation, either version 3 of the * License, or any later version. * <p> * 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 * Lesser General Public License for more details. A copy of the GNU Lesser General Public License * is distributed along with this program and can be found at * <http://www.gnu.org/licenses/lgpl.html>. */ package ddf.catalog.federation.impl; import java.io.Serializable; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ddf.catalog.data.Result; import ddf.catalog.federation.FederationStrategy; import ddf.catalog.operation.ProcessingDetails; import ddf.catalog.operation.Query; import ddf.catalog.operation.QueryRequest; import ddf.catalog.operation.QueryResponse; import ddf.catalog.operation.SourceResponse; import ddf.catalog.operation.impl.ProcessingDetailsImpl; import ddf.catalog.operation.impl.QueryImpl; import ddf.catalog.operation.impl.QueryRequestImpl; import ddf.catalog.operation.impl.QueryResponseImpl; import ddf.catalog.plugin.PluginExecutionException; import ddf.catalog.plugin.PostFederatedQueryPlugin; import ddf.catalog.plugin.PreFederatedQueryPlugin; import ddf.catalog.plugin.StopProcessingException; import ddf.catalog.source.Source; /** * The Class {@code FifoFederationStrategy} represents a First In First Out (FIFO) federation * strategy that returns results in the order they are received. This means that the first results * received by this strategy are the first results sent back to the client. * * @deprecated This federation strategy has been left for historical purposes. Refer to the * {@code CachingFederationStrategy} provided by the {@code catalog-core-standardframework} bundle. */ @Deprecated public class FifoFederationStrategy implements FederationStrategy { private static final Logger LOGGER = LoggerFactory.getLogger(FifoFederationStrategy.class); private static final int DEFAULT_MAX_START_INDEX = 50000; private int maxStartIndex; private ExecutorService queryExecutorService = null; private List<PreFederatedQueryPlugin> preQuery; private List<PostFederatedQueryPlugin> postQuery; /** * Instantiates a {@code FifoFederationStrategy} with the provided {@link ExecutorService}. * * @param queryExecutorService the {@link ExecutorService} for queries */ public FifoFederationStrategy(ExecutorService queryExecutorService, List<PreFederatedQueryPlugin> preQuery, List<PostFederatedQueryPlugin> postQuery) { this.queryExecutorService = queryExecutorService; this.preQuery = preQuery; this.postQuery = postQuery; this.maxStartIndex = DEFAULT_MAX_START_INDEX; } @Override public QueryResponse federate(List<Source> sources, final QueryRequest queryRequest) { Query originalQuery = queryRequest.getQuery(); int offset = originalQuery.getStartIndex(); // limit offset to max value if (offset > this.maxStartIndex) { offset = this.maxStartIndex; } final int pageSize = originalQuery.getPageSize(); QueryResponseImpl queryResponse = new QueryResponseImpl(queryRequest, null); Map<Source, Future<SourceResponse>> futures = new HashMap<Source, Future<SourceResponse>>(); Query modifiedQuery = getModifiedQuery(originalQuery, sources.size(), offset, pageSize); QueryRequest modifiedQueryRequest = new QueryRequestImpl(modifiedQuery, queryRequest.isEnterprise(), queryRequest.getSourceIds(), queryRequest.getProperties()); executeSourceQueries(sources, futures, modifiedQueryRequest); int resultsToSkip = 0; if (offset > 1 && sources.size() > 1) { resultsToSkip = offset - 1; } queryExecutorService.submit(new FifoQueryMonitor(queryExecutorService, futures, queryResponse, modifiedQueryRequest.getQuery(), resultsToSkip)); return executePostFederationPlugins(queryResponse); } protected QueryResponse executePostFederationPlugins(QueryResponse queryResponse) { try { for (PostFederatedQueryPlugin service : postQuery) { try { queryResponse = service.process(queryResponse); } catch (PluginExecutionException e) { LOGGER.info("Error executing PostFederatedQueryPlugin: ", e); } } } catch (StopProcessingException e) { LOGGER.info("Plugin stopped processing: ", e); } return queryResponse; } protected void executeSourceQueries(List<Source> sources, Map<Source, Future<SourceResponse>> futures, QueryRequest modifiedQueryRequest) { // Do NOT call source.isAvailable() when checking sources for (final Source source : sources) { if (source != null) { if (!futures.containsKey(source)) { try { for (PreFederatedQueryPlugin service : preQuery) { try { modifiedQueryRequest = service.process(source, modifiedQueryRequest); } catch (PluginExecutionException e) { LOGGER.info("Error executing PreFederatedQueryPlugin: ", e); } } } catch (StopProcessingException e) { LOGGER.info("Plugin stopped processing: ", e); } futures.put(source, queryExecutorService.submit(new CallableSourceResponse(source, modifiedQueryRequest.getQuery(), modifiedQueryRequest.getProperties()))); } else { LOGGER.info("Duplicate source found with name {}. Ignoring second one.", source.getId()); } } } } protected Query getModifiedQuery(Query originalQuery, int numberOfSources, int offset, int pageSize) { Query query = null; // If offset is not specified, our offset is 1 if (offset > 1 && numberOfSources > 1) { final int modifiedOffset = 1; int modifiedPageSize = offset + pageSize - 1; /** * Federated sources always query from offset of 1. When all query results are received * from all federated sources and merged together - then the offset is applied. */ query = new QueryImpl(originalQuery, modifiedOffset, modifiedPageSize, originalQuery.getSortBy(), originalQuery.requestsTotalResultsCount(), originalQuery.getTimeoutMillis()); } else { query = originalQuery; } return query; } private static class CallableSourceResponse implements Callable<SourceResponse> { private Query query = null; private Source source = null; private Map<String, Serializable> properties = null; public CallableSourceResponse(Source source, Query query, Map<String, Serializable> properties) { this.source = source; this.query = query; this.properties = properties; } @Override public SourceResponse call() throws Exception { long startTime = System.currentTimeMillis(); SourceResponse sourceResponse = source.query(new QueryRequestImpl(query, properties)); long elapsedTime = System.currentTimeMillis() - startTime; LOGGER.debug("The source {} responded to the query in {} milliseconds", source.getId(), elapsedTime); sourceResponse.getProperties() .put(QueryResponse.ELAPSED_TIME, elapsedTime); return sourceResponse; } } private static class FifoQueryMonitor implements Runnable { private QueryResponseImpl returnResults; private Map<Source, Future<SourceResponse>> futures; private Query query; private ExecutorService pool; private AtomicInteger sites = new AtomicInteger(); private AtomicInteger resultsToSkip = null; public FifoQueryMonitor(ExecutorService pool, Map<Source, Future<SourceResponse>> futuress, QueryResponseImpl returnResults, Query query, int resultsToSkip) { this.pool = pool; this.returnResults = returnResults; this.query = query; this.futures = futuress; this.resultsToSkip = new AtomicInteger(resultsToSkip); } private int updateSites(int addition) { return sites.addAndGet(addition); } @Override public void run() { int pageSize = query.getPageSize() > 0 ? query.getPageSize() : Integer.MAX_VALUE; for (final Map.Entry<Source, Future<SourceResponse>> entry : futures.entrySet()) { Source site = entry.getKey(); // Add a List of siteIds so endpoints know what sites got queried Serializable siteListObject = returnResults.getProperties() .get(QueryResponse.SITE_LIST); if (siteListObject != null && siteListObject instanceof List<?>) { ((List) siteListObject).add(site.getId()); } else { siteListObject = new ArrayList<String>(); ((List) siteListObject).add(site.getId()); returnResults.getProperties() .put(QueryResponse.SITE_LIST, (Serializable) siteListObject); } updateSites(1); pool.submit(new SourceQueryThread(site, entry.getValue(), returnResults, pageSize)); } } private long getTimeRemaining(long deadline) { long timeleft; if (System.currentTimeMillis() > deadline) { timeleft = 0; } else { timeleft = deadline - System.currentTimeMillis(); } return timeleft; } private class SourceQueryThread implements Runnable { Future<SourceResponse> curFuture = null; QueryResponseImpl returnResults = null; private long maxResults = 0; private Source site = null; private long deadline; public SourceQueryThread(Source site, Future<SourceResponse> curFuture, QueryResponseImpl returnResults, long maxResults) { this.curFuture = curFuture; this.returnResults = returnResults; this.site = site; this.maxResults = maxResults; deadline = System.currentTimeMillis() + query.getTimeoutMillis(); } @Override public void run() { SourceResponse sourceResponse = null; Set<ProcessingDetails> processingDetails = returnResults.getProcessingDetails(); try { sourceResponse = query.getTimeoutMillis() < 1 ? curFuture.get() : curFuture.get( getTimeRemaining(deadline), TimeUnit.MILLISECONDS); sourceResponse = curFuture.get(); } catch (Exception e) { LOGGER.info("Federated query returned exception {}", e.getMessage()); processingDetails.add(new ProcessingDetailsImpl(site.getId(), e)); } long sourceHits = 0; if (sourceResponse != null) { sourceHits = sourceResponse.getHits(); // Check if we have hit the maximum number // of results List<Result> results = sourceResponse.getResults(); int resultsReturned = results.size(); Map<String, Serializable> newSourceProperties = new HashMap<String, Serializable>(); newSourceProperties.put(QueryResponse.TOTAL_HITS, sourceHits); newSourceProperties.put(QueryResponse.TOTAL_RESULTS_RETURNED, resultsReturned); synchronized (returnResults) { long sentTotal = returnResults.getHits(); returnResults.setHits(sourceHits + sentTotal); for (Result result : results) { if (sentTotal >= maxResults) { LOGGER.debug("Received max number of results so ending polling"); break; } else if (resultsToSkip.get() == 0) { returnResults.addResult(result, false); sentTotal++; } else { resultsToSkip.decrementAndGet(); sentTotal++; } } if (sentTotal >= maxResults) { returnResults.closeResultQueue(); LOGGER.debug("sending terminator for fifo federation strategy."); } } returnResults.getProperties() .put(site.getId(), (Serializable) newSourceProperties); Map<String, Serializable> originalSourceProperties = sourceResponse.getProperties(); if (originalSourceProperties != null) { Serializable object = originalSourceProperties.get(QueryResponse.ELAPSED_TIME); if (object != null && object instanceof Long) { newSourceProperties.put(QueryResponse.ELAPSED_TIME, (Long) object); originalSourceProperties.remove(QueryResponse.ELAPSED_TIME); LOGGER.debug( "Setting the elapsedTime responseProperty to {} for source {}", object, site.getId()); } returnResults.getProperties() .putAll(originalSourceProperties); } } if (updateSites(-1) == 0) { LOGGER.debug("sending terminator for fifo federation strategy."); returnResults.closeResultQueue(); } } } } }