package ru.yandex.market.graphouse.search.tree; import com.google.common.base.Preconditions; import com.google.common.util.concurrent.SettableFuture; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import ru.yandex.market.graphouse.search.MetricSearch; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; /** * @author Dmitry Andreev <a href="mailto:AndreevDm@yandex-team.ru"></a> * @date 31/01/2017 */ public class DirContentBatcher { private static final Logger log = LogManager.getLogger(); private final MetricSearch metricSearch; private final int maxBatchSize; private final int batchAggregationTimeMillis; private final AtomicReference<Batch> currentBatch = new AtomicReference<>(); private final Semaphore requestSemaphore; private final ScheduledExecutorService executorService; public DirContentBatcher(MetricSearch metricSearch, int maxParallelRequests, int maxBatchSize, int batchAggregationTimeMillis) { this.metricSearch = metricSearch; this.maxBatchSize = maxBatchSize; this.batchAggregationTimeMillis = batchAggregationTimeMillis; executorService = Executors.newScheduledThreadPool(maxParallelRequests); requestSemaphore = new Semaphore(maxParallelRequests, true); } public DirContent loadDirContent(MetricDir dir) throws Exception { if (requestSemaphore.tryAcquire()) { return directLoad(dir); } Batch dirBatch = currentBatch.updateAndGet(batch -> { if (batch == null || batch.size() >= maxBatchSize) { batch = createNewBatch(); } batch.addToBatch(dir); return batch; }); return dirBatch.getResult(dir); } private DirContent directLoad(MetricDir dir) throws Exception { try { return metricSearch.loadDirContent(dir); } finally { requestSemaphore.release(); } } private Batch createNewBatch() { Batch batch = new Batch(); executorService.schedule(batch, batchAggregationTimeMillis, TimeUnit.MILLISECONDS); return batch; } private class Batch implements Runnable { private Map<MetricDir, SettableFuture<DirContent>> requests = new ConcurrentHashMap<>(); @Override public void run() { requestSemaphore.acquireUninterruptibly(); try { currentBatch.getAndUpdate(batch -> (batch == this) ? null : batch); //Removing this batch from current Map<MetricDir, DirContent> dirsContent = metricSearch.loadDirsContent(requests.keySet()); for (Map.Entry<MetricDir, DirContent> dirDirContentEntry : dirsContent.entrySet()) { requests.remove(dirDirContentEntry.getKey()).set(dirDirContentEntry.getValue()); } if (!requests.isEmpty()) { log.error(requests.size() + " requests without data for dirs: " + requests.entrySet()); throw new IllegalStateException("No data for dirs"); } } catch (Exception e) { log.error("Failed to load content for dirs: " + requests.keySet(), e); for (SettableFuture<DirContent> settableFuture : requests.values()) { settableFuture.setException(e); } } finally { requestSemaphore.release(); } } private void addToBatch(MetricDir dir) { requests.computeIfAbsent(dir, metricDir -> SettableFuture.create()); } private DirContent getResult(MetricDir dir) throws Exception { Future<DirContent> future = requests.get(dir); Preconditions.checkNotNull(future); return future.get(); } private int size() { return requests.size(); } } }