/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.distributedlog.util; import com.twitter.distributedlog.exceptions.BKTransmitException; import com.twitter.distributedlog.DistributedLogConstants; import com.twitter.distributedlog.exceptions.LockingException; import com.twitter.distributedlog.ZooKeeperClient; import com.twitter.distributedlog.exceptions.DLInterruptedException; import com.twitter.distributedlog.exceptions.UnexpectedException; import com.twitter.distributedlog.exceptions.ZKException; import com.twitter.util.Await; import com.twitter.util.Duration; import com.twitter.util.Function; import com.twitter.util.Future; import com.twitter.util.FutureCancelledException; import com.twitter.util.FutureEventListener; import com.twitter.util.Promise; import com.twitter.util.Return; import com.twitter.util.Throw; import org.apache.bookkeeper.client.BKException; import org.apache.zookeeper.KeeperException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import scala.runtime.BoxedUnit; import javax.annotation.Nullable; import java.io.IOException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; /** * Utilities to process future */ public class FutureUtils { private static final Logger logger = LoggerFactory.getLogger(FutureUtils.class); public static class OrderedFutureEventListener<R> implements FutureEventListener<R> { public static <R> OrderedFutureEventListener<R> of( FutureEventListener<R> listener, OrderedScheduler scheduler, Object key) { return new OrderedFutureEventListener<R>(scheduler, key, listener); } private final OrderedScheduler scheduler; private final Object key; private final FutureEventListener<R> listener; private OrderedFutureEventListener(OrderedScheduler scheduler, Object key, FutureEventListener<R> listener) { this.scheduler = scheduler; this.key = key; this.listener = listener; } @Override public void onSuccess(final R value) { scheduler.submit(key, new Runnable() { @Override public void run() { listener.onSuccess(value); } }); } @Override public void onFailure(final Throwable cause) { scheduler.submit(key, new Runnable() { @Override public void run() { listener.onFailure(cause); } }); } } public static class FutureEventListenerRunnable<R> implements FutureEventListener<R> { public static <R> FutureEventListenerRunnable<R> of( FutureEventListener<R> listener, ExecutorService executorService) { return new FutureEventListenerRunnable<R>(executorService, listener); } private final ExecutorService executorService; private final FutureEventListener<R> listener; private FutureEventListenerRunnable(ExecutorService executorService, FutureEventListener<R> listener) { this.executorService = executorService; this.listener = listener; } @Override public void onSuccess(final R value) { executorService.submit(new Runnable() { @Override public void run() { listener.onSuccess(value); } }); } @Override public void onFailure(final Throwable cause) { executorService.submit(new Runnable() { @Override public void run() { listener.onFailure(cause); } }); } } private static class ListFutureProcessor<T, R> extends Function<Throwable, BoxedUnit> implements FutureEventListener<R>, Runnable { private volatile boolean interrupted = false; private final Iterator<T> itemsIter; private final Function<T, Future<R>> processFunc; private final Promise<List<R>> promise; private final List<R> results; private final ExecutorService callbackExecutor; ListFutureProcessor(List<T> items, Function<T, Future<R>> processFunc, ExecutorService callbackExecutor) { this.itemsIter = items.iterator(); this.processFunc = processFunc; this.promise = new Promise<List<R>>(); this.promise.setInterruptHandler(this); this.results = new ArrayList<R>(); this.callbackExecutor = callbackExecutor; } @Override public BoxedUnit apply(Throwable cause) { interrupted = true; return BoxedUnit.UNIT; } @Override public void onSuccess(R value) { results.add(value); if (null == callbackExecutor) { run(); } else { callbackExecutor.submit(this); } } @Override public void onFailure(final Throwable cause) { if (null == callbackExecutor) { promise.setException(cause); } else { callbackExecutor.submit(new Runnable() { @Override public void run() { promise.setException(cause); } }); } } @Override public void run() { if (interrupted) { logger.debug("ListFutureProcessor is interrupted."); return; } if (!itemsIter.hasNext()) { promise.setValue(results); return; } processFunc.apply(itemsIter.next()).addEventListener(this); } } /** * Process the list of items one by one using the process function <i>processFunc</i>. * The process will be stopped immediately if it fails on processing any one. * * @param collection list of items * @param processFunc process function * @param callbackExecutor executor to process the item * @return future presents the list of processed results */ public static <T, R> Future<List<R>> processList(List<T> collection, Function<T, Future<R>> processFunc, @Nullable ExecutorService callbackExecutor) { ListFutureProcessor<T, R> processor = new ListFutureProcessor<T, R>(collection, processFunc, callbackExecutor); if (null != callbackExecutor) { callbackExecutor.submit(processor); } else { processor.run(); } return processor.promise; } /** * Await for the result of the future and thrown bk related exceptions. * * @param result future to wait for * @return the result of future * @throws BKException when exceptions are thrown by the future. If there is unkown exceptions * thrown from the future, the exceptions will be wrapped into * {@link org.apache.bookkeeper.client.BKException.BKUnexpectedConditionException}. */ public static <T> T bkResult(Future<T> result) throws BKException { try { return Await.result(result); } catch (BKException bke) { throw bke; } catch (InterruptedException ie) { throw BKException.create(BKException.Code.InterruptedException); } catch (Exception e) { logger.warn("Encountered unexpected exception on waiting bookkeeper results : ", e); throw BKException.create(BKException.Code.UnexpectedConditionException); } } /** * Return the bk exception return code for a <i>throwable</i>. * * @param throwable the cause of the exception * @return the bk exception return code. if the exception isn't bk exceptions, * it would return bk exception code. */ public static int bkResultCode(Throwable throwable) { if (throwable instanceof BKException) { return ((BKException)throwable).getCode(); } return BKException.Code.UnexpectedConditionException; } /** * Wait for the result until it completes. * * @param result result to wait * @return the result * @throws IOException when encountered exceptions on the result */ public static <T> T result(Future<T> result) throws IOException { return result(result, Duration.Top()); } /** * Wait for the result for a given <i>duration</i>. * <p>If the result is not ready within `duration`, an IOException will thrown wrapping with * corresponding {@link com.twitter.util.TimeoutException}. * * @param result result to wait * @param duration duration to wait * @return the result * @throws IOException when encountered exceptions on the result or waiting for the result. */ public static <T> T result(Future<T> result, Duration duration) throws IOException { try { return Await.result(result, duration); } catch (KeeperException ke) { throw new ZKException("Encountered zookeeper exception on waiting result", ke); } catch (BKException bke) { throw new BKTransmitException("Encountered bookkeeper exception on waiting result", bke.getCode()); } catch (IOException ioe) { throw ioe; } catch (InterruptedException ie) { throw new DLInterruptedException("Interrupted on waiting result", ie); } catch (Exception e) { throw new IOException("Encountered exception on waiting result", e); } } /** * Wait for the result of a lock operation. * * @param result result to wait * @param lockPath path of the lock * @return the result * @throws LockingException when encountered exceptions on the result of lock operation */ public static <T> T lockResult(Future<T> result, String lockPath) throws LockingException { try { return Await.result(result); } catch (LockingException le) { throw le; } catch (Exception e) { throw new LockingException(lockPath, "Encountered exception on locking ", e); } } /** * Convert the <i>throwable</i> to zookeeper related exceptions. * * @param throwable cause * @param path zookeeper path * @return zookeeper related exceptions */ public static Throwable zkException(Throwable throwable, String path) { if (throwable instanceof KeeperException) { return throwable; } else if (throwable instanceof ZooKeeperClient.ZooKeeperConnectionException) { return KeeperException.create(KeeperException.Code.CONNECTIONLOSS, path); } else if (throwable instanceof InterruptedException) { return new DLInterruptedException("Interrupted on operating " + path, throwable); } else { return new UnexpectedException("Encountered unexpected exception on operatiing " + path, throwable); } } /** * Cancel the future. It would interrupt the future. * * @param future future to cancel */ public static <T> void cancel(Future<T> future) { future.raise(new FutureCancelledException()); } /** * Raise an exception to the <i>promise</i> within a given <i>timeout</i> period. * If the promise has been satisfied before raising, it won't change the state of the promise. * * @param promise promise to raise exception * @param timeout timeout period * @param unit timeout period unit * @param cause cause to raise * @param scheduler scheduler to execute raising exception * @param key the submit key used by the scheduler * @return the promise applied with the raise logic */ public static <T> Promise<T> within(final Promise<T> promise, final long timeout, final TimeUnit unit, final Throwable cause, final OrderedScheduler scheduler, final Object key) { if (timeout < DistributedLogConstants.FUTURE_TIMEOUT_IMMEDIATE || promise.isDefined()) { return promise; } scheduler.schedule(key, new Runnable() { @Override public void run() { logger.info("Raise exception", cause); // satisfy the promise FutureUtils.setException(promise, cause); } }, timeout, unit); return promise; } /** * Satisfy the <i>promise</i> with provide value in an ordered scheduler. * <p>If the promise was already satisfied, nothing will be changed. * * @param promise promise to satisfy * @param value value to satisfy * @param scheduler scheduler to satisfy the promise with provided value * @param key the submit key of the ordered scheduler */ public static <T> void setValue(final Promise<T> promise, final T value, OrderedScheduler scheduler, Object key) { scheduler.submit(key, new Runnable() { @Override public void run() { setValue(promise, value); } }); } /** * Satisfy the <i>promise</i> with provide value. * <p>If the promise was already satisfied, nothing will be changed. * * @param promise promise to satisfy * @param value value to satisfy * @return true if successfully satisfy the future. false if the promise has been satisfied. */ public static <T> boolean setValue(Promise<T> promise, T value) { boolean success = promise.updateIfEmpty(new Return<T>(value)); if (!success) { logger.info("Result set multiple times. Value = '{}', New = 'Return({})'", promise.poll(), value); } return success; } /** * Satisfy the <i>promise</i> with provided <i>cause</i> in an ordered scheduler. * * @param promise promise to satisfy * @param throwable cause to satisfy * @param scheduler the scheduler to satisfy the promise * @param key submit key of the ordered scheduler */ public static <T> void setException(final Promise<T> promise, final Throwable throwable, OrderedScheduler scheduler, Object key) { scheduler.submit(key, new Runnable() { @Override public void run() { setException(promise, throwable); } }); } /** * Satisfy the <i>promise</i> with provided <i>cause</i>. * * @param promise promise to satisfy * @param cause cause to satisfy * @return true if successfully satisfy the future. false if the promise has been satisfied. */ public static <T> boolean setException(Promise<T> promise, Throwable cause) { boolean success = promise.updateIfEmpty(new Throw<T>(cause)); if (!success) { logger.info("Result set multiple times. Value = '{}', New = 'Throw({})'", promise.poll(), cause); } return success; } /** * Ignore exception from the <i>future</i>. * * @param future the original future * @return a transformed future ignores exceptions */ public static <T> Promise<Void> ignore(Future<T> future) { return ignore(future, null); } /** * Ignore exception from the <i>future</i> and log <i>errorMsg</i> on exceptions * * @param future the original future * @param errorMsg the error message to log on exceptions * @return a transformed future ignores exceptions */ public static <T> Promise<Void> ignore(Future<T> future, final String errorMsg) { final Promise<Void> promise = new Promise<Void>(); future.addEventListener(new FutureEventListener<T>() { @Override public void onSuccess(T value) { setValue(promise, null); } @Override public void onFailure(Throwable cause) { if (null != errorMsg) { logger.error(errorMsg, cause); } setValue(promise, null); } }); return promise; } /** * Create transmit exception from transmit result. * * @param transmitResult * transmit result (basically bk exception code) * @return transmit exception */ public static BKTransmitException transmitException(int transmitResult) { return new BKTransmitException("Failed to write to bookkeeper; Error is (" + transmitResult + ") " + BKException.getMessage(transmitResult), transmitResult); } }