001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *      http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.oozie.util;
019
020import com.google.common.base.Preconditions;
021import com.google.common.collect.Sets;
022import org.apache.curator.framework.recipes.locks.Reaper;
023import org.apache.curator.utils.CloseableUtils;
024import org.apache.curator.framework.CuratorFramework;
025import org.apache.curator.utils.CloseableScheduledExecutorService;
026import org.apache.curator.utils.ThreadUtils;
027import org.apache.curator.utils.ZKPaths;
028import org.apache.zookeeper.data.Stat;
029import org.slf4j.Logger;
030import org.slf4j.LoggerFactory;
031import java.io.Closeable;
032import java.io.IOException;
033import java.util.Collection;
034import java.util.List;
035import java.util.Set;
036import java.util.concurrent.ConcurrentHashMap;
037import java.util.concurrent.Future;
038import java.util.concurrent.ScheduledExecutorService;
039import java.util.concurrent.TimeUnit;
040import java.util.concurrent.atomic.AtomicReference;
041import org.apache.curator.utils.PathUtils;
042
043// CLOUDERA-BUILD: CDH-25273
044// This is a copy of Curator 2.7.1's ChildReaper class, modified to work with
045// Guava 11.0.2.  The problem is the 'paths' Collection, which calls Guava's
046// Sets.newConcurrentHashSet(), which was added in Guava 15.0.
047/**
048 * Utility to reap empty child nodes of a parent node. Periodically calls getChildren on
049 * the node and adds empty nodes to an internally managed {@link Reaper}
050 */
051public class ChildReaper implements Closeable
052{
053  private final Logger log = LoggerFactory.getLogger(getClass());
054  private final Reaper reaper;
055  private final AtomicReference<State> state = new AtomicReference<State>(State.LATENT);
056  private final CuratorFramework client;
057  private final Collection<String> paths = newConcurrentHashSet();
058  private final Reaper.Mode mode;
059  private final CloseableScheduledExecutorService executor;
060  private final int reapingThresholdMs;
061
062  private volatile Future<?> task;
063
064  // This is copied from Curator's Reaper class
065  static final int DEFAULT_REAPING_THRESHOLD_MS = (int)TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES);
066
067  // This is copied from Guava
068  /**
069   * Creates a thread-safe set backed by a hash map. The set is backed by a
070   * {@link ConcurrentHashMap} instance, and thus carries the same concurrency
071   * guarantees.
072   *
073   * <p>Unlike {@code HashSet}, this class does NOT allow {@code null} to be
074   * used as an element. The set is serializable.
075   *
076   * @return a new, empty thread-safe {@code Set}
077   * @since 15.0
078   */
079  public static <E> Set<E> newConcurrentHashSet() {
080    return Sets.newSetFromMap(new ConcurrentHashMap<E, Boolean>());
081  }
082
083  private enum State
084  {
085    LATENT,
086    STARTED,
087    CLOSED
088  }
089
090  /**
091   * @param client the client
092   * @param path path to reap children from
093   * @param mode reaping mode
094   */
095  public ChildReaper(CuratorFramework client, String path, Reaper.Mode mode)
096  {
097    this(client, path, mode, newExecutorService(), DEFAULT_REAPING_THRESHOLD_MS, null);
098  }
099
100  /**
101   * @param client the client
102   * @param path path to reap children from
103   * @param reapingThresholdMs threshold in milliseconds that determines that a path can be deleted
104   * @param mode reaping mode
105   */
106  public ChildReaper(CuratorFramework client, String path, Reaper.Mode mode, int reapingThresholdMs)
107  {
108    this(client, path, mode, newExecutorService(), reapingThresholdMs, null);
109  }
110
111  /**
112   * @param client the client
113   * @param path path to reap children from
114   * @param executor executor to use for background tasks
115   * @param reapingThresholdMs threshold in milliseconds that determines that a path can be deleted
116   * @param mode reaping mode
117   */
118  public ChildReaper(CuratorFramework client, String path, Reaper.Mode mode, ScheduledExecutorService executor, int reapingThresholdMs)
119  {
120    this(client, path, mode, executor, reapingThresholdMs, null);
121  }
122
123  /**
124   * @param client the client
125   * @param path path to reap children from
126   * @param executor executor to use for background tasks
127   * @param reapingThresholdMs threshold in milliseconds that determines that a path can be deleted
128   * @param mode reaping mode
129   * @param leaderPath if not null, uses a leader selection so that only 1 reaper is active in the cluster
130   */
131  public ChildReaper(CuratorFramework client, String path, Reaper.Mode mode, ScheduledExecutorService executor, int reapingThresholdMs, String leaderPath)
132  {
133    this.client = client;
134    this.mode = mode;
135    this.executor = new CloseableScheduledExecutorService(executor);
136    this.reapingThresholdMs = reapingThresholdMs;
137    this.reaper = new Reaper(client, executor, reapingThresholdMs, leaderPath);
138    addPath(path);
139  }
140
141  /**
142   * The reaper must be started
143   *
144   * @throws Exception errors
145   */
146  public void start() throws Exception
147  {
148    Preconditions.checkState(state.compareAndSet(State.LATENT, State.STARTED), "Cannot be started more than once");
149
150    task = executor.scheduleWithFixedDelay
151        (
152            new Runnable()
153            {
154              @Override
155              public void run()
156              {
157                doWork();
158              }
159            },
160            reapingThresholdMs,
161            reapingThresholdMs,
162            TimeUnit.MILLISECONDS
163        );
164
165    reaper.start();
166  }
167
168  @Override
169  public void close() throws IOException
170  {
171    if ( state.compareAndSet(State.STARTED, State.CLOSED) )
172    {
173      CloseableUtils.closeQuietly(reaper);
174      task.cancel(true);
175    }
176  }
177
178  /**
179   * Add a path to reap children from
180   *
181   * @param path the path
182   * @return this for chaining
183   */
184  public ChildReaper addPath(String path)
185  {
186    paths.add(PathUtils.validatePath(path));
187    return this;
188  }
189
190  /**
191   * Remove a path from reaping
192   *
193   * @param path the path
194   * @return true if the path existed and was removed
195   */
196  public boolean removePath(String path)
197  {
198    return paths.remove(PathUtils.validatePath(path));
199  }
200
201  private static ScheduledExecutorService newExecutorService()
202  {
203    return ThreadUtils.newFixedThreadScheduledPool(2, "ChildReaper");
204  }
205
206  private void doWork()
207  {
208    for ( String path : paths )
209    {
210      try
211      {
212        List<String> children = client.getChildren().forPath(path);
213        for ( String name : children )
214        {
215          String thisPath = ZKPaths.makePath(path, name);
216          Stat stat = client.checkExists().forPath(thisPath);
217          if ( (stat != null) && (stat.getNumChildren() == 0) )
218          {
219            reaper.addPath(thisPath, mode);
220          }
221        }
222      }
223      catch ( Exception e )
224      {
225        log.error("Could not get children for path: " + path, e);
226      }
227    }
228  }
229}