/** * 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 org.codice.ddf.commands.catalog; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.RejectedExecutionHandler; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import org.apache.commons.lang.StringUtils; import org.apache.karaf.shell.api.action.Option; import org.codice.ddf.commands.catalog.facade.CatalogFacade; import org.opengis.filter.Filter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ddf.catalog.data.Metacard; import ddf.catalog.data.Result; import ddf.catalog.data.impl.MetacardImpl; import ddf.catalog.operation.CreateRequest; import ddf.catalog.operation.CreateResponse; import ddf.catalog.operation.SourceResponse; import ddf.catalog.operation.impl.CreateRequestImpl; import ddf.catalog.source.IngestException; import ddf.catalog.source.SourceUnavailableException; public abstract class DuplicateCommands extends CqlCommands { protected static final int MAX_BATCH_SIZE = 1000; private static final Logger LOGGER = LoggerFactory.getLogger(DuplicateCommands.class); protected AtomicInteger ingestedCount = new AtomicInteger(0); protected AtomicInteger failedCount = new AtomicInteger(0); protected Set<Metacard> failedMetacards = Collections.synchronizedSet(new HashSet<>()); protected long start; @Option(name = "--batchsize", required = false, aliases = { "-b"}, multiValued = false, description = "Number of Metacards to query and ingest at a time. Change this argument based on system memory and Catalog Provider limits.") int batchSize = MAX_BATCH_SIZE; @Option(name = "--multithreaded", required = false, aliases = { "-m"}, multiValued = false, description = "Number of threads to use when ingesting. Setting this value too high for your system can cause performance degradation.") int multithreaded = 1; @Option(name = "--failedDir", required = false, aliases = { "-f"}, multiValued = false, description = "Option to specify where to write metacards that failed to ingest.") String failedDir; @Option(name = "--maxMetacards", required = false, aliases = {"-mm", "-max"}, multiValued = false, description = "Option to specify a maximum amount of metacards to query.") int maxMetacards; abstract SourceResponse query(CatalogFacade framework, Filter filter, int startIndex, long querySize); /** * In batches, loops through a query of the queryFacade and an ingest to the ingestFacade of the * metacards from the response until there are no more metacards from the queryFacade or the * maxMetacards has been reached. * * @param queryFacade - the CatalogFacade to duplicate from * @param ingestFacade - the CatalogFacade to duplicate to * @param filter - the filter to query with */ protected void duplicateInBatches(CatalogFacade queryFacade, CatalogFacade ingestFacade, Filter filter) { AtomicInteger queryIndex = new AtomicInteger(1); final long originalQuerySize; if (maxMetacards > 0 && maxMetacards < batchSize) { originalQuerySize = maxMetacards; } else { originalQuerySize = batchSize; } final SourceResponse originalResponse = query(queryFacade, filter, queryIndex.get(), originalQuerySize); if (originalResponse == null) { return; } final long totalHits = originalResponse.getHits(); if (totalHits <= 0) { LOGGER.debug("Query returned 0 hits."); return; } // If the maxMetacards is set, restrict the totalWanted to the number of maxMetacards final long totalWanted; if (maxMetacards > 0 && maxMetacards <= totalHits) { totalWanted = maxMetacards; } else { totalWanted = totalHits; } ingestMetacards(ingestFacade, getMetacardsFromSourceResponse(originalResponse)); if (multithreaded > 1) { BlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<>(multithreaded); RejectedExecutionHandler rejectedExecutionHandler = new ThreadPoolExecutor.CallerRunsPolicy(); final ExecutorService executorService = new ThreadPoolExecutor(multithreaded, multithreaded, 0L, TimeUnit.MILLISECONDS, blockingQueue, rejectedExecutionHandler); console.printf("Running a maximum of %d threads during replication.%n", multithreaded); printProgressAndFlush(start, totalWanted, ingestedCount.get()); int index; while ((index = queryIndex.addAndGet(batchSize)) <= totalWanted) { final int i = index; executorService.submit(() -> { final SourceResponse response = query(queryFacade, filter, i, getQuerySizeFromIndex(totalWanted, i)); if (response != null) { ingestMetacards(ingestFacade, getMetacardsFromSourceResponse(response)); } printProgressAndFlush(start, totalWanted, ingestedCount.get()); }); } executorService.shutdown(); while (!executorService.isTerminated()) { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { // ignore } } } else { while (queryIndex.addAndGet(batchSize) <= totalWanted) { printProgressAndFlush(start, totalWanted, ingestedCount.get()); final SourceResponse response = query(queryFacade, filter, queryIndex.get(), getQuerySizeFromIndex(totalWanted, queryIndex.get())); if (response != null) { ingestMetacards(ingestFacade, getMetacardsFromSourceResponse(response)); } } } printProgressAndFlush(start, totalWanted, ingestedCount.get()); if (failedCount.get() > 0) { LOGGER.info("Not all records were ingested. [{}] failed", failedCount.get()); if (StringUtils.isNotBlank(failedDir)) { try { writeFailedMetacards(failedMetacards); } catch (IOException e) { console.println("Error occurred while writing failed metacards to failedDir."); } } } } /** * On the final iteration of the loop when the maxMetacards is less than the number of metacards * available in the ingestFacade, the query should only return the remaining wanted metacards, * not the full batch size. * * @param totalPossible - the total hits from the ingestFacade or the maxMetacards * @param index - the index to be queried next * @return how many metacards should be returned by a query starting at {@param index} */ private long getQuerySizeFromIndex(final long totalPossible, final long index) { return Math.min(totalPossible - (index - 1), batchSize); } protected List<Metacard> ingestMetacards(CatalogFacade provider, List<Metacard> metacards) { if (metacards.isEmpty()) { return Collections.emptyList(); } List<Metacard> createdMetacards = new ArrayList<>(); LOGGER.debug("Preparing to ingest {} records", metacards.size()); CreateRequest createRequest = new CreateRequestImpl(metacards); CreateResponse createResponse; try { createResponse = provider.create(createRequest); createdMetacards = createResponse.getCreatedMetacards(); } catch (IngestException e) { printErrorMessage(String.format("Received error while ingesting: %s%n", e.getMessage())); LOGGER.debug("Error during ingest. Attempting to ingest batch individually."); return ingestSingly(provider, metacards); } catch (SourceUnavailableException e) { printErrorMessage(String.format("Received error while ingesting: %s%n", e.getMessage())); LOGGER.debug("Error during ingest:", e); return createdMetacards; } catch (Exception e) { printErrorMessage(String.format("Unexpected Exception received while ingesting: %s%n", e.getMessage())); LOGGER.debug("Unexpected Exception during ingest:", e); return createdMetacards; } ingestedCount.addAndGet(createdMetacards.size()); failedCount.addAndGet(metacards.size() - createdMetacards.size()); failedMetacards.addAll(subtract(metacards, createdMetacards)); return createdMetacards; } private List<Metacard> ingestSingly(CatalogFacade provider, List<Metacard> metacards) { if (metacards.isEmpty()) { return Collections.emptyList(); } List<Metacard> createdMetacards = new ArrayList<>(); LOGGER.debug("Preparing to ingest {} records one at time.", metacards.size()); for (Metacard metacard : metacards) { CreateRequest createRequest = new CreateRequestImpl(Arrays.asList(metacard)); CreateResponse createResponse; try { createResponse = provider.create(createRequest); createdMetacards.addAll(createResponse.getCreatedMetacards()); } catch (IngestException | SourceUnavailableException e) { LOGGER.debug("Error during ingest:", e); } catch (Exception e) { LOGGER.debug("Unexpected Exception during ingest:", e); } } return createdMetacards; } protected String getInput(String message) throws IOException { StringBuilder buffer = new StringBuilder(); console.print(String.format(message)); console.flush(); while (true) { int byteOfData = session.getKeyboard() .read(); if (byteOfData < 0) { // end of stream return null; } console.print((char) byteOfData); if (byteOfData == '\r' || byteOfData == '\n') { break; } buffer.append((char) byteOfData); } return buffer.toString(); } protected List<Metacard> subtract(List<Metacard> queried, List<Metacard> ingested) { List<Metacard> result = new ArrayList<>(queried); result.removeAll(ingested); return result; } protected void writeFailedMetacards(Set<Metacard> failedMetacardsToWrite) throws IOException { File directory = new File(failedDir); if (!directory.exists()) { if (!directory.mkdirs()) { printErrorMessage( "Unable to create directory [" + directory.getAbsolutePath() + "]."); return; } } if (!directory.canWrite()) { printErrorMessage("Directory [" + directory.getAbsolutePath() + "] is not writable."); return; } for (Metacard metacard : failedMetacardsToWrite) { try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File( directory.getAbsolutePath(), metacard.getId())))) { oos.writeObject(new MetacardImpl(metacard)); oos.flush(); } } } private List<Metacard> getMetacardsFromSourceResponse(SourceResponse response) { return response.getResults() .stream() .map(Result::getMetacard) .collect(Collectors.toList()); } }