com.hubspot.ringleader.watcher.PersistentWatcher Maven / Gradle / Ivy
package com.hubspot.ringleader.watcher;
import com.google.common.base.Function;
import com.google.common.base.Supplier;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.api.CuratorWatcher;
import org.apache.curator.framework.api.UnhandledErrorListener;
import org.apache.curator.framework.imps.CuratorFrameworkState;
import org.apache.curator.framework.listen.Listenable;
import org.apache.curator.framework.listen.ListenerContainer;
import org.apache.curator.framework.state.ConnectionState;
import org.apache.curator.framework.state.ConnectionStateListener;
import org.apache.zookeeper.KeeperException.NoNodeException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Closeable;
import java.io.IOException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
public class PersistentWatcher implements Closeable {
private static final Logger LOG = LoggerFactory.getLogger(PersistentWatcher.class);
private final Supplier curatorSupplier;
private final AtomicReference curatorReference;
private final AtomicLong curatorTimestamp;
private final AtomicInteger lastVersion;
private final AtomicBoolean started;
private final String path;
private final ScheduledExecutorService executor;
private final CuratorWatcher watcher;
private final ListenerContainer listeners;
PersistentWatcher(Supplier curatorSupplier, final String path) {
this.curatorSupplier = curatorSupplier;
this.curatorReference = new AtomicReference();
this.curatorTimestamp = new AtomicLong(0);
this.lastVersion = new AtomicInteger(-1);
this.started = new AtomicBoolean();
this.path = path;
this.executor = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
//@Override Java 5 compatibility
public Thread newThread(Runnable r) {
Thread thread = Executors.defaultThreadFactory().newThread(r);
thread.setName("PersistentWatcher-" + path);
thread.setDaemon(true);
return thread;
}
});
// keep a reference to the watcher so we don't add duplicates (Curator uses a set)
this.watcher = new CuratorWatcher() {
//@Override Java 5 compatibility
public void process(WatchedEvent event) throws Exception {
System.out.println("RECEIVED EVENT: " + event.getType());
switch (event.getType()) {
case NodeDeleted:
lastVersion.set(-1);
notifyListeners(Event.nodeDeleted());
default:
fetch(false);
}
}
};
this.listeners = new ListenerContainer();
}
public void start() {
if (started.compareAndSet(false, true)) {
curatorReference.set(newCurator());
executor.submit(new Runnable() {
//@Override Java 5 compatibility
public void run() {
try {
fetch(true);
} finally {
executor.schedule(this, 10, TimeUnit.MINUTES);
}
}
});
}
}
public Listenable getEventListenable() {
return listeners;
}
//@Override Java 5 compatibility
public void close() throws IOException {
if (started.compareAndSet(true, false)) {
cleanup(curatorReference.getAndSet(null));
listeners.clear();
lastVersion.set(-1);
curatorTimestamp.set(0);
executor.shutdown();
}
}
private synchronized void fetch(final boolean backgroundFetch) {
try {
CuratorFramework curator = curatorReference.get();
if (curator == null) {
LOG.error("No curator present, replacing client");
replaceCurator();
return;
}
Stat stat = new Stat();
byte[] data = curator.getData()
.storingStatIn(stat)
.usingWatcher(watcher)
.forPath(path);
int version = stat.getVersion();
int previousVersion = lastVersion.getAndSet(version);
if (version != previousVersion) {
notifyListeners(Event.nodeUpdated(stat, data));
if (previousVersion != -1 && backgroundFetch) {
LOG.error("Watcher stopped firing, replacing client");
replaceCurator();
}
}
} catch (NoNodeException e) {
LOG.debug("No node exists for path {}", path);
if (lastVersion.getAndSet(-1) != -1) {
notifyListeners(Event.nodeDeleted());
}
} catch (Exception e) {
LOG.error("Error fetching data, replacing client", e);
replaceCurator();
}
}
private void notifyListeners(final Event event) {
executor.submit(new Runnable() {
//@Override Java 5 compatibility
public void run() {
listeners.forEach(new Function() {
//@Override Java 5 compatibility
public Void apply(EventListener listener) {
listener.newEvent(event);
return null;
}
});
}
});
}
private synchronized void replaceCurator() {
long timestamp = curatorTimestamp.get();
long age = System.currentTimeMillis() - timestamp;
long minAge = TimeUnit.MINUTES.toMillis(1);
// only attempt reconnect once per minute so we don't exacerbate a failure scenario
if (age < minAge || !curatorTimestamp.compareAndSet(timestamp, System.currentTimeMillis())) {
return;
}
executor.submit(new Runnable() {
//@Override Java 5 compatibility
public void run() {
CuratorFramework previous = curatorReference.getAndSet(newCurator());
cleanup(previous);
fetch(false);
}
});
}
private CuratorFramework newCurator() {
CuratorFramework curator = null;
try {
curator = curatorSupplier.get();
if (curator.getState() != CuratorFrameworkState.STARTED) {
curator.start();
}
curator.getConnectionStateListenable().addListener(new ConnectionStateListener() {
//@Override Java 5 compatibility
public void stateChanged(CuratorFramework client, ConnectionState newState) {
switch (newState) {
case SUSPENDED:
case LOST:
LOG.error("Connection lost or suspended, replacing client");
replaceCurator();
}
}
});
curator.getUnhandledErrorListenable().addListener(new UnhandledErrorListener() {
//@Override Java 5 compatibility
public void unhandledError(String message, Throwable e) {
LOG.error("Curator error, replacing client", e);
replaceCurator();
}
});
return curator;
} catch (Exception e) {
LOG.error("Error creating curator", e);
cleanup(curator);
return null;
}
}
private void cleanup(final CuratorFramework curator) {
if (curator == null) {
return;
}
executor.submit(new Runnable() {
//@Override Java 5 compatibility
public void run() {
try {
curator.close();
} catch (Exception e) {
LOG.debug("Error closing curator", e);
}
}
});
}
}