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}