/* (c) 2017 Open Source Geospatial Foundation - all rights reserved * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.config; import java.io.Closeable; import java.io.IOException; import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; import org.geoserver.platform.GeoServerExtensions; import org.geoserver.platform.resource.Resource; import org.geoserver.util.Filter; import org.geotools.util.logging.Logging; /** * An iterator listing and mapping resources to a target object via a {@link ResourceMapper}. * The mapping is performed in a background thread pool, allowing to decouple CPU bound activities * from IO bound ones. * * @author Andrea Aime - GeoSolutions */ public class AsynchResourceIterator<T> implements Iterator<T>, Closeable { static final Logger LOGGER = Logging.getLogger(AsynchResourceIterator.class); static final int ASYNCH_RESOURCE_THREADS; static { String value = GeoServerExtensions.getProperty("org.geoserver.catalog.loadingThreads"); if (value != null) { ASYNCH_RESOURCE_THREADS = Integer.parseInt(value); } else { // empirical determination after benchmarks, the value is related not the // available CPUs, but by how well the disk handles parallel access, different // disk subsystems will have a different optimal value ASYNCH_RESOURCE_THREADS = 4; } } /** * Maps a resource into a target object * * @author Andrea Aime - GeoSolutions */ @FunctionalInterface public interface ResourceMapper<T> { T apply(Resource t) throws IOException; } /** * Indicates the end of a blocking queue contents */ static final Object TERMINATOR = new Object(); /** * The queue connecting the background threads loading the resources with the iterator */ BlockingQueue<Object> queue; /** * The thread coordinating the background resource load */ Thread thread; /** * The current mapped resource for the iterator */ T mapped; /** * A flag used to mark completion */ volatile boolean completed = false; /** * Builds an asynchronous {@link Resource} iterator * * @param root The directory resource * @param filter The filter getting specific child resources out of the root * @param mapper The mapper performing work on the resources found */ public AsynchResourceIterator(Resource root, Filter<Resource> filter, ResourceMapper<T> mapper) { // parallelize filtering (this is still synch'ed, cannot do anything in parallel with this) List<Resource> resources = root.list().parallelStream().filter(r -> filter.accept(r)) .collect(Collectors.toList()); // decide if we want to have a background thread for loading resources, or not if (resources.size() > 1) { queue = new LinkedBlockingQueue<>(10000); // create a background thread allowing this constructor to return immediately and // start accumulating in the queue asynchronously thread = new Thread(() -> { // parallelize IO in a local thread pool ExecutorService executor = Executors.newFixedThreadPool(ASYNCH_RESOURCE_THREADS); BlockingQueue<Object> sourceQueue = new LinkedBlockingQueue<>(resources); for (int i = 0; i < ASYNCH_RESOURCE_THREADS; i++) { // each IO thread will exit when close is called or when the terminator is // reached, we need one terminator per IO thread sourceQueue.add(TERMINATOR); executor.submit(() -> { try { Object o; while (!completed && (o = sourceQueue.take()) != TERMINATOR) { Resource r = (Resource) o; try { T mapped = mapper.apply(r); if (mapped != null) { queue.put(mapped); } } catch (IOException e) { LOGGER.log(Level.WARNING, "Failed to load resource '" + r.name() + "'", e); } } } catch (InterruptedException e) { return; } }); } // wait for everything to comlete and then add the terminator marker try { executor.shutdown(); executor.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS); // add the terminator queue.put(TERMINATOR); } catch (InterruptedException e) { LOGGER.log(Level.WARNING, "Failed to put the terminator in the queue", e); } }, "Loader" + root.name()); thread.start(); } else if (resources.size() == 1) { // don't start a thread for a single resource, there is no parallelism advantage final Resource r = resources.get(0); try { mapped = mapper.apply(r); completed = true; } catch (IOException e) { LOGGER.log(Level.WARNING, "Failed to load resource '" + r.name() + "'", e); } } else { // nothing found mapped = null; completed = true; } } @Override public boolean hasNext() { if (mapped != null) { return true; } // important that this is second to handle the "1 item" case correctly if (completed) { return false; } try { Object o = queue.take(); if (o == TERMINATOR) { completed = true; return false; } else { mapped = (T) o; return true; } } catch (InterruptedException e) { return false; } } @Override public T next() { if (hasNext()) { T result = mapped; mapped = null; return result; } else { throw new NoSuchElementException(); } } @Override public void close() { if (thread != null && thread.isAlive()) { thread.interrupt(); completed = true; queue.clear(); } } }