package com.meltmedia.dropwizard.etcd.cluster;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import com.codahale.metrics.Gauge;
import com.codahale.metrics.MetricRegistry;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.meltmedia.dropwizard.etcd.json.EtcdEvent;
import com.meltmedia.dropwizard.etcd.json.EtcdJson.MappedEtcdDirectory;
import com.meltmedia.dropwizard.etcd.json.WatchService;
/**
* Tracks the state of assignments in the system.
*
* @author Christian Trimble
*/
public class ClusterAssignmentTracker {
public static final String ASSIGNED = "assigned";
public static final String UNASSIGNED = "unassigned";
public static final String TOTAL = "total";
public static class AssignmentState {
final int totalProcessCount;
final int nodeProcessCount;
final int unassignedProcessCount;
final long etcdIndex;
final ClusterNode node;
final Map<String, Set<String>> processes;
final Set<String> unassigned;
AssignmentState( ClusterNode node, long etcdIndex, Map<String, Set<String>> processes, Set<String> unassigned, int totalProcessCount, int nodeProcessCount, int unassignedProcessCount ) {
this.node = node;
this.etcdIndex = etcdIndex;
this.processes = Collections.unmodifiableMap(processes);
this.unassigned = Collections.unmodifiableSet(unassigned);
this.totalProcessCount = totalProcessCount;
this.nodeProcessCount = nodeProcessCount;
this.unassignedProcessCount = unassignedProcessCount;
}
public AssignmentState addProcess(long etcdIndex, String key, ClusterProcess process) {
Optional<String> assignedTo = assignedToKey(process);
int totalProcessCount = this.totalProcessCount + 1;
int nodeProcessCount = this.nodeProcessCount + assignedTo.filter(node.getId()::equals).map(s->1).orElse(0);
int unassignedProcessCount = this.unassignedProcessCount + assignedTo.map(s->0).orElse(1);
Map<String, Set<String>> processes = Maps.newHashMap(this.processes);
Set<String> unassigned = Sets.newHashSet(this.unassigned);
if( assignedTo.isPresent() ) {
processes.computeIfPresent(assignedTo.get(), (k, v) -> {
Set<String> ids = Sets.newHashSet(v);
ids.add(key);
return ids.isEmpty() ? null : ids;
});
processes.computeIfAbsent(assignedTo.get(), k->Sets.newHashSet(key));
}
else {
unassigned.add(key);
}
return new AssignmentState(node, etcdIndex, processes, unassigned, totalProcessCount, nodeProcessCount, unassignedProcessCount );
}
public AssignmentState removeProcess( long etcdIndex, String key, ClusterProcess process ) {
Optional<String> assignedTo = assignedToKey(process);
int totalProcessCount = this.totalProcessCount - 1;
int nodeProcessCount = this.nodeProcessCount - assignedTo.filter(node.getId()::equals).map(s->1).orElse(0);
int unassignedProcessCount = this.unassignedProcessCount - assignedTo.map(s->0).orElse(1);
Map<String, Set<String>> processes = Maps.newHashMap(this.processes);
Set<String> unassigned = Sets.newHashSet(this.unassigned);
if( assignedTo.isPresent() ) {
processes.computeIfPresent(assignedTo.get(), (k, v) -> {
Set<String> ids = Sets.newHashSet(v);
ids.remove(key);
return ids.isEmpty() ? null : ids;
});
}
else {
unassigned.remove(key);
}
return new AssignmentState(node, etcdIndex, processes, unassigned, totalProcessCount, nodeProcessCount, unassignedProcessCount );
}
public int nodeProcessCount() {
return nodeProcessCount;
}
public int totalProcessCount() {
return totalProcessCount;
}
public int unassignedProcessCount() {
return unassignedProcessCount;
}
}
static Optional<String> assignedToKey(ClusterProcess process) {
return Optional.ofNullable(process.getAssignedTo());
}
private volatile AssignmentState state;
MappedEtcdDirectory<ClusterProcess> processDir;
WatchService.Watch jobsWatch;
MetricRegistry registry;
Function<String, String> metricName;
public ClusterAssignmentTracker( ClusterNode node, MappedEtcdDirectory<ClusterProcess> processDir, MetricRegistry registry, Function<String, String> metricName ) {
state = new AssignmentState(node, 0, Collections.emptyMap(), Collections.emptySet(), 0, 0, 0);
this.registry = registry;
this.metricName = metricName;
this.processDir = processDir;
}
public AssignmentState getState() {
return state;
}
public void handleProcess(EtcdEvent<ClusterProcess> event) {
switch (event.getType()) {
case added:
state = state.addProcess(event.getIndex(), event.getKey(), event.getValue());
break;
case updated:
state = state
.removeProcess(event.getIndex(), event.getKey(), event.getPrevValue())
.addProcess(event.getIndex(), event.getKey(), event.getValue());
break;
case removed:
state = state.removeProcess(event.getIndex(), event.getKey(), event.getPrevValue());
break;
}
}
public void start() {
registry.register(metricName.apply(TOTAL), (Gauge<Integer>)()->state.totalProcessCount);
registry.register(metricName.apply(ASSIGNED), (Gauge<Integer>)()->state.nodeProcessCount);
registry.register(metricName.apply(UNASSIGNED), (Gauge<Integer>)()->state.unassignedProcessCount);
jobsWatch = processDir.registerWatch(this::handleProcess);
}
public void stop() {
jobsWatch.stop();
registry.remove(metricName.apply(UNASSIGNED));
registry.remove(metricName.apply(ASSIGNED));
registry.remove(metricName.apply(TOTAL));
}
}