GOSSIP-25 Create reaper process to expire per-node data

This commit is contained in:
Edward Capriolo
2016-10-07 02:03:43 -04:00
parent daea6edb15
commit f35dddd8f2
12 changed files with 206 additions and 46 deletions

View File

@ -30,13 +30,12 @@ import org.apache.log4j.Logger;
/** /**
* This object represents the service which is responsible for gossiping with other gossip members. * This object represents the service which is responsible for gossiping with other gossip members.
* *
* @author joshclemm, harmenw
*/ */
public class GossipService { public class GossipService {
public static final Logger LOGGER = Logger.getLogger(GossipService.class); public static final Logger LOGGER = Logger.getLogger(GossipService.class);
private GossipManager gossipManager; private final GossipManager gossipManager;
/** /**
* Constructor with the default settings. * Constructor with the default settings.
@ -71,7 +70,7 @@ public class GossipService {
} }
public void start() { public void start() {
LOGGER.debug("Starting: " + get_gossipManager().getMyself().getUri()); LOGGER.debug("Starting: " + getGossipManager().getMyself().getUri());
gossipManager.init(); gossipManager.init();
} }
@ -79,25 +78,26 @@ public class GossipService {
gossipManager.shutdown(); gossipManager.shutdown();
} }
public GossipManager get_gossipManager() { public GossipManager getGossipManager() {
return gossipManager; return gossipManager;
} }
/** /**
* Gossip data to the entire cluster * Gossip data in a namespace that is per-node { node-id { key->value } }
* @param message * @param message
*/ */
public void gossipData(GossipDataMessage message){ public void gossipPerNodeData(GossipDataMessage message){
gossipManager.gossipData(message); gossipManager.gossipPerNodeData(message);
} }
/**
public GossipDataMessage findGossipData(String nodeId, String key){ * Retrieve per-node gossip data by key
return this.get_gossipManager().findGossipData(nodeId, key); * @param nodeId
} * @param key
* @return return the value if found or null if not found or expired
public void set_gossipManager(GossipManager _gossipManager) { */
this.gossipManager = _gossipManager; public GossipDataMessage findPerNodeData(String nodeId, String key){
return getGossipManager().findGossipData(nodeId, key);
} }
} }

View File

@ -0,0 +1,8 @@
package org.apache.gossip.manager;
public interface Clock {
long currentTimeMillis();
long nanoTime();
}

View File

@ -0,0 +1,58 @@
package org.apache.gossip.manager;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.apache.gossip.model.GossipDataMessage;
/**
* We wish to periodically sweep user data and remove entries past their timestamp. This
* implementation periodically sweeps through the data and removes old entries. While it might make
* sense to use a more specific high performance data-structure to handle eviction, keep in mind
* that we are not looking to store a large quantity of data as we currently have to transmit this
* data cluster wide.
*/
public class DataReaper {
private final GossipCore gossipCore;
private final ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(1);
private final Clock clock;
public DataReaper(GossipCore gossipCore, Clock clock){
this.gossipCore = gossipCore;
this.clock = clock;
}
public void init(){
Runnable reapPerNodeData = () -> {
runOnce();
};
scheduledExecutor.scheduleAtFixedRate(reapPerNodeData, 0, 5, TimeUnit.SECONDS);
}
void runOnce(){
for (Entry<String, ConcurrentHashMap<String, GossipDataMessage>> node : gossipCore.getPerNodeData().entrySet()){
reapData(node.getValue());
}
}
void reapData(ConcurrentHashMap<String, GossipDataMessage> concurrentHashMap){
for (Entry<String, GossipDataMessage> entry : concurrentHashMap.entrySet()){
if (entry.getValue().getExpireAt() < clock.currentTimeMillis()){
concurrentHashMap.remove(entry.getKey(), entry.getValue());
}
}
}
public void close(){
scheduledExecutor.shutdown();
try {
scheduledExecutor.awaitTermination(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
}
}
}

View File

@ -47,16 +47,21 @@ public class GossipCore {
perNodeData = new ConcurrentHashMap<>(); perNodeData = new ConcurrentHashMap<>();
} }
/**
*
* @param message
*/
public void addPerNodeData(GossipDataMessage message){ public void addPerNodeData(GossipDataMessage message){
ConcurrentHashMap<String,GossipDataMessage> m = new ConcurrentHashMap<>(); ConcurrentHashMap<String,GossipDataMessage> nodeMap = new ConcurrentHashMap<>();
m.put(message.getKey(), message); nodeMap.put(message.getKey(), message);
m = perNodeData.putIfAbsent(message.getNodeId(), m); nodeMap = perNodeData.putIfAbsent(message.getNodeId(), nodeMap);
if (m != null){ if (nodeMap != null){
m.put(message.getKey(), message); //TODO only put if > ts //m.put(message.getKey(), message); //TODO only put if > ts
GossipDataMessage current = nodeMap.get(message.getKey());
if (current == null){
nodeMap.replace(message.getKey(), null, message);
} else {
if (current.getTimestamp() < message.getTimestamp()){
nodeMap.replace(message.getKey(), current, message);
}
}
} }
} }

View File

@ -22,6 +22,7 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
@ -67,14 +68,20 @@ public abstract class GossipManager implements NotificationListener {
private ExecutorService gossipThreadExecutor; private ExecutorService gossipThreadExecutor;
private GossipCore gossipCore; private final GossipCore gossipCore;
private final DataReaper dataReaper;
private final Clock clock;
public GossipManager(String cluster, public GossipManager(String cluster,
URI uri, String id, GossipSettings settings, URI uri, String id, GossipSettings settings,
List<GossipMember> gossipMembers, GossipListener listener) { List<GossipMember> gossipMembers, GossipListener listener) {
this.settings = settings; this.settings = settings;
this.gossipCore = new GossipCore(this); gossipCore = new GossipCore(this);
clock = new SystemClock();
dataReaper = new DataReaper(gossipCore, clock);
me = new LocalGossipMember(cluster, uri, id, System.currentTimeMillis(), this, me = new LocalGossipMember(cluster, uri, id, System.currentTimeMillis(), this,
settings.getCleanupInterval()); settings.getCleanupInterval());
members = new ConcurrentSkipListMap<>(); members = new ConcurrentSkipListMap<>();
@ -192,6 +199,7 @@ public abstract class GossipManager implements NotificationListener {
gossipThreadExecutor.execute(passiveGossipThread); gossipThreadExecutor.execute(passiveGossipThread);
activeGossipThread = new ActiveGossipThread(this, this.gossipCore); activeGossipThread = new ActiveGossipThread(this, this.gossipCore);
activeGossipThread.init(); activeGossipThread.init();
dataReaper.init();
GossipService.LOGGER.debug("The GossipService is started."); GossipService.LOGGER.debug("The GossipService is started.");
} }
@ -202,6 +210,7 @@ public abstract class GossipManager implements NotificationListener {
gossipServiceRunning.set(false); gossipServiceRunning.set(false);
gossipThreadExecutor.shutdown(); gossipThreadExecutor.shutdown();
gossipCore.shutdown(); gossipCore.shutdown();
dataReaper.close();
if (passiveGossipThread != null) { if (passiveGossipThread != null) {
passiveGossipThread.shutdown(); passiveGossipThread.shutdown();
} }
@ -218,7 +227,10 @@ public abstract class GossipManager implements NotificationListener {
} }
} }
public void gossipData(GossipDataMessage message){ public void gossipPerNodeData(GossipDataMessage message){
Objects.nonNull(message.getKey());
Objects.nonNull(message.getTimestamp());
Objects.nonNull(message.getPayload());
message.setNodeId(me.getId()); message.setNodeId(me.getId());
gossipCore.addPerNodeData(message); gossipCore.addPerNodeData(message);
} }
@ -228,8 +240,19 @@ public abstract class GossipManager implements NotificationListener {
if (j == null){ if (j == null){
return null; return null;
} else { } else {
return j.get(key); GossipDataMessage l = j.get(key);
if (l == null){
return null;
}
if (l.getExpireAt() != null && l.getExpireAt() < clock.currentTimeMillis()) {
return null;
}
return l;
} }
} }
public DataReaper getDataReaper() {
return dataReaper;
}
} }

View File

@ -23,15 +23,11 @@ import java.net.DatagramSocket;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.SocketAddress; import java.net.SocketAddress;
import java.net.SocketException; import java.net.SocketException;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.gossip.GossipMember;
import org.apache.gossip.GossipService;
import org.apache.gossip.model.Base; import org.apache.gossip.model.Base;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import org.codehaus.jackson.map.ObjectMapper; import org.codehaus.jackson.map.ObjectMapper;
import org.apache.gossip.RemoteGossipMember;
/** /**
* [The passive thread: reply to incoming gossip request.] This class handles the passive cycle, * [The passive thread: reply to incoming gossip request.] This class handles the passive cycle,
@ -107,9 +103,9 @@ abstract public class PassiveGossipThread implements Runnable {
} }
private void debug(int packetLength, byte[] jsonBytes) { private void debug(int packetLength, byte[] jsonBytes) {
if (GossipService.LOGGER.isDebugEnabled()){ if (LOGGER.isDebugEnabled()){
String receivedMessage = new String(jsonBytes); String receivedMessage = new String(jsonBytes);
GossipService.LOGGER.debug("Received message (" + packetLength + " bytes): " LOGGER.debug("Received message (" + packetLength + " bytes): "
+ receivedMessage); + receivedMessage);
} }
} }

View File

@ -0,0 +1,15 @@
package org.apache.gossip.manager;
public class SystemClock implements Clock {
@Override
public long currentTimeMillis() {
return System.currentTimeMillis();
}
@Override
public long nanoTime() {
return System.nanoTime();
}
}

View File

@ -46,17 +46,17 @@ public class DataTest {
public Integer call() throws Exception { public Integer call() throws Exception {
int total = 0; int total = 0;
for (int i = 0; i < clusterMembers; ++i) { for (int i = 0; i < clusterMembers; ++i) {
total += clients.get(i).get_gossipManager().getLiveMembers().size(); total += clients.get(i).getGossipManager().getLiveMembers().size();
} }
return total; return total;
}}).afterWaitingAtMost(20, TimeUnit.SECONDS).isEqualTo(2); }}).afterWaitingAtMost(20, TimeUnit.SECONDS).isEqualTo(2);
clients.get(0).gossipData(msg()); clients.get(0).gossipPerNodeData(msg());
Thread.sleep(10000); Thread.sleep(10000);
TUnit.assertThat( TUnit.assertThat(
new Callable<Object> (){ new Callable<Object> (){
public Object call() throws Exception { public Object call() throws Exception {
GossipDataMessage x = clients.get(1).findGossipData(1+"" , "a"); GossipDataMessage x = clients.get(1).findPerNodeData(1+"" , "a");
if (x == null) return ""; if (x == null) return "";
else return x.getPayload(); else return x.getPayload();
}}) }})

View File

@ -78,7 +78,7 @@ public class ShutdownDeadtimeTest {
public Integer call() throws Exception { public Integer call() throws Exception {
int total = 0; int total = 0;
for (int i = 0; i < clusterMembers; ++i) { for (int i = 0; i < clusterMembers; ++i) {
total += clients.get(i).get_gossipManager().getLiveMembers().size(); total += clients.get(i).getGossipManager().getLiveMembers().size();
} }
return total; return total;
} }
@ -88,15 +88,15 @@ public class ShutdownDeadtimeTest {
Random r = new Random(); Random r = new Random();
int randomClientId = r.nextInt(clusterMembers); int randomClientId = r.nextInt(clusterMembers);
log.info("shutting down " + randomClientId); log.info("shutting down " + randomClientId);
final int shutdownPort = clients.get(randomClientId).get_gossipManager().getMyself().getUri() final int shutdownPort = clients.get(randomClientId).getGossipManager().getMyself().getUri()
.getPort(); .getPort();
final String shutdownId = clients.get(randomClientId).get_gossipManager().getMyself().getId(); final String shutdownId = clients.get(randomClientId).getGossipManager().getMyself().getId();
clients.get(randomClientId).shutdown(); clients.get(randomClientId).shutdown();
TUnit.assertThat(new Callable<Integer>() { TUnit.assertThat(new Callable<Integer>() {
public Integer call() throws Exception { public Integer call() throws Exception {
int total = 0; int total = 0;
for (int i = 0; i < clusterMembers; ++i) { for (int i = 0; i < clusterMembers; ++i) {
total += clients.get(i).get_gossipManager().getLiveMembers().size(); total += clients.get(i).getGossipManager().getLiveMembers().size();
} }
return total; return total;
} }
@ -107,7 +107,7 @@ public class ShutdownDeadtimeTest {
public Integer call() throws Exception { public Integer call() throws Exception {
int total = 0; int total = 0;
for (int i = 0; i < clusterMembers - 1; ++i) { for (int i = 0; i < clusterMembers - 1; ++i) {
total += clients.get(i).get_gossipManager().getDeadList().size(); total += clients.get(i).getGossipManager().getDeadList().size();
} }
return total; return total;
} }
@ -130,7 +130,7 @@ public class ShutdownDeadtimeTest {
public Integer call() throws Exception { public Integer call() throws Exception {
int total = 0; int total = 0;
for (int i = 0; i < clusterMembers; ++i) { for (int i = 0; i < clusterMembers; ++i) {
total += clients.get(i).get_gossipManager().getLiveMembers().size(); total += clients.get(i).getGossipManager().getLiveMembers().size();
} }
return total; return total;
} }

View File

@ -62,7 +62,7 @@ public class StartupSettingsTest {
TUnit.assertThat(new Callable<Integer> (){ TUnit.assertThat(new Callable<Integer> (){
public Integer call() throws Exception { public Integer call() throws Exception {
return firstService.get_gossipManager().getLiveMembers().size(); return firstService.getGossipManager().getLiveMembers().size();
}}).afterWaitingAtMost(30, TimeUnit.SECONDS).isEqualTo(0); }}).afterWaitingAtMost(30, TimeUnit.SECONDS).isEqualTo(0);
final GossipService serviceUnderTest = new GossipService( final GossipService serviceUnderTest = new GossipService(
StartupSettings.fromJSONFile( settingsFile ) StartupSettings.fromJSONFile( settingsFile )
@ -70,7 +70,7 @@ public class StartupSettingsTest {
serviceUnderTest.start(); serviceUnderTest.start();
TUnit.assertThat(new Callable<Integer> (){ TUnit.assertThat(new Callable<Integer> (){
public Integer call() throws Exception { public Integer call() throws Exception {
return serviceUnderTest.get_gossipManager().getLiveMembers().size(); return serviceUnderTest.getGossipManager().getLiveMembers().size();
}}).afterWaitingAtMost(10, TimeUnit.SECONDS).isEqualTo(1); }}).afterWaitingAtMost(10, TimeUnit.SECONDS).isEqualTo(1);
firstService.shutdown(); firstService.shutdown();
serviceUnderTest.shutdown(); serviceUnderTest.shutdown();

View File

@ -82,7 +82,7 @@ public class TenNodeThreeSeedTest {
public Integer call() throws Exception { public Integer call() throws Exception {
int total = 0; int total = 0;
for (int i = 0; i < clusterMembers; ++i) { for (int i = 0; i < clusterMembers; ++i) {
total += clients.get(i).get_gossipManager().getLiveMembers().size(); total += clients.get(i).getGossipManager().getLiveMembers().size();
} }
return total; return total;
}}).afterWaitingAtMost(20, TimeUnit.SECONDS).isEqualTo(20); }}).afterWaitingAtMost(20, TimeUnit.SECONDS).isEqualTo(20);

View File

@ -0,0 +1,55 @@
package org.apache.gossip.manager;
import java.net.URI;
import org.apache.gossip.GossipSettings;
import org.apache.gossip.manager.random.RandomGossipManager;
import org.apache.gossip.model.GossipDataMessage;
import org.junit.Assert;
import org.junit.Test;
import io.teknek.tunit.TUnit;
public class DataReaperTest {
@Test
public void testReaperOneShot() {
String myId = "4";
String key = "key";
String value = "a";
GossipSettings settings = new GossipSettings();
GossipManager gm = RandomGossipManager.newBuilder().cluster("abc").settings(settings)
.withId(myId).uri(URI.create("udp://localhost:5000")).build();
gm.gossipPerNodeData(perNodeDatum(key, value));
Assert.assertEquals(value, gm.findGossipData(myId, key).getPayload());
gm.getDataReaper().runOnce();
TUnit.assertThat(() -> gm.findGossipData(myId, key)).equals(null);
}
private GossipDataMessage perNodeDatum(String key, String value) {
GossipDataMessage m = new GossipDataMessage();
m.setExpireAt(System.currentTimeMillis() + 5L);
m.setKey(key);
m.setPayload(value);
m.setTimestamp(System.currentTimeMillis());
return m;
}
@Test
public void testHigherTimestampWins() {
String myId = "4";
String key = "key";
String value = "a";
GossipSettings settings = new GossipSettings();
GossipManager gm = RandomGossipManager.newBuilder().cluster("abc").settings(settings)
.withId(myId).uri(URI.create("udp://localhost:5000")).build();
GossipDataMessage before = perNodeDatum(key, value);
GossipDataMessage after = perNodeDatum(key, "b");
after.setTimestamp(after.getTimestamp() - 1);
gm.gossipPerNodeData(before);
Assert.assertEquals(value, gm.findGossipData(myId, key).getPayload());
gm.gossipPerNodeData(after);
Assert.assertEquals(value, gm.findGossipData(myId, key).getPayload());
}
}