/**
* Copyright (C) 2015 meltmedia (christian.trimble@meltmedia.com)
*
* 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.meltmedia.dropwizard.etcd.cluster;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Function;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Maps;
import com.meltmedia.dropwizard.etcd.json.EtcdEvent;
import com.meltmedia.dropwizard.etcd.json.EtcdJson.MappedEtcdDirectory;
import com.meltmedia.dropwizard.etcd.json.WatchService;
/**
* Clusters processors using an Etcd directory to store process configuration.
*
* @author Christian Trimble
*/
public class ClusterProcessor<C> {
private static Logger logger = LoggerFactory.getLogger(ClusterProcessor.class);
public static class Builder<C> {
private String nodeId;
private ObjectMapper mapper;
private MappedEtcdDirectory<ClusterProcess> directory;
private Function<C, ClusterProcessLifecycle> lifecycleFactory;
private TypeReference<C> type;
public ClusterProcessor.Builder<C> withNodeId(String nodeId) {
this.nodeId = nodeId;
return this;
}
public ClusterProcessor.Builder<C> withMapper(ObjectMapper mapper) {
this.mapper = mapper;
return this;
}
public ClusterProcessor.Builder<C> withDirectory(MappedEtcdDirectory<ClusterProcess> directory) {
this.directory = directory;
return this;
}
public ClusterProcessor.Builder<C> withLifecycleFactory(
Function<C, ClusterProcessLifecycle> lifecycleFactory) {
this.lifecycleFactory = lifecycleFactory;
return this;
}
public ClusterProcessor.Builder<C> withType(TypeReference<C> type) {
this.type = type;
return this;
}
public ClusterProcessor<C> build() {
ClusterProcessor<C> service =
new ClusterProcessor<C>(directory, mapper, nodeId, type, lifecycleFactory);
return service;
}
}
public static <C> ClusterProcessor.Builder<C> builder() {
return new ClusterProcessor.Builder<C>();
}
private ConcurrentMap<String, ClusterProcessLifecycle> lifecycles = Maps.newConcurrentMap();
private MappedEtcdDirectory<ClusterProcess> directory;
private String nodeId;
private Function<C, ClusterProcessLifecycle> lifecycleFactory;
private ObjectMapper mapper;
private TypeReference<C> type;
private WatchService.Watch watch;
public ClusterProcessor(MappedEtcdDirectory<ClusterProcess> directory, ObjectMapper mapper,
String nodeId, TypeReference<C> type, Function<C, ClusterProcessLifecycle> lifecycleFactory) {
this.directory = directory;
this.mapper = mapper;
this.nodeId = nodeId;
this.type = type;
this.lifecycleFactory = lifecycleFactory;
}
public void start() {
watch = directory.registerWatch(this::handle);
}
public void stop() {
logger.debug("stopping watch");
watch.stop();
logger.debug("stopped watch");
lifecycles.values().stream().forEach(lifecycle -> {
try {
logger.debug("stopping lifecycle");
lifecycle.stop();
logger.debug("stopped lifecycle");
} catch (Exception e) {
logger.warn("execption encountered while stopping processor", e);
}
});
lifecycles.clear();
}
public void handle(EtcdEvent<ClusterProcess> event) {
switch (event.getType()) {
case added:
if (nodeId.equals(event.getValue().getAssignedTo())) {
startProcess(event.getKey(), event.getValue());
}
break;
case updated:
if (nodeId.equals(event.getPrevValue().getAssignedTo())) {
stopProcess(event.getKey(), event.getPrevValue());
}
if (nodeId.equals(event.getValue().getAssignedTo())) {
startProcess(event.getKey(), event.getValue());
}
break;
case removed:
if (nodeId.equals(event.getPrevValue().getAssignedTo())) {
stopProcess(event.getKey(), event.getPrevValue());
}
break;
}
}
protected void startProcess(String key, ClusterProcess node) {
C configuration = convertConfiguration(node);
ClusterProcessLifecycle process = lifecycleFactory.apply(configuration);
ClusterProcessLifecycle existing = lifecycles.put(key, process);
if (existing != null)
safe(key, existing::stop);
safe(key, process::start);
}
protected void stopProcess(String key, ClusterProcess node) {
ClusterProcessLifecycle existing = lifecycles.remove(key);
if (existing != null)
safe(key, existing::stop);
}
protected static void safe(String key, Runnable runnable) {
try {
runnable.run();
} catch (Throwable t) {
logger.warn(String.format("message thrown while calling process %s", key), t);
}
}
protected C convertConfiguration(ClusterProcess node) {
try {
return mapper.convertValue(node.getConfiguration(), type);
} catch (IllegalArgumentException e) {
logger.error("could not convert process configuration", e);
throw e;
}
}
}