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.hadoop.hbase.zookeeper; 019 020import static org.apache.hadoop.hbase.HConstants.DEFAULT_ZK_SESSION_TIMEOUT; 021import static org.apache.hadoop.hbase.HConstants.ZK_SESSION_TIMEOUT; 022 023import java.io.Closeable; 024import java.io.IOException; 025import java.util.Arrays; 026import java.util.EnumSet; 027import java.util.concurrent.CompletableFuture; 028import java.util.concurrent.DelayQueue; 029import java.util.concurrent.Delayed; 030import java.util.concurrent.TimeUnit; 031import java.util.concurrent.atomic.AtomicBoolean; 032import org.apache.hadoop.conf.Configuration; 033import org.apache.hadoop.hbase.util.Threads; 034import org.apache.yetus.audience.InterfaceAudience; 035import org.apache.zookeeper.KeeperException; 036import org.apache.zookeeper.KeeperException.Code; 037import org.apache.zookeeper.ZooKeeper; 038import org.apache.zookeeper.data.Stat; 039import org.slf4j.Logger; 040import org.slf4j.LoggerFactory; 041 042import org.apache.hbase.thirdparty.com.google.common.annotations.VisibleForTesting; 043 044/** 045 * A very simple read only zookeeper implementation without watcher support. 046 */ 047@InterfaceAudience.Private 048public final class ReadOnlyZKClient implements Closeable { 049 050 private static final Logger LOG = LoggerFactory.getLogger(ReadOnlyZKClient.class); 051 052 public static final String RECOVERY_RETRY = "zookeeper.recovery.retry"; 053 054 private static final int DEFAULT_RECOVERY_RETRY = 30; 055 056 public static final String RECOVERY_RETRY_INTERVAL_MILLIS = 057 "zookeeper.recovery.retry.intervalmill"; 058 059 private static final int DEFAULT_RECOVERY_RETRY_INTERVAL_MILLIS = 1000; 060 061 public static final String KEEPALIVE_MILLIS = "zookeeper.keep-alive.time"; 062 063 private static final int DEFAULT_KEEPALIVE_MILLIS = 60000; 064 065 private static final EnumSet<Code> FAIL_FAST_CODES = EnumSet.of(Code.NOAUTH, Code.AUTHFAILED); 066 067 private final String connectString; 068 069 private final int sessionTimeoutMs; 070 071 private final int maxRetries; 072 073 private final int retryIntervalMs; 074 075 private final int keepAliveTimeMs; 076 077 private static abstract class Task implements Delayed { 078 079 protected long time = System.nanoTime(); 080 081 public boolean needZk() { 082 return false; 083 } 084 085 public void exec(ZooKeeper zk) { 086 } 087 088 public void connectFailed(IOException e) { 089 } 090 091 public void closed(IOException e) { 092 } 093 094 @Override 095 public int compareTo(Delayed o) { 096 Task that = (Task) o; 097 int c = Long.compare(time, that.time); 098 if (c != 0) { 099 return c; 100 } 101 return Integer.compare(System.identityHashCode(this), System.identityHashCode(that)); 102 } 103 104 @Override 105 public long getDelay(TimeUnit unit) { 106 return unit.convert(time - System.nanoTime(), TimeUnit.NANOSECONDS); 107 } 108 } 109 110 private static final Task CLOSE = new Task() { 111 }; 112 113 private final DelayQueue<Task> tasks = new DelayQueue<>(); 114 115 private final AtomicBoolean closed = new AtomicBoolean(false); 116 117 @VisibleForTesting 118 ZooKeeper zookeeper; 119 120 private int pendingRequests = 0; 121 122 private String getId() { 123 return String.format("0x%08x", System.identityHashCode(this)); 124 } 125 126 public ReadOnlyZKClient(Configuration conf) { 127 this.connectString = ZKConfig.getZKQuorumServersString(conf); 128 this.sessionTimeoutMs = conf.getInt(ZK_SESSION_TIMEOUT, DEFAULT_ZK_SESSION_TIMEOUT); 129 this.maxRetries = conf.getInt(RECOVERY_RETRY, DEFAULT_RECOVERY_RETRY); 130 this.retryIntervalMs = 131 conf.getInt(RECOVERY_RETRY_INTERVAL_MILLIS, DEFAULT_RECOVERY_RETRY_INTERVAL_MILLIS); 132 this.keepAliveTimeMs = conf.getInt(KEEPALIVE_MILLIS, DEFAULT_KEEPALIVE_MILLIS); 133 LOG.info( 134 "Connect {} to {} with session timeout={}ms, retries {}, " + 135 "retry interval {}ms, keepAlive={}ms", 136 getId(), connectString, sessionTimeoutMs, maxRetries, retryIntervalMs, keepAliveTimeMs); 137 Threads.setDaemonThreadRunning(new Thread(this::run), 138 "ReadOnlyZKClient-" + connectString + "@" + getId()); 139 } 140 141 private abstract class ZKTask<T> extends Task { 142 143 protected final String path; 144 145 private final CompletableFuture<T> future; 146 147 private final String operationType; 148 149 private int retries; 150 151 protected ZKTask(String path, CompletableFuture<T> future, String operationType) { 152 this.path = path; 153 this.future = future; 154 this.operationType = operationType; 155 } 156 157 protected final void onComplete(ZooKeeper zk, int rc, T ret, boolean errorIfNoNode) { 158 tasks.add(new Task() { 159 160 @Override 161 public void exec(ZooKeeper alwaysNull) { 162 pendingRequests--; 163 Code code = Code.get(rc); 164 if (code == Code.OK) { 165 future.complete(ret); 166 } else if (code == Code.NONODE) { 167 if (errorIfNoNode) { 168 future.completeExceptionally(KeeperException.create(code, path)); 169 } else { 170 future.complete(ret); 171 } 172 } else if (FAIL_FAST_CODES.contains(code)) { 173 future.completeExceptionally(KeeperException.create(code, path)); 174 } else { 175 if (code == Code.SESSIONEXPIRED) { 176 LOG.warn("{} to {} session expired, close and reconnect", getId(), connectString); 177 try { 178 zk.close(); 179 } catch (InterruptedException e) { 180 } 181 } 182 if (ZKTask.this.delay(retryIntervalMs, maxRetries)) { 183 LOG.warn("{} to {} failed for {} of {}, code = {}, retries = {}", getId(), 184 connectString, operationType, path, code, ZKTask.this.retries); 185 tasks.add(ZKTask.this); 186 } else { 187 LOG.warn("{} to {} failed for {} of {}, code = {}, retries = {}, give up", getId(), 188 connectString, operationType, path, code, ZKTask.this.retries); 189 future.completeExceptionally(KeeperException.create(code, path)); 190 } 191 } 192 } 193 194 @Override 195 public void closed(IOException e) { 196 // It may happen that a request is succeeded and the onComplete has been called and pushed 197 // us into the task queue, but before we get called a close is called and here we will 198 // fail the request, although it is succeeded actually. 199 // This is not a perfect solution but anyway, it is better than hang the requests for 200 // ever, and also acceptable as if you close the zk client before actually getting the 201 // response then a failure is always possible. 202 future.completeExceptionally(e); 203 } 204 }); 205 } 206 207 @Override 208 public boolean needZk() { 209 return true; 210 } 211 212 protected abstract void doExec(ZooKeeper zk); 213 214 @Override 215 public final void exec(ZooKeeper zk) { 216 pendingRequests++; 217 doExec(zk); 218 } 219 220 public boolean delay(long intervalMs, int maxRetries) { 221 if (retries >= maxRetries) { 222 return false; 223 } 224 retries++; 225 time = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(intervalMs); 226 return true; 227 } 228 229 @Override 230 public void connectFailed(IOException e) { 231 if (delay(retryIntervalMs, maxRetries)) { 232 LOG.warn("{} to {} failed to connect to zk fo {} of {}, retries = {}", getId(), 233 connectString, operationType, path, retries, e); 234 tasks.add(this); 235 } else { 236 LOG.warn("{} to {} failed to connect to zk fo {} of {}, retries = {}, give up", getId(), 237 connectString, operationType, path, retries, e); 238 future.completeExceptionally(e); 239 } 240 } 241 242 @Override 243 public void closed(IOException e) { 244 future.completeExceptionally(e); 245 } 246 } 247 248 private static <T> CompletableFuture<T> failed(Throwable e) { 249 CompletableFuture<T> future = new CompletableFuture<>(); 250 future.completeExceptionally(e); 251 return future; 252 } 253 254 public CompletableFuture<byte[]> get(String path) { 255 if (closed.get()) { 256 return failed(new IOException("Client already closed")); 257 } 258 CompletableFuture<byte[]> future = new CompletableFuture<>(); 259 tasks.add(new ZKTask<byte[]>(path, future, "get") { 260 261 @Override 262 protected void doExec(ZooKeeper zk) { 263 zk.getData(path, false, 264 (rc, path, ctx, data, stat) -> onComplete(zk, rc, data, true), null); 265 } 266 }); 267 return future; 268 } 269 270 public CompletableFuture<Stat> exists(String path) { 271 if (closed.get()) { 272 return failed(new IOException("Client already closed")); 273 } 274 CompletableFuture<Stat> future = new CompletableFuture<>(); 275 tasks.add(new ZKTask<Stat>(path, future, "exists") { 276 277 @Override 278 protected void doExec(ZooKeeper zk) { 279 zk.exists(path, false, (rc, path, ctx, stat) -> onComplete(zk, rc, stat, false), null); 280 } 281 }); 282 return future; 283 } 284 285 private void closeZk() { 286 if (zookeeper != null) { 287 try { 288 zookeeper.close(); 289 } catch (InterruptedException e) { 290 } 291 zookeeper = null; 292 } 293 } 294 295 private ZooKeeper getZk() throws IOException { 296 // may be closed when session expired 297 if (zookeeper == null || !zookeeper.getState().isAlive()) { 298 zookeeper = new ZooKeeper(connectString, sessionTimeoutMs, e -> {}); 299 } 300 return zookeeper; 301 } 302 303 private void run() { 304 for (;;) { 305 Task task; 306 try { 307 task = tasks.poll(keepAliveTimeMs, TimeUnit.MILLISECONDS); 308 } catch (InterruptedException e) { 309 continue; 310 } 311 if (task == CLOSE) { 312 break; 313 } 314 if (task == null) { 315 if (pendingRequests == 0) { 316 LOG.trace("{} to {} inactive for {}ms; closing (Will reconnect when new requests)", 317 getId(), connectString, keepAliveTimeMs); 318 closeZk(); 319 } 320 continue; 321 } 322 if (!task.needZk()) { 323 task.exec(null); 324 } else { 325 ZooKeeper zk; 326 try { 327 zk = getZk(); 328 } catch (IOException e) { 329 task.connectFailed(e); 330 continue; 331 } 332 task.exec(zk); 333 } 334 } 335 closeZk(); 336 IOException error = new IOException("Client already closed"); 337 Arrays.stream(tasks.toArray(new Task[0])).forEach(t -> t.closed(error)); 338 tasks.clear(); 339 } 340 341 @Override 342 public void close() { 343 if (closed.compareAndSet(false, true)) { 344 LOG.info("Close zookeeper connection {} to {}", getId(), connectString); 345 tasks.add(CLOSE); 346 } 347 } 348 349 @VisibleForTesting 350 public String getConnectString() { 351 return connectString; 352 } 353}