/* * Copyright © 2014 Cask Data, Inc. * * Licensed 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 co.cask.cdap.common.zookeeper.coordination; import co.cask.cdap.api.common.Bytes; import co.cask.cdap.common.zookeeper.ZKExtOperations; import com.google.common.base.Function; import com.google.common.base.Functions; import com.google.common.base.Objects; import com.google.common.base.Throwables; import com.google.common.collect.LinkedHashMultimap; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; import com.google.common.util.concurrent.AbstractService; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import org.apache.twill.common.Cancellable; import org.apache.twill.common.Threads; import org.apache.twill.zookeeper.NodeData; import org.apache.twill.zookeeper.ZKClient; import org.apache.twill.zookeeper.ZKOperations; import org.apache.zookeeper.KeeperException; import org.apache.zookeeper.WatchedEvent; import org.apache.zookeeper.Watcher; import org.apache.zookeeper.data.Stat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.EnumSet; import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import javax.annotation.Nullable; /** * This class helps client to participate in resource coordination process. */ public final class ResourceCoordinatorClient extends AbstractService { private static final Logger LOG = LoggerFactory.getLogger(ResourceCoordinatorClient.class); private static final Function<NodeData, ResourceRequirement> NODE_DATA_TO_REQUIREMENT = new Function<NodeData, ResourceRequirement>() { @Override public ResourceRequirement apply(@Nullable NodeData input) { if (input == null) { return null; } try { return CoordinationConstants.RESOURCE_REQUIREMENT_CODEC.decode(input.getData()); } catch (Throwable t) { LOG.error("Failed to decode resource requirement: {}", Bytes.toStringBinary(input.getData()), t); throw Throwables.propagate(t); } } }; private final ZKClient zkClient; private final Multimap<String, AssignmentChangeListener> changeListeners; private final Set<String> serviceWatched; private final Map<String, ResourceAssignment> assignments; private ExecutorService handlerExecutor; public ResourceCoordinatorClient(ZKClient zkClient) { this.zkClient = zkClient; this.changeListeners = LinkedHashMultimap.create(); this.serviceWatched = Sets.newHashSet(); this.assignments = Maps.newHashMap(); } /** * Submits the given {@link ResourceRequirement} for allocation. * * @param requirement The requirement to be submitted. * @return A {@link ListenableFuture} that will be completed when submission is completed and it'll carry the * submitted requirement as result. The future will fail if failed to submit the requirement. Calling * {@link ListenableFuture#cancel(boolean)} has no effect. */ public ListenableFuture<ResourceRequirement> submitRequirement(ResourceRequirement requirement) { try { String zkPath = CoordinationConstants.REQUIREMENTS_PATH + "/" + requirement.getName(); byte[] data = CoordinationConstants.RESOURCE_REQUIREMENT_CODEC.encode(requirement); return ZKExtOperations.createOrSet(zkClient, zkPath, data, requirement, CoordinationConstants.MAX_ZK_FAILURE_RETRY); } catch (Exception e) { return Futures.immediateFailedFuture(e); } } /** * Modify an existing {@link ResourceRequirement}. * * @param name Resource name * @param modifier A function to modify an existing requirement. The function might get called multiple times * if there are concurrent modifications from multiple clients. * @return A {@link ListenableFuture} that will be completed when submission is completed and it'll carry the * modified requirement as result or {@code null} if the modifier decided not to modify the requirement. * The future will fail if failed to submit the requirement. * Calling {@link ListenableFuture#cancel(boolean)} has no effect. */ public ListenableFuture<ResourceRequirement> modifyRequirement(String name, final ResourceModifier modifier) { String zkPath = CoordinationConstants.REQUIREMENTS_PATH + "/" + name; return ZKExtOperations.updateOrCreate(zkClient, zkPath, modifier, CoordinationConstants.RESOURCE_REQUIREMENT_CODEC); } /** * Fetches the {@link ResourceRequirement} for the given resource. * * @param resourceName Name of the resource. * @return A {@link ListenableFuture} that will be completed when the requirement is fetch. A {@code null} result * will be set into the future if no such requirement exists. The future will fail if failed to fetch * the requirement due to error other than requirement not exists. * Calling {@link ListenableFuture#cancel(boolean)} has no effect. */ public ListenableFuture<ResourceRequirement> fetchRequirement(String resourceName) { String zkPath = CoordinationConstants.REQUIREMENTS_PATH + "/" + resourceName; return Futures.transform( ZKOperations.ignoreError(zkClient.getData(zkPath), KeeperException.NoNodeException.class, null), NODE_DATA_TO_REQUIREMENT ); } /** * Deletes the {@link ResourceRequirement} for the given resource. * * @param resourceName Name of the resource. * @return A {@link ListenableFuture} that will be completed when the requirement is successfully removed. * If the requirement doesn't exists, the deletion would still be treated as successful. */ public ListenableFuture<String> deleteRequirement(String resourceName) { String zkPath = CoordinationConstants.REQUIREMENTS_PATH + "/" + resourceName; return Futures.transform( ZKOperations.ignoreError(zkClient.delete(zkPath), KeeperException.NoNodeException.class, resourceName), Functions.constant(resourceName) ); } /** * Subscribes for changes in resource assignment. Upon subscription started, * the {@link AssignmentChangeListener#onChange(ResourceAssignment)} method will be invoked to receive the * current assignment if it exists. * * @param serviceName Name of the service to watch for changes. * @param listener The listener to invoke when there are changes. * @return A {@link Cancellable} for cancelling the subscription. */ public synchronized Cancellable subscribe(String serviceName, AssignmentChangeListener listener) { AssignmentChangeListenerCaller caller = new AssignmentChangeListenerCaller(serviceName, listener); if (serviceWatched.add(serviceName)) { // Not yet watching ZK, add the handler and start watching ZK for changes in assignment. changeListeners.put(serviceName, caller); watchAssignment(serviceName); } else { // Invoke the listener with the cached assignment if there is any before adding to the resource handler list ResourceAssignment assignment = assignments.get(serviceName); if (assignment != null && !assignment.getAssignments().isEmpty()) { caller.onChange(assignment); } changeListeners.put(serviceName, caller); } return new AssignmentListenerCancellable(caller); } @Override protected void doStart() { handlerExecutor = Executors.newSingleThreadExecutor( Threads.createDaemonThreadFactory("resource-coordinator-client")); notifyStarted(); } @Override protected void doStop() { try { finishHandlers(null); notifyStopped(); } finally { handlerExecutor.shutdown(); } } private void doNotifyFailed(Throwable cause) { try { finishHandlers(cause); } finally { handlerExecutor.shutdown(); notifyFailed(cause); } } /** * Calls the {@link ResourceHandler#finished(Throwable)} method on all existing handlers. * * @param failureCause Failure reason for finish or {@code null} if finish is not due to failure. */ private synchronized void finishHandlers(Throwable failureCause) { for (AssignmentChangeListener listener : changeListeners.values()) { listener.finished(failureCause); } } /** * Starts watching ZK for ResourceAssignment changes for the given service. */ private void watchAssignment(final String serviceName) { final String zkPath = CoordinationConstants.ASSIGNMENTS_PATH + "/" + serviceName; // Watch for both getData() and exists() call Watcher watcher = wrapWatcher(new AssignmentWatcher(serviceName, EnumSet.of(Watcher.Event.EventType.NodeDataChanged, Watcher.Event.EventType.NodeDeleted))); Futures.addCallback(zkClient.getData(zkPath, watcher), wrapCallback(new FutureCallback<NodeData>() { @Override public void onSuccess(NodeData result) { try { ResourceAssignment assignment = CoordinationConstants.RESOURCE_ASSIGNMENT_CODEC.decode(result.getData()); LOG.debug("Received resource assignment for {}. {}", serviceName, assignment.getAssignments()); handleAssignmentChange(serviceName, assignment); } catch (Exception e) { LOG.error("Failed to decode ResourceAssignment {}", Bytes.toStringBinary(result.getData()), e); } } @Override public void onFailure(Throwable t) { if (t instanceof KeeperException.NoNodeException) { // Treat it as assignment has been removed. If the node doesn't exists for the first time fetch data, // there will be no oldAssignment, hence the following call would be a no-op. handleAssignmentChange(serviceName, new ResourceAssignment(serviceName)); // Watch for exists if it still interested synchronized (ResourceCoordinatorClient.this) { if (changeListeners.containsKey(serviceName)) { watchAssignmentOnExists(serviceName); } } } else { LOG.error("Failed to getData on ZK {}{}", zkClient.getConnectString(), zkPath, t); doNotifyFailed(t); } } }), Threads.SAME_THREAD_EXECUTOR); } /** * Starts watch for assignment changes when the node exists. * * @param serviceName Name of the service. */ private void watchAssignmentOnExists(final String serviceName) { final String zkPath = CoordinationConstants.ASSIGNMENTS_PATH + "/" + serviceName; Watcher watcher = wrapWatcher(new AssignmentWatcher(serviceName, EnumSet.of(Watcher.Event.EventType.NodeCreated))); Futures.addCallback(zkClient.exists(zkPath, watcher), wrapCallback(new FutureCallback<Stat>() { @Override public void onSuccess(Stat result) { if (result != null) { watchAssignment(serviceName); } } @Override public void onFailure(Throwable t) { LOG.error("Failed to call exists on ZK {}{}", zkClient.getConnectString(), zkPath, t); doNotifyFailed(t); } }), Threads.SAME_THREAD_EXECUTOR); } /** * Handles changes in assignment. * * @param newAssignment The updated assignment. */ private synchronized void handleAssignmentChange(String serviceName, ResourceAssignment newAssignment) { ResourceAssignment oldAssignment = assignments.get(serviceName); // Nothing changed. if (Objects.equal(oldAssignment, newAssignment)) { return; } // If the new assignment is empty, simply remove it from cache, otherwise remember it. if (newAssignment.getAssignments().isEmpty()) { assignments.remove(serviceName); } else { assignments.put(serviceName, newAssignment); } // If the change is from null to empty, no need to notify listeners. if (oldAssignment == null && newAssignment.getAssignments().isEmpty()) { return; } // Otherwise, notify all listeners for (AssignmentChangeListener listener : changeListeners.get(serviceName)) { listener.onChange(newAssignment); } } /** * Wraps a ZK watcher so that it only get triggered if this service is running. * * @param watcher The Watcher to wrap. * @return A wrapped Watcher. */ private Watcher wrapWatcher(final Watcher watcher) { return new Watcher() { @Override public void process(WatchedEvent event) { if (isRunning()) { watcher.process(event); } } }; } /** * Wraps a FutureCallback so that it only get triggered if this service is running. * * @param callback The callback to wrap. * @param <V> Type of the callback result. * @return A wrapped FutureCallback. */ private <V> FutureCallback<V> wrapCallback(final FutureCallback<V> callback) { return new FutureCallback<V>() { @Override public void onSuccess(V result) { if (isRunning()) { callback.onSuccess(result); } } @Override public void onFailure(Throwable t) { if (isRunning()) { callback.onFailure(t); } } }; } /** * Wraps a {@link AssignmentChangeListener} so that it's always invoked from the handler executor. It also make sure * upon {@link AssignmentChangeListener#finished(Throwable)} is called, it get removed from the listener list. */ private final class AssignmentChangeListenerCaller implements AssignmentChangeListener { private final String service; private final AssignmentChangeListener delegate; private AssignmentChangeListenerCaller(String service, AssignmentChangeListener delegate) { this.service = service; this.delegate = delegate; } @Override public void onChange(final ResourceAssignment assignment) { handlerExecutor.execute(new Runnable() { @Override public void run() { delegate.onChange(assignment); } }); } @Override public void finished(final Throwable failureCause) { // Remove itself from the handlers and only invoke finish call if successfully removing itself. synchronized (ResourceCoordinatorClient.this) { if (!changeListeners.remove(service, this)) { return; } } handlerExecutor.execute(new Runnable() { @Override public void run() { delegate.finished(failureCause); } }); } } /** * ZK Watcher to set on the resource assignment node. It's used for both getData and exists call. */ private final class AssignmentWatcher implements Watcher { private final String serviceName; private final EnumSet<Event.EventType> actOnTypes; AssignmentWatcher(String serviceName, EnumSet<Event.EventType> actOnTypes) { this.serviceName = serviceName; this.actOnTypes = actOnTypes; } @Override public void process(WatchedEvent event) { if (actOnTypes.contains(event.getType())) { // If no handler is interested in the event, simply ignore the event and not setting the watch again. synchronized (ResourceCoordinatorClient.this) { if (!changeListeners.containsKey(serviceName)) { serviceWatched.remove(serviceName); return; } } // If some handler exists, call watchAssignment again to fetch the data and set the Watch. watchAssignment(serviceName); } } } /** * Cancellable that delegates to the {@link AssignmentChangeListenerCaller#finished(Throwable)} method. */ private static final class AssignmentListenerCancellable implements Cancellable { private final AssignmentChangeListenerCaller caller; private AssignmentListenerCancellable(AssignmentChangeListenerCaller caller) { this.caller = caller; } @Override public void cancel() { caller.finished(null); } } }