/* * 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 org.apache.twill.internal.zookeeper; import com.google.common.base.Supplier; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import org.apache.twill.common.Threads; import org.apache.twill.zookeeper.ForwardingZKClient; import org.apache.twill.zookeeper.NodeChildren; import org.apache.twill.zookeeper.NodeData; import org.apache.twill.zookeeper.OperationFuture; import org.apache.twill.zookeeper.RetryStrategy; import org.apache.twill.zookeeper.RetryStrategy.OperationType; import org.apache.twill.zookeeper.ZKClient; import org.apache.zookeeper.CreateMode; import org.apache.zookeeper.Watcher; import org.apache.zookeeper.data.Stat; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; /** * A {@link ZKClient} that will invoke {@link RetryStrategy} on operation failure. * This {@link ZKClient} works by delegating calls to another {@link ZKClient} * and listen for the result. If the result is a failure, and is * {@link RetryUtils#canRetry(org.apache.zookeeper.KeeperException.Code) retryable}, the given {@link RetryStrategy} * will be called to determine the next retry time, or give up, depending on the value returned by the strategy. */ public final class FailureRetryZKClient extends ForwardingZKClient { private static final ScheduledExecutorService SCHEDULER = Executors.newSingleThreadScheduledExecutor( Threads.createDaemonThreadFactory("retry-zkclient")); private final RetryStrategy retryStrategy; public FailureRetryZKClient(ZKClient delegate, RetryStrategy retryStrategy) { super(delegate); this.retryStrategy = retryStrategy; } @Override public OperationFuture<String> create(String path, byte[] data, CreateMode createMode) { return create(path, data, createMode, true); } @Override public OperationFuture<String> create(final String path, final byte[] data, final CreateMode createMode, final boolean createParent) { // No retry for any SEQUENTIAL node, as some algorithms depends on only one sequential node being created. if (createMode == CreateMode.PERSISTENT_SEQUENTIAL || createMode == CreateMode.EPHEMERAL_SEQUENTIAL) { return super.create(path, data, createMode, createParent); } final SettableOperationFuture<String> result = SettableOperationFuture.create(path, Threads.SAME_THREAD_EXECUTOR); Futures.addCallback(super.create(path, data, createMode, createParent), new OperationFutureCallback<String>(OperationType.CREATE, System.currentTimeMillis(), path, result, new Supplier<OperationFuture<String>>() { @Override public OperationFuture<String> get() { return FailureRetryZKClient.super.create(path, data, createMode, createParent); } })); return result; } @Override public OperationFuture<Stat> exists(String path) { return exists(path, null); } @Override public OperationFuture<Stat> exists(final String path, final Watcher watcher) { final SettableOperationFuture<Stat> result = SettableOperationFuture.create(path, Threads.SAME_THREAD_EXECUTOR); Futures.addCallback(super.exists(path, watcher), new OperationFutureCallback<Stat>(OperationType.EXISTS, System.currentTimeMillis(), path, result, new Supplier<OperationFuture<Stat>>() { @Override public OperationFuture<Stat> get() { return FailureRetryZKClient.super.exists(path, watcher); } })); return result; } @Override public OperationFuture<NodeChildren> getChildren(String path) { return getChildren(path, null); } @Override public OperationFuture<NodeChildren> getChildren(final String path, final Watcher watcher) { final SettableOperationFuture<NodeChildren> result = SettableOperationFuture.create(path, Threads.SAME_THREAD_EXECUTOR); Futures.addCallback(super.getChildren(path, watcher), new OperationFutureCallback<NodeChildren>(OperationType.GET_CHILDREN, System.currentTimeMillis(), path, result, new Supplier<OperationFuture<NodeChildren>>() { @Override public OperationFuture<NodeChildren> get() { return FailureRetryZKClient.super.getChildren(path, watcher); } })); return result; } @Override public OperationFuture<NodeData> getData(String path) { return getData(path, null); } @Override public OperationFuture<NodeData> getData(final String path, final Watcher watcher) { final SettableOperationFuture<NodeData> result = SettableOperationFuture.create(path, Threads.SAME_THREAD_EXECUTOR); Futures.addCallback(super.getData(path, watcher), new OperationFutureCallback<NodeData>(OperationType.GET_DATA, System.currentTimeMillis(), path, result, new Supplier<OperationFuture<NodeData>>() { @Override public OperationFuture<NodeData> get() { return FailureRetryZKClient.super.getData(path, watcher); } })); return result; } @Override public OperationFuture<Stat> setData(String path, byte[] data) { return setData(path, data, -1); } @Override public OperationFuture<Stat> setData(final String dataPath, final byte[] data, final int version) { final SettableOperationFuture<Stat> result = SettableOperationFuture.create(dataPath, Threads.SAME_THREAD_EXECUTOR); Futures.addCallback(super.setData(dataPath, data, version), new OperationFutureCallback<Stat>(OperationType.SET_DATA, System.currentTimeMillis(), dataPath, result, new Supplier<OperationFuture<Stat>>() { @Override public OperationFuture<Stat> get() { return FailureRetryZKClient.super.setData(dataPath, data, version); } })); return result; } @Override public OperationFuture<String> delete(String path) { return delete(path, -1); } @Override public OperationFuture<String> delete(final String deletePath, final int version) { final SettableOperationFuture<String> result = SettableOperationFuture.create(deletePath, Threads.SAME_THREAD_EXECUTOR); Futures.addCallback(super.delete(deletePath, version), new OperationFutureCallback<String>(OperationType.DELETE, System.currentTimeMillis(), deletePath, result, new Supplier<OperationFuture<String>> () { @Override public OperationFuture<String> get() { return FailureRetryZKClient.super.delete(deletePath, version); } })); return result; } /** * Callback to watch for operation result and trigger retry if necessary. * @param <V> Type of operation result. */ private final class OperationFutureCallback<V> implements FutureCallback<V> { private final OperationType type; private final long startTime; private final String path; private final SettableOperationFuture<V> result; private final Supplier<OperationFuture<V>> retryAction; private final AtomicInteger failureCount; private OperationFutureCallback(OperationType type, long startTime, String path, SettableOperationFuture<V> result, Supplier<OperationFuture<V>> retryAction) { this.type = type; this.startTime = startTime; this.path = path; this.result = result; this.retryAction = retryAction; this.failureCount = new AtomicInteger(0); } @Override public void onSuccess(V result) { this.result.set(result); } @Override public void onFailure(Throwable t) { if (!doRetry(t)) { result.setException(t); } } private boolean doRetry(Throwable t) { if (!RetryUtils.canRetry(t)) { return false; } // Determine the relay delay long nextRetry = retryStrategy.nextRetry(failureCount.incrementAndGet(), startTime, type, path); if (nextRetry < 0) { return false; } // Schedule the retry. SCHEDULER.schedule(new Runnable() { @Override public void run() { Futures.addCallback(retryAction.get(), OperationFutureCallback.this); } }, nextRetry, TimeUnit.MILLISECONDS); return true; } } }