package com.hubspot.baragon.utils; import java.util.Collection; import java.util.HashSet; import java.util.Map; import java.util.Queue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import com.google.common.base.Function; import com.google.inject.Inject; import org.apache.curator.framework.CuratorFramework; import org.apache.curator.framework.api.BackgroundCallback; import org.apache.curator.framework.api.CuratorEvent; import org.apache.curator.utils.ZKPaths; import org.apache.log4j.Logger; import org.apache.zookeeper.KeeperException; public class ZkParallelFetcher { private static final Logger LOG = Logger.getLogger(ZkParallelFetcher.class); private static final int TIMEOUT_SECONDS = 10; private final CuratorFramework curatorFramework; @Inject public ZkParallelFetcher(CuratorFramework framework) { this.curatorFramework = framework; } public <T> Map<String, T> fetchDataInParallel(Collection<String> paths, Function<byte[], T> transformFunction) throws Exception { Map<String, T> dataMap = new ConcurrentHashMap<>(); CountDownLatch countDownLatch = new CountDownLatch(paths.size()); Queue<KeeperException> exceptions = new ConcurrentLinkedQueue<>(); BackgroundCallback callback = new GetDataCallback<>(dataMap, transformFunction, countDownLatch, exceptions); for (String path : paths) { curatorFramework.getData().inBackground(callback).forPath(path); } waitAndThrowExceptions(countDownLatch, exceptions); return dataMap; } public Map<String, Collection<String>> fetchChildrenInParallel(Collection<String> paths) throws Exception { // Didn't use Guava Multimap because we need thread-safety Map<String, Collection<String>> childMap = new ConcurrentHashMap<>(); CountDownLatch countDownLatch = new CountDownLatch(paths.size()); Queue<KeeperException> exceptions = new ConcurrentLinkedQueue<>(); BackgroundCallback callback = new GetChildrenCallback(childMap, countDownLatch, exceptions); for (String path : paths) { curatorFramework.getChildren().inBackground(callback).forPath(path); } waitAndThrowExceptions(countDownLatch, exceptions); return childMap; } private void waitAndThrowExceptions(CountDownLatch countDownLatch, Queue<KeeperException> exceptions) throws Exception { if (!countDownLatch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)) { throw new TimeoutException("ZkChildrenFetcher timed out waiting for data"); } for (KeeperException exception : exceptions) { LOG.error(exception); } if (!exceptions.isEmpty()) { throw exceptions.peek(); } } private static class GetDataCallback<T> implements BackgroundCallback { private final Map<String, T> dataMap; private final Function<byte[], T> transformFunction; private final CountDownLatch countDownLatch; private final Queue<KeeperException> exceptions; private GetDataCallback(Map<String, T> dataMap, Function<byte[], T> transformFunction, CountDownLatch countDownLatch, Queue<KeeperException> exceptions) { this.dataMap = dataMap; this.transformFunction = transformFunction; this.countDownLatch = countDownLatch; this.exceptions = exceptions; } @Override public void processResult(CuratorFramework client, CuratorEvent event) throws Exception { try { KeeperException.Code code = KeeperException.Code.get(event.getResultCode()); switch (code) { case OK: T data = event.getData() == null ? null : transformFunction.apply(event.getData()); dataMap.put(ZKPaths.getNodeFromPath(event.getPath()), data); break; case NONODE: // In this case there was a race condition in which the child node was deleted before we asked for data. break; default: exceptions.add(KeeperException.create(code, event.getPath())); } } finally { countDownLatch.countDown(); } } } private static class GetChildrenCallback implements BackgroundCallback { private final Map<String, Collection<String>> childMap; private final CountDownLatch countDownLatch; private final Queue<KeeperException> exceptions; private GetChildrenCallback(Map<String, Collection<String>> childMap, CountDownLatch countDownLatch, Queue<KeeperException> exceptions) { this.childMap = childMap; this.countDownLatch = countDownLatch; this.exceptions = exceptions; } @Override public void processResult(CuratorFramework client, CuratorEvent event) throws Exception { try { KeeperException.Code code = KeeperException.Code.get(event.getResultCode()); switch (code) { case OK: childMap.put(ZKPaths.getNodeFromPath(event.getPath()), new HashSet<>(event.getChildren())); break; case NONODE: // In this case there was a race condition in which the child node was deleted before we asked for data. break; default: exceptions.add(KeeperException.create(code, event.getPath())); } } finally { countDownLatch.countDown(); } } } }