All Downloads are FREE. Search and download functionalities are using the official Maven repository.

co.cask.common.security.zookeeper.ZKExtOperations Maven / Gradle / Ivy

/*
 * 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.common.security.zookeeper;

import co.cask.common.io.AsyncFunctions;
import co.cask.common.io.Codec;
import com.google.common.base.Function;
import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import org.apache.twill.common.Threads;
import org.apache.twill.zookeeper.NodeData;
import org.apache.twill.zookeeper.OperationFuture;
import org.apache.twill.zookeeper.ZKClient;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.data.ACL;
import org.apache.zookeeper.data.Stat;

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nullable;

/**
 * Collection of common zk operations.
 *
 * NOTE: When this class is matured, we could move this into twill ZKOperations.
 */
public final class ZKExtOperations {

  /**
   * Attempts to create a persistent node with the given content. If creation failed because the node already
   * exists ({@link KeeperException.NodeExistsException}), the node will be set with the given content.
   * This method is suitable for cases where the node expected to be non-existed.
   *
   * @param zkClient The ZKClient to perform the operations.
   * @param path The path in ZK.
   * @param data The content of the ZK node.
   * @param result The result that will be set into the result future when completed successfully.
   * @param maxFailure Maximum number of times to try to create/set the content.
   * @param  Type of the result.
   * @return A {@link ListenableFuture} that will be completed when node is created or data is set. The future will
   *         fail if failed to create and to set the data. Calling {@link ListenableFuture#cancel(boolean)} has
   *         no effect.
   */
  public static  ListenableFuture createOrSet(ZKClient zkClient, String path,
                                                    byte[] data, V result, int maxFailure) {
    return setContent(zkClient, path, data, result, maxFailure, true, null);
  }

  /**
   * Attempts to create a persistent node with the given content. If creation failed because the node already
   * exists ({@link KeeperException.NodeExistsException}), the node will be set with the given content.
   * This method is suitable for cases where the node expected to be non-existed.
   *
   * @param zkClient The ZKClient to perform the operations.
   * @param path The path in ZK.
   * @param data The content of the ZK node.
   * @param result The result that will be set into the result future when completed successfully.
   * @param maxFailure Maximum number of times to try to create/set the content.
   * @param createAcl The access control list to set on the node, if it is created.
   * @param  Type of the result.
   * @return A {@link ListenableFuture} that will be completed when node is created or data is set. The future will
   *         fail if failed to create and to set the data. Calling {@link ListenableFuture#cancel(boolean)} has
   *         no effect.
   */
  public static  ListenableFuture createOrSet(ZKClient zkClient, String path,
                                                    byte[] data, V result, int maxFailure, List createAcl) {
    return setContent(zkClient, path, data, result, maxFailure, true, createAcl);
  }

  /**
   * Attempts to set the content of the given node. If it failed due to node not exists
   * ({@link KeeperException.NoNodeException}), a persistent node will be created with the given content.
   * This method is suitable for cases where the node is expected to be existed.
   *
   * @param zkClient The ZKClient to perform the operations.
   * @param path The path in ZK.
   * @param data The content of the ZK node.
   * @param result The result that will be set into the result future when completed successfully.
   * @param maxFailure Maximum number of times to try to create/set the content.
   * @param  Type of the result.
   * @return A {@link ListenableFuture} that will be completed when node is created or data is set. The future will
   *         fail if failed to create and to set the data. Calling {@link ListenableFuture#cancel(boolean)} has
   *         no effect.
   */
  public static  ListenableFuture setOrCreate(ZKClient zkClient, String path,
                                                    byte[] data, V result, int maxFailure) {
    return setContent(zkClient, path, data, result, maxFailure, false, null);
  }

  /**
   * Update the content of the given node. If the node doesn't exists, it will try to create the node. Same as calling
   *
   * {@link #updateOrCreate(ZKClient, String, Function, Codec, List)
   * updateOrCreate(zkClient, path, modifier, codec, null)}
   *
   * @see #updateOrCreate(ZKClient, String, Function, Codec, List)
   */
  public static  ListenableFuture updateOrCreate(ZKClient zkClient, String path,
                                                       Function modifier, Codec codec) {
    return updateOrCreate(zkClient, path, modifier, codec, null);
  }

  /**
   * Update the content of the given node. If the node doesn't exists, it will try to create the node.
   * The modifier will be executed in the ZooKeeper callback thread, hence no blocking operation should be performed
   * in it. If blocking operation is needed, use the async version of this method.
   *
   * @see #updateOrCreate(ZKClient, String, AsyncFunction, Codec, List)
   */
  public static  ListenableFuture updateOrCreate(ZKClient zkClient, String path,
                                                       Function modifier, Codec codec,
                                                       @Nullable List createAcl) {
    SettableFuture resultFuture = SettableFuture.create();
    AsyncFunction asyncModifier = AsyncFunctions.asyncWrap(modifier);
    getAndSet(zkClient, path, asyncModifier, codec, resultFuture, createAcl);
    return resultFuture;
  }

  /**
   * Update the content of the given node. If the node doesn't exists, it will try to create the node. Same as calling
   *
   * {@link #updateOrCreate(ZKClient, String, AsyncFunction, Codec, List)
   * updateOrCreate(zkClient, path, modifier, codec, null)}
   *
   * @see #updateOrCreate(ZKClient, String, AsyncFunction, Codec, List)
   */
  public static  ListenableFuture updateOrCreate(ZKClient zkClient, String path,
                                                       AsyncFunction modifier, Codec codec) {
    return updateOrCreate(zkClient, path, modifier, codec, null);
  }

  /**
   * Update the content of the given node. If the node doesn't exists, it will try to create the node. If the node
   * exists, the existing content of the data will be provided to the modifier function to generate new content. A
   * conditional set will be performed which requires existing content the same as the one provided to the modifier
   * function. If the conditional set failed, the latest content will be fetched and fed to the modifier function
   * again.
   * This will continue until the set is successful or the modifier gave up the update, by returning {@code null}.
   *
   * @param zkClient The ZKClient to perform the operations.
   * @param path The path in ZK.
   * @param modifier A function to generate new content
   * @param codec Codec to encode/decode content to/from byte array
   * @param createAcl If not {@code null}, the access control list to set on the node, if it is created.
   * @param  Type of the content
   * @return A {@link ListenableFuture} that will be completed when node is created or data is set.
   *         The future will carry the actual content being set into the node. The future will
   *         fail if failed to create and to set the data. Calling {@link ListenableFuture#cancel(boolean)} has
   *         no effect.
   */
  public static  ListenableFuture updateOrCreate(ZKClient zkClient, String path,
                                                       AsyncFunction modifier, Codec codec,
                                                       @Nullable List createAcl) {
    SettableFuture resultFuture = SettableFuture.create();
    getAndSet(zkClient, path, modifier, codec, resultFuture, createAcl);
    return resultFuture;
  }

  /**
   * Attempts to set the content of the given node. If it failed due to node not exists
   * ({@link KeeperException.NoNodeException}), a persistent node will be created with the given content.
   * This method is suitable for cases where the node is expected to be existed.
   *
   * @param zkClient The ZKClient to perform the operations.
   * @param path The path in ZK.
   * @param data The content of the ZK node.
   * @param result The result that will be set into the result future when completed successfully.
   * @param maxFailure Maximum number of times to try to create/set the content.
   * @param createAcl The access control list to set on the node, if it is created.
   * @param  Type of the result.
   * @return A {@link ListenableFuture} that will be completed when node is created or data is set. The future will
   *         fail if failed to create and to set the data. Calling {@link ListenableFuture#cancel(boolean)} has
   *         no effect.
   */
  public static  ListenableFuture setOrCreate(ZKClient zkClient, String path,
                                                    byte[] data, V result, int maxFailure, List createAcl) {
    return setContent(zkClient, path, data, result, maxFailure, false, createAcl);
  }

  /**
   * Sets the content of a ZK node. Depends on the {@code createFirst} value,
   * either {@link ZKClient#create(String, byte[], org.apache.zookeeper.CreateMode)} or
   * {@link ZKClient#setData(String, byte[])} wil be called first.
   *
   * @param zkClient The ZKClient to perform the operations.
   * @param path The path in ZK.
   * @param data The content of the ZK node.
   * @param result The result that will be set into the result future when completed successfully.
   * @param maxFailure Maximum number of times to try to create/set the content.
   * @param createFirst If true, create is called first, otherwise setData is called first.
   * @param  Type of the result.
   * @return A {@link ListenableFuture} that will be completed when node is created or data is set. The future will
   *         fail if failed to create and to set the data. Calling {@link ListenableFuture#cancel(boolean)} has
   *         no effect.
   */
  private static  ListenableFuture setContent(final ZKClient zkClient, final String path,
                                                    final byte[] data, final V result,
                                                    final int maxFailure, boolean createFirst,
                                                    final List createAcls) {

    final SettableFuture resultFuture = SettableFuture.create();
    final AtomicInteger failureCount = new AtomicInteger(0);

    OperationFuture operationFuture;

    if (createFirst) {
      if (createAcls != null) {
        operationFuture = zkClient.create(path, data, CreateMode.PERSISTENT, createAcls);
      } else {
        operationFuture = zkClient.create(path, data, CreateMode.PERSISTENT);
      }
    } else {
      operationFuture = zkClient.setData(path, data);
    }

    Futures.addCallback(operationFuture, new FutureCallback() {
      @Override
      public void onSuccess(Object zkResult) {
        resultFuture.set(result);
      }

      @Override
      public void onFailure(Throwable t) {
        if (failureCount.getAndIncrement() > maxFailure) {
          resultFuture.setException(new Exception("Failed more than " + maxFailure + "times", t));
        } else if (t instanceof KeeperException.NoNodeException) {
          // If node not exists, create it with the data
          OperationFuture createFuture;
          if (createAcls != null) {
            createFuture = zkClient.create(path, data, CreateMode.PERSISTENT, createAcls);
          } else {
            createFuture = zkClient.create(path, data, CreateMode.PERSISTENT);
          }
          Futures.addCallback(createFuture, this, Threads.SAME_THREAD_EXECUTOR);
        } else if (t instanceof KeeperException.NodeExistsException) {
          // If the node exists when trying to create, set data.
          Futures.addCallback(zkClient.setData(path, data), this, Threads.SAME_THREAD_EXECUTOR);
        } else {
          resultFuture.setException(t);
        }
      }
    }, Threads.SAME_THREAD_EXECUTOR);

    return resultFuture;
  }

  /**
   * Performs the get and condition set part as described in
   * {@link #updateOrCreate(ZKClient, String, Function, Codec, List)}.
   */
  private static  void getAndSet(final ZKClient zkClient, final String path,
                                    final AsyncFunction modifier, final Codec codec,
                                    final SettableFuture resultFuture, final List createAcl) {

    // Try to fetch the node data
    Futures.addCallback(zkClient.getData(path), new FutureCallback() {
      @Override
      public void onSuccess(final NodeData result) {
        try {
          // Node has data. Call modifier to get newer version of content
          final int version = result.getStat().getVersion();

          Futures.addCallback(modifier.apply(codec.decode(result.getData())), new FutureCallback() {
            @Override
            public void onSuccess(final V content) {
              // When modifier calls completed, try to set the content

              // Modifier decided to abort
              if (content == null) {
                resultFuture.set(null);
                return;
              }
              try {
                byte[] data = codec.encode(content);

                // No change in content. No need to update and simply set the future to complete.
                if (Arrays.equals(data, result.getData())) {
                  resultFuture.set(content);
                  return;
                }

                Futures.addCallback(zkClient.setData(path, data, version), new FutureCallback() {
                  @Override
                  public void onSuccess(Stat result) {
                    resultFuture.set(content);
                  }

                  @Override
                  public void onFailure(Throwable t) {
                    if (t instanceof KeeperException.BadVersionException) {
                      // If the version is not good, get and set again
                      getAndSet(zkClient, path, modifier, codec, resultFuture, createAcl);
                    } else if (t instanceof KeeperException.NoNodeException) {
                      // If the node not exists, try to do create
                      createOrGetAndSet(zkClient, path, modifier, codec, resultFuture, createAcl);
                    } else {
                      resultFuture.setException(t);
                    }
                  }
                }, Threads.SAME_THREAD_EXECUTOR);
              } catch (Throwable t) {
                resultFuture.setException(t);
              }
            }

            @Override
            public void onFailure(Throwable t) {
              resultFuture.setException(t);
            }
          }, Threads.SAME_THREAD_EXECUTOR);

        } catch (Throwable t) {
          resultFuture.setException(t);
        }
      }

      @Override
      public void onFailure(Throwable t) {
        // If failed to get data because node not exists, try the create.
        if (t instanceof KeeperException.NoNodeException) {
          createOrGetAndSet(zkClient, path, modifier, codec, resultFuture, createAcl);
        } else {
          resultFuture.setException(t);
        }
      }
    }, Threads.SAME_THREAD_EXECUTOR);
  }


  /**
   * Performs the create part as described in
   * {@link #updateOrCreate(ZKClient, String, Function, Codec, List)}. If the creation failed with
   * {@link KeeperException.NodeExistsException}, the
   * {@link #getAndSet(ZKClient, String, AsyncFunction, Codec, SettableFuture, List)} will be called.
   */
  private static  void createOrGetAndSet(final ZKClient zkClient, final String path,
                                            final AsyncFunction modifier, final Codec codec,
                                            final SettableFuture resultFuture, final List createAcl) {
    try {
      Futures.addCallback(modifier.apply(null), new FutureCallback() {
        @Override
        public void onSuccess(final V content) {
          if (content == null) {
            resultFuture.set(null);
            return;
          }

          try {
            byte[] data = codec.encode(content);

            OperationFuture future;
            if (createAcl == null) {
              future = zkClient.create(path, data, CreateMode.PERSISTENT);
            } else {
              future = zkClient.create(path, data, CreateMode.PERSISTENT, createAcl);
            }

            Futures.addCallback(future, new FutureCallback() {
              @Override
              public void onSuccess(String result) {
                resultFuture.set(content);
              }

              @Override
              public void onFailure(Throwable t) {
                if (t instanceof KeeperException.NodeExistsException) {
                  // If failed to create due to node exists, try to do getAndSet.
                  getAndSet(zkClient, path, modifier, codec, resultFuture, createAcl);
                } else {
                  resultFuture.setException(t);
                }
              }
            }, Threads.SAME_THREAD_EXECUTOR);
          } catch (Throwable t) {
            resultFuture.setException(t);
          }

        }

        @Override
        public void onFailure(Throwable t) {
          resultFuture.setException(t);
        }
      }, Threads.SAME_THREAD_EXECUTOR);
    } catch (Throwable e) {
      resultFuture.setException(e);
    }
  }

  private ZKExtOperations() {
  }
}