co.cask.cdap.common.zookeeper.coordination.ResourceCoordinatorClient.java Source code

Java tutorial

Introduction

Here is the source code for co.cask.cdap.common.zookeeper.coordination.ResourceCoordinatorClient.java

Source

/*
 * 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);
        }
    }
}