/**
* Copyright 2016 Yahoo 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 com.yahoo.pulsar.zookeeper;
import static com.google.common.base.Preconditions.checkNotNull;
import java.io.Closeable;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.apache.bookkeeper.util.OrderedSafeExecutor;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.ZooKeeper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.yahoo.pulsar.zookeeper.ZooKeeperClientFactory.SessionType;
/**
* Per ZK client ZooKeeper cache supporting ZNode data and children list caches. A cache entry is identified, accessed
* and invalidated by the ZNode path. For the data cache, ZNode data parsing is done at request time with the given
* {@link Deserializer} argument.
*
* @param <T>
*/
public class GlobalZooKeeperCache extends ZooKeeperCache implements Closeable {
private static final Logger LOG = LoggerFactory.getLogger(GlobalZooKeeperCache.class);
private final ZooKeeperClientFactory zlClientFactory;
private final int zkSessionTimeoutMillis;
private final String globalZkConnect;
private final ScheduledExecutorService scheduledExecutor;
public GlobalZooKeeperCache(ZooKeeperClientFactory zkClientFactory, int zkSessionTimeoutMillis,
String globalZkConnect, OrderedSafeExecutor orderedExecutor, ScheduledExecutorService scheduledExecutor) {
super(null, orderedExecutor, scheduledExecutor);
this.zlClientFactory = zkClientFactory;
this.zkSessionTimeoutMillis = zkSessionTimeoutMillis;
this.globalZkConnect = globalZkConnect;
this.scheduledExecutor = scheduledExecutor;
}
public void start() throws IOException {
CompletableFuture<ZooKeeper> zkFuture = zlClientFactory.create(globalZkConnect, SessionType.AllowReadOnly,
zkSessionTimeoutMillis);
// Initial session creation with global ZK must work
try {
ZooKeeper newSession = zkFuture.get(10, TimeUnit.SECONDS);
// Register self as a watcher to receive notification when session expires and trigger a new session to be
// created
newSession.register(this);
zkSession.set(newSession);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
LOG.error("Failed to establish global zookeeper session: {}", e.getMessage(), e);
throw new IOException(e);
}
}
public void close() throws IOException {
ZooKeeper currentSession = zkSession.getAndSet(null);
if (currentSession != null) {
try {
currentSession.close();
} catch (InterruptedException e) {
throw new IOException(e);
}
}
}
@Override
public <T> void process(WatchedEvent event, final CacheUpdater<T> updater) {
synchronized (this) {
if (LOG.isDebugEnabled()) {
LOG.debug("[{}] Got Global ZooKeeper WatchdEvent: EventType: {}, KeeperState: {}, path: {}",
this.hashCode(), event.getType(), event.getState(), event.getPath());
}
if (event.getType() == Event.EventType.None) {
switch (event.getState()) {
case Expired:
// in case of expired, the zkSession is no longer good for sure.
// We need to restart the session immediately.
// cancel any timer event since it is already bad
ZooKeeper oldSession = this.zkSession.getAndSet(null);
LOG.warn("Global ZK session lost. Triggering reconnection {}", oldSession);
safeCloseZkSession(oldSession);
asyncRestartZooKeeperSession();
return;
case SyncConnected:
case ConnectedReadOnly:
checkNotNull(zkSession.get());
LOG.info("Global ZK session {} restored connection.", zkSession.get());
//
dataCache.synchronous().invalidateAll();
childrenCache.invalidateAll();
return;
default:
break;
}
}
}
// Other types of events
super.process(event, updater);
}
protected void asyncRestartZooKeeperSession() {
// schedule a re-connect event using this as the watch
LOG.info("Re-starting global ZK session in the background...");
CompletableFuture<ZooKeeper> zkFuture = zlClientFactory.create(globalZkConnect, SessionType.AllowReadOnly,
zkSessionTimeoutMillis);
zkFuture.thenAccept(zk -> {
if (zkSession.compareAndSet(null, zk)) {
LOG.info("Successfully re-created global ZK session: {}", zk);
} else {
// Other reconnection happened, we can close the session
safeCloseZkSession(zk);
}
}).exceptionally(ex -> {
LOG.warn("Failed to re-create global ZK session: {}", ex.getMessage());
// Schedule another attempt for later
scheduledExecutor.schedule(() -> {
asyncRestartZooKeeperSession();
}, 10, TimeUnit.SECONDS);
return null;
});
}
private void safeCloseZkSession(ZooKeeper zkSession) {
if (zkSession != null) {
if (LOG.isDebugEnabled()) {
LOG.debug("Closing global zkSession:{}", zkSession.getSessionId());
}
try {
zkSession.close();
} catch (Exception e) {
LOG.info("Closing Global ZK Session encountered an exception: {}. Disposed anyway.", e.getMessage());
}
}
}
}