├── .gitignore ├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src └── main │ └── java │ └── jamsesso │ └── meshmap │ ├── MeshMap.java │ ├── MessageHandler.java │ ├── MeshMapCluster.java │ ├── MeshMapException.java │ ├── MeshMapRuntimeException.java │ ├── MeshMapMarshallException.java │ ├── CachedMeshMapCluster.java │ ├── examples │ ├── LocalWorkerNode.java │ ├── Timer.java │ ├── BenchmarkNode.java │ └── InteractiveNode.java │ ├── Retryable.java │ ├── Node.java │ ├── LocalMeshMapCluster.java │ ├── Message.java │ ├── MeshMapServer.java │ └── MeshMapImpl.java ├── LICENSE ├── gradlew.bat ├── README.md └── gradlew /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .gradle 3 | cluster* 4 | build -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'meshmap' 2 | 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamsesso/meshmap/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/java/jamsesso/meshmap/MeshMap.java: -------------------------------------------------------------------------------- 1 | package jamsesso.meshmap; 2 | 3 | import java.util.Map; 4 | 5 | public interface MeshMap extends Map, AutoCloseable { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/jamsesso/meshmap/MessageHandler.java: -------------------------------------------------------------------------------- 1 | package jamsesso.meshmap; 2 | 3 | @FunctionalInterface 4 | public interface MessageHandler { 5 | Message handle(Message message); 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/jamsesso/meshmap/MeshMapCluster.java: -------------------------------------------------------------------------------- 1 | package jamsesso.meshmap; 2 | 3 | import java.util.List; 4 | 5 | public interface MeshMapCluster { 6 | List getAllNodes(); 7 | 8 | MeshMap join() throws MeshMapException; 9 | } 10 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Dec 22 19:45:07 AST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip 7 | -------------------------------------------------------------------------------- /src/main/java/jamsesso/meshmap/MeshMapException.java: -------------------------------------------------------------------------------- 1 | package jamsesso.meshmap; 2 | 3 | import lombok.NoArgsConstructor; 4 | 5 | @NoArgsConstructor 6 | public class MeshMapException extends Exception { 7 | public MeshMapException(String msg) { 8 | super(msg); 9 | } 10 | 11 | public MeshMapException(String msg, Throwable cause) { 12 | super(msg, cause); 13 | } 14 | 15 | public MeshMapException(Throwable cause) { 16 | super(cause); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/jamsesso/meshmap/MeshMapRuntimeException.java: -------------------------------------------------------------------------------- 1 | package jamsesso.meshmap; 2 | 3 | import lombok.NoArgsConstructor; 4 | 5 | @NoArgsConstructor 6 | public class MeshMapRuntimeException extends RuntimeException { 7 | public MeshMapRuntimeException(String msg) { 8 | super(msg); 9 | } 10 | 11 | public MeshMapRuntimeException(String msg, Throwable cause) { 12 | super(msg, cause); 13 | } 14 | 15 | public MeshMapRuntimeException(Throwable cause) { 16 | super(cause); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/jamsesso/meshmap/MeshMapMarshallException.java: -------------------------------------------------------------------------------- 1 | package jamsesso.meshmap; 2 | 3 | import lombok.NoArgsConstructor; 4 | 5 | @NoArgsConstructor 6 | public class MeshMapMarshallException extends MeshMapRuntimeException { 7 | public MeshMapMarshallException(String msg) { 8 | super(msg); 9 | } 10 | 11 | public MeshMapMarshallException(String msg, Throwable cause) { 12 | super(msg, cause); 13 | } 14 | 15 | public MeshMapMarshallException(Throwable cause) { 16 | super(cause); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/jamsesso/meshmap/CachedMeshMapCluster.java: -------------------------------------------------------------------------------- 1 | package jamsesso.meshmap; 2 | 3 | import java.util.List; 4 | 5 | public class CachedMeshMapCluster implements MeshMapCluster { 6 | private final Object[] lock = new Object[0]; 7 | private final MeshMapCluster delegate; 8 | private List nodes; 9 | 10 | public CachedMeshMapCluster(MeshMapCluster cluster) { 11 | this.delegate = cluster; 12 | } 13 | 14 | @Override 15 | public List getAllNodes() { 16 | synchronized (lock) { 17 | if(nodes == null) { 18 | nodes = delegate.getAllNodes(); 19 | } 20 | 21 | return nodes; 22 | } 23 | } 24 | 25 | @Override 26 | public MeshMap join() throws MeshMapException { 27 | return delegate.join(); 28 | } 29 | 30 | public void clearCache() { 31 | synchronized (lock) { 32 | nodes = null; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/jamsesso/meshmap/examples/LocalWorkerNode.java: -------------------------------------------------------------------------------- 1 | package jamsesso.meshmap.examples; 2 | 3 | import jamsesso.meshmap.LocalMeshMapCluster; 4 | import jamsesso.meshmap.MeshMap; 5 | import jamsesso.meshmap.Node; 6 | 7 | import java.io.File; 8 | import java.net.InetSocketAddress; 9 | 10 | import static java.lang.System.in; 11 | import static java.lang.System.out; 12 | 13 | public class LocalWorkerNode { 14 | public static void main(String[] args) throws Exception { 15 | // Get input from arguments. 16 | int port = Integer.parseInt(args[0]); 17 | String directory = args[1]; 18 | 19 | // Set up cluster and wait. Enter key kills the server. 20 | Node self = new Node(new InetSocketAddress("127.0.0.1", port)); 21 | 22 | try (LocalMeshMapCluster cluster = new LocalMeshMapCluster(self, new File("cluster/" + directory)); 23 | MeshMap map = cluster.join()) { 24 | in.read(); 25 | out.println("Node is going down..."); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 R. Sam Jesso 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/main/java/jamsesso/meshmap/examples/Timer.java: -------------------------------------------------------------------------------- 1 | package jamsesso.meshmap.examples; 2 | 3 | import java.util.LongSummaryStatistics; 4 | import java.util.function.Consumer; 5 | import java.util.stream.LongStream; 6 | 7 | import static java.lang.System.out; 8 | 9 | public class Timer { 10 | public static void time(String name, int iterations, Consumer iteration) { 11 | long[] timings = new long[iterations]; 12 | out.println("Started " + name + "..."); 13 | 14 | for (int i = 0; i < iterations; i++) { 15 | long iterationStartTime = System.currentTimeMillis(); 16 | iteration.accept(i); 17 | timings[i] = System.currentTimeMillis() - iterationStartTime; 18 | } 19 | 20 | LongSummaryStatistics stats = LongStream.of(timings).summaryStatistics(); 21 | long elapsedTime = stats.getSum(); 22 | long maxTime = stats.getMax(); 23 | long minTime = stats.getMin(); 24 | double avgTime = stats.getAverage(); 25 | 26 | out.println(name + " took " + elapsedTime + "ms " + 27 | "(max=" + maxTime + "ms, min=" + minTime + ", avg=" + avgTime + ")"); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/jamsesso/meshmap/Retryable.java: -------------------------------------------------------------------------------- 1 | package jamsesso.meshmap; 2 | 3 | import java.util.stream.Stream; 4 | 5 | public final class Retryable { 6 | private final Task task; 7 | private Class[] causes; 8 | 9 | private Retryable(Task task) { 10 | this.task = task; 11 | } 12 | 13 | public static Retryable retry(Task task) { 14 | return new Retryable<>(task); 15 | } 16 | 17 | @SafeVarargs 18 | public final Retryable on(Class... causes) { 19 | this.causes = causes; 20 | return this; 21 | } 22 | 23 | public final T times(int times) throws Exception { 24 | // Performs the action times-1 times. 25 | for (int i = 1; i < times; i++) { 26 | try { 27 | return task.apply(); 28 | } 29 | catch (Exception e) { 30 | boolean shouldRetry = Stream.of(causes).anyMatch(cause -> cause.isInstance(e)); 31 | 32 | if (!shouldRetry) { 33 | throw e; 34 | } 35 | } 36 | } 37 | 38 | // Last try. 39 | return task.apply(); 40 | } 41 | 42 | public interface Task { 43 | T apply() throws Exception; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/jamsesso/meshmap/examples/BenchmarkNode.java: -------------------------------------------------------------------------------- 1 | package jamsesso.meshmap.examples; 2 | 3 | import jamsesso.meshmap.LocalMeshMapCluster; 4 | import jamsesso.meshmap.MeshMap; 5 | import jamsesso.meshmap.Node; 6 | 7 | import java.io.File; 8 | import java.net.InetSocketAddress; 9 | import java.util.UUID; 10 | 11 | import static java.lang.System.out; 12 | 13 | public class BenchmarkNode { 14 | private static final int COUNT = 100_000; 15 | 16 | public static void main(String[] args) throws Exception { 17 | // Get input from arguments. 18 | int port = Integer.parseInt(args[0]); 19 | String directory = args[1]; 20 | 21 | // Generate data 22 | String[] keys = new String[COUNT]; 23 | String[] values = new String[COUNT]; 24 | 25 | for (int i = 0; i < COUNT; i++) { 26 | keys[i] = UUID.randomUUID().toString(); 27 | values[i] = String.valueOf(i); 28 | } 29 | 30 | // Set up cluster and wait. Enter key kills the server. 31 | Node self = new Node(new InetSocketAddress("127.0.0.1", port)); 32 | 33 | try (LocalMeshMapCluster cluster = new LocalMeshMapCluster(self, new File("cluster/" + directory)); 34 | MeshMap map = cluster.join()) { 35 | Timer.time("PUT", COUNT, i -> { 36 | map.put(keys[i], values[i]); 37 | 38 | if(i % 1_000 == 0) { 39 | out.println("Put " + i + " key/value pairs"); 40 | } 41 | }); 42 | 43 | map.clear(); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/jamsesso/meshmap/Node.java: -------------------------------------------------------------------------------- 1 | package jamsesso.meshmap; 2 | 3 | import lombok.EqualsAndHashCode; 4 | import lombok.Getter; 5 | 6 | import java.io.Serializable; 7 | import java.net.InetSocketAddress; 8 | import java.util.UUID; 9 | 10 | @EqualsAndHashCode 11 | public class Node implements Serializable { 12 | private UUID id = UUID.randomUUID(); 13 | private @Getter InetSocketAddress address; 14 | 15 | public Node(InetSocketAddress address) { 16 | this(UUID.randomUUID(), address); 17 | } 18 | 19 | public Node(UUID id, InetSocketAddress address) { 20 | this.id = id; 21 | this.address = address; 22 | } 23 | 24 | public int getId() { 25 | return id.hashCode() & Integer.MAX_VALUE; 26 | } 27 | 28 | @Override 29 | public String toString() { 30 | return address.getHostString() + '#' + address.getPort() + '#' + id; 31 | } 32 | 33 | public static Node from(String str) { 34 | if (str == null) { 35 | throw new IllegalArgumentException("String must not be null"); 36 | } 37 | 38 | String[] parts = str.split("#"); 39 | 40 | if (parts.length != 3) { 41 | throw new IllegalArgumentException("Node address must contain only a host and port"); 42 | } 43 | 44 | String host = parts[0]; 45 | int port; 46 | UUID id; 47 | 48 | try { 49 | port = Integer.parseInt(parts[1]); 50 | } 51 | catch (NumberFormatException e) { 52 | throw new IllegalArgumentException("Node address port must be a valid number", e); 53 | } 54 | 55 | try { 56 | id = UUID.fromString(parts[2]); 57 | } 58 | catch (IllegalArgumentException e) { 59 | throw new IllegalArgumentException("Node ID must be a valid UUID", e); 60 | } 61 | 62 | return new Node(id, new InetSocketAddress(host, port)); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /src/main/java/jamsesso/meshmap/LocalMeshMapCluster.java: -------------------------------------------------------------------------------- 1 | package jamsesso.meshmap; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.util.Comparator; 6 | import java.util.List; 7 | import java.util.stream.Collectors; 8 | import java.util.stream.Stream; 9 | 10 | public class LocalMeshMapCluster implements MeshMapCluster, AutoCloseable { 11 | private final Node self; 12 | private final File directory; 13 | private MeshMapServer server; 14 | private MeshMap map; 15 | 16 | public LocalMeshMapCluster(Node self, File directory) { 17 | directory.mkdirs(); 18 | 19 | if (!directory.isDirectory()) { 20 | throw new IllegalArgumentException("File passed to LocalMeshMapCluster must be a directory"); 21 | } 22 | 23 | if (!directory.canRead() || !directory.canWrite()) { 24 | throw new IllegalArgumentException("Directory must be readable and writable"); 25 | } 26 | 27 | this.self = self; 28 | this.directory = directory; 29 | } 30 | 31 | @Override 32 | public List getAllNodes() { 33 | return Stream.of(directory.listFiles()) 34 | .filter(File::isFile) 35 | .map(File::getName) 36 | .map(Node::from) 37 | .sorted(Comparator.comparingInt(Node::getId)) 38 | .collect(Collectors.toList()); 39 | } 40 | 41 | @Override 42 | public MeshMap join() throws MeshMapException { 43 | if (this.map != null) { 44 | return (MeshMap) this.map; 45 | } 46 | 47 | File file = new File(directory.getAbsolutePath() + File.separator + self.toString()); 48 | 49 | try { 50 | boolean didCreateFile = file.createNewFile(); 51 | 52 | if(!didCreateFile) { 53 | throw new MeshMapException("File could not be created: " + file.getName()); 54 | } 55 | } 56 | catch (IOException e) { 57 | throw new MeshMapException("Unable to join cluster", e); 58 | } 59 | 60 | file.deleteOnExit(); 61 | 62 | server = new MeshMapServer(this, self); 63 | MeshMapImpl map = new MeshMapImpl<>(this, server, self); 64 | 65 | try { 66 | server.start(map); 67 | map.open(); 68 | } 69 | catch(IOException e) { 70 | throw new MeshMapException("Unable to start the mesh map server", e); 71 | } 72 | 73 | server.broadcast(Message.HI); 74 | this.map = map; 75 | 76 | return map; 77 | } 78 | 79 | @Override 80 | public void close() throws Exception { 81 | File file = new File(directory.getAbsolutePath() + File.separator + self.toString()); 82 | boolean didDeleteFile = file.delete(); 83 | 84 | if (!didDeleteFile) { 85 | throw new MeshMapException("File could not be deleted: " + file.getName()); 86 | } 87 | 88 | if (server != null) { 89 | server.broadcast(Message.BYE); 90 | server.close(); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/jamsesso/meshmap/examples/InteractiveNode.java: -------------------------------------------------------------------------------- 1 | package jamsesso.meshmap.examples; 2 | 3 | import jamsesso.meshmap.LocalMeshMapCluster; 4 | import jamsesso.meshmap.MeshMap; 5 | import jamsesso.meshmap.Node; 6 | 7 | import java.io.File; 8 | import java.net.InetSocketAddress; 9 | import java.util.Arrays; 10 | import java.util.Scanner; 11 | 12 | import static java.lang.System.in; 13 | import static java.lang.System.out; 14 | 15 | public class InteractiveNode { 16 | private final static Scanner scanner = new Scanner(in); 17 | 18 | public static void main(String[] args) throws Exception { 19 | int port = Integer.parseInt(args[0]); 20 | String directory = args[1]; 21 | Node self = new Node(new InetSocketAddress("127.0.0.1", port)); 22 | 23 | try (LocalMeshMapCluster cluster = new LocalMeshMapCluster(self, new File("cluster/" + directory)); 24 | MeshMap map = cluster.join()) { 25 | boolean running = true; 26 | 27 | do { 28 | out.println("Menu:"); 29 | out.println(" 1 - add a key/value pair"); 30 | out.println(" 2 - get a value"); 31 | out.println(" 3 - remove a value"); 32 | out.println(" 4 - get map size"); 33 | out.println(" 5 - get all keys"); 34 | out.println(" 6 - quit"); 35 | out.print("Choose an option: "); 36 | 37 | int option = scanner.nextInt(); 38 | scanner.nextLine(); // Consume the new line character. 39 | 40 | switch (option) { 41 | case 1: 42 | addPair(map); 43 | break; 44 | case 2: 45 | getValue(map); 46 | break; 47 | case 3: 48 | removeValue(map); 49 | break; 50 | case 4: 51 | getSize(map); 52 | break; 53 | case 5: 54 | getKeys(map); 55 | break; 56 | default: 57 | running = false; 58 | break; 59 | } 60 | } 61 | while(running); 62 | } 63 | } 64 | 65 | private static void addPair(MeshMap map) { 66 | out.print("Key: "); 67 | String key = scanner.nextLine(); 68 | out.print("Value: "); 69 | String value = scanner.nextLine(); 70 | 71 | map.put(key, value); 72 | out.println("OK"); 73 | } 74 | 75 | private static void getValue(MeshMap map) { 76 | out.print("Search for key: "); 77 | String key = scanner.nextLine(); 78 | String value = map.get(key); 79 | 80 | if (value == null) { 81 | out.println("(not found)"); 82 | } 83 | else { 84 | out.println(value); 85 | } 86 | } 87 | 88 | private static void getKeys(MeshMap map) { 89 | out.println("All keys: " + Arrays.toString(map.keySet().toArray())); 90 | } 91 | 92 | private static void getSize(MeshMap map) { 93 | out.println("Size: " + map.size()); 94 | } 95 | 96 | private static void removeValue(MeshMap map) { 97 | out.print("Key to remove: "); 98 | String key = scanner.nextLine(); 99 | map.remove(key); 100 | out.println("OK"); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/jamsesso/meshmap/Message.java: -------------------------------------------------------------------------------- 1 | package jamsesso.meshmap; 2 | 3 | import lombok.Data; 4 | import lombok.EqualsAndHashCode; 5 | import lombok.ToString; 6 | 7 | import java.io.*; 8 | import java.nio.ByteBuffer; 9 | 10 | /** 11 | * Messages have the following byte format. 12 | * 13 | * +-----------------+------------------+----------------+ 14 | * | 32 byte type ID | 4 byte size (=X) | X byte payload | 15 | * +-----------------+------------------+----------------+ 16 | */ 17 | @Data 18 | @EqualsAndHashCode 19 | @ToString(exclude = "payload") 20 | public class Message { 21 | public static final String TYPE_HI = "HI"; 22 | public static final String TYPE_BYE = "BYE"; 23 | public static final String TYPE_ACK = "ACK"; 24 | public static final String TYPE_ERR = "ERR"; 25 | public static final String TYPE_YES = "YES"; 26 | public static final String TYPE_NO = "NO"; 27 | 28 | public static final Message HI = new Message(TYPE_HI); 29 | public static final Message BYE = new Message(TYPE_BYE); 30 | public static final Message ACK = new Message(TYPE_ACK); 31 | public static final Message ERR = new Message(TYPE_ERR); 32 | public static final Message YES = new Message(TYPE_YES); 33 | public static final Message NO = new Message(TYPE_NO); 34 | 35 | private static final int MESSAGE_TYPE = 32; 36 | private static final int MESSAGE_SIZE = 4; 37 | 38 | private final String type; 39 | private final int length; 40 | private final byte[] payload; 41 | 42 | public Message(String type) { 43 | this(type, new byte[0]); 44 | } 45 | 46 | public Message(String type, Object payload) { 47 | this(type, toBytes(payload)); 48 | } 49 | 50 | public Message(String type, byte[] payload) { 51 | checkType(type); 52 | this.type = type; 53 | this.length = payload.length; 54 | this.payload = payload; 55 | } 56 | 57 | public T getPayload(Class clazz) { 58 | return clazz.cast(fromBytes(payload)); 59 | } 60 | 61 | public int getPayloadAsInt() { 62 | return ByteBuffer.wrap(payload).getInt(); 63 | } 64 | 65 | public void write(OutputStream outputStream) throws IOException { 66 | ByteBuffer buffer = ByteBuffer.allocate(MESSAGE_TYPE + MESSAGE_SIZE + length); 67 | byte[] typeBytes = type.getBytes(); 68 | byte[] remainingBytes = new byte[MESSAGE_TYPE - typeBytes.length]; 69 | 70 | buffer.put(typeBytes); 71 | buffer.put(remainingBytes); 72 | buffer.putInt(length); 73 | buffer.put(payload); 74 | 75 | outputStream.write(buffer.array()); 76 | } 77 | 78 | public static Message read(InputStream inputStream) throws IOException { 79 | byte[] msgType = new byte[MESSAGE_TYPE]; 80 | byte[] msgSize = new byte[MESSAGE_SIZE]; 81 | 82 | inputStream.read(msgType); 83 | inputStream.read(msgSize); 84 | 85 | // Create a buffer for the payload 86 | int size = ByteBuffer.wrap(msgSize).getInt(); 87 | byte[] msgPayload = new byte[size]; 88 | 89 | inputStream.read(msgPayload); 90 | 91 | return new Message(new String(msgType).trim(), msgPayload); 92 | } 93 | 94 | private static byte[] toBytes(Object object) { 95 | try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); 96 | ObjectOutput out = new ObjectOutputStream(bos)) { 97 | out.writeObject(object); 98 | return bos.toByteArray(); 99 | } 100 | catch(IOException e) { 101 | throw new MeshMapMarshallException(e); 102 | } 103 | } 104 | 105 | private static Object fromBytes(byte[] bytes) { 106 | try (ByteArrayInputStream bis = new ByteArrayInputStream(bytes); 107 | ObjectInput in = new ObjectInputStream(bis)) { 108 | return in.readObject(); 109 | } 110 | catch(IOException | ClassNotFoundException e) { 111 | throw new MeshMapMarshallException(e); 112 | } 113 | } 114 | 115 | private static void checkType(String type) { 116 | if (type == null) { 117 | throw new IllegalArgumentException("Type cannot be null"); 118 | } 119 | 120 | if (type.getBytes().length > MESSAGE_TYPE) { 121 | throw new IllegalArgumentException("Type cannot exceed 32 bytes"); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/main/java/jamsesso/meshmap/MeshMapServer.java: -------------------------------------------------------------------------------- 1 | package jamsesso.meshmap; 2 | 3 | import lombok.Value; 4 | 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.io.OutputStream; 8 | import java.net.ServerSocket; 9 | import java.net.Socket; 10 | import java.net.SocketException; 11 | import java.util.Map; 12 | import java.util.stream.Collectors; 13 | 14 | import static java.lang.System.err; 15 | 16 | public class MeshMapServer implements Runnable, AutoCloseable { 17 | private final MeshMapCluster cluster; 18 | private final Node self; 19 | private MessageHandler messageHandler; 20 | private volatile boolean started = false; 21 | private volatile IOException failure = null; 22 | private ServerSocket serverSocket; 23 | 24 | public MeshMapServer(MeshMapCluster cluster, Node self) { 25 | this.cluster = cluster; 26 | this.self = self; 27 | } 28 | 29 | public void start(MessageHandler messageHandler) throws IOException { 30 | if (this.messageHandler != null) { 31 | throw new IllegalStateException("Cannot restart a dead mesh map server"); 32 | } 33 | 34 | this.messageHandler = messageHandler; 35 | new Thread(this).start(); 36 | 37 | // Wait for the server to start. 38 | while (!started); 39 | 40 | if (failure != null) { 41 | throw failure; 42 | } 43 | } 44 | 45 | public Message message(Node node, Message message) throws IOException { 46 | try { 47 | return Retryable.retry(() -> { 48 | try (Socket socket = new Socket()) { 49 | socket.connect(node.getAddress()); 50 | 51 | try (OutputStream outputStream = socket.getOutputStream(); 52 | InputStream inputStream = socket.getInputStream()) { 53 | message.write(outputStream); 54 | outputStream.flush(); 55 | return Message.read(inputStream); 56 | } 57 | } 58 | }).on(IOException.class).times(3); 59 | } 60 | catch (Exception e) { 61 | throw new IOException(e); 62 | } 63 | } 64 | 65 | public Map broadcast(Message message) { 66 | return cluster.getAllNodes().parallelStream() 67 | .filter(node -> !node.equals(self)) 68 | .map(node -> { 69 | try { 70 | return new BroadcastResponse(node, message(node, message)); 71 | } 72 | catch(IOException e) { 73 | // TODO Better error handling strategy needed. 74 | err.println("Unable to broadcast message to node: " + node); 75 | e.printStackTrace(); 76 | 77 | return new BroadcastResponse(node, Message.ERR); 78 | } 79 | }) 80 | .collect(Collectors.toMap(BroadcastResponse::getNode, BroadcastResponse::getResponse)); 81 | } 82 | 83 | @Override 84 | public void run() { 85 | try { 86 | serverSocket = new ServerSocket(self.getAddress().getPort()); 87 | } 88 | catch (IOException e) { 89 | failure = e; 90 | } 91 | finally { 92 | started = true; 93 | } 94 | 95 | while (!serverSocket.isClosed()) { 96 | try (Socket socket = serverSocket.accept(); 97 | InputStream inputStream = socket.getInputStream(); 98 | OutputStream outputStream = socket.getOutputStream()) { 99 | Message message = Message.read(inputStream); 100 | Message response = messageHandler.handle(message); 101 | 102 | if(response == null) { 103 | response = Message.ACK; 104 | } 105 | 106 | response.write(outputStream); 107 | outputStream.flush(); 108 | } 109 | catch (SocketException e) { 110 | // Socket was closed. Nothing to do here. Node is going down. 111 | } 112 | catch (IOException e) { 113 | // TODO Better error handling strategy is needed. 114 | err.println("Unable to accept connection"); 115 | e.printStackTrace(); 116 | } 117 | } 118 | } 119 | 120 | @Override 121 | public void close() throws Exception { 122 | serverSocket.close(); 123 | } 124 | 125 | @Value 126 | private static class BroadcastResponse { 127 | Node node; 128 | Message response; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MeshMap 2 | === 3 | 4 | MeshMap is a Java-based implementation of a P2P distributed hash map. Service discovery is provided out of the box, but easy to extend for applications with their own service discovery mechanisms. 5 | 6 | The goal of MeshMap is to provide a simple API with all of the functionality of a map data structure with the data distributed across the member nodes in a cluster. MeshMap uses a P2P system (modified ideas borrowed from the Chord algorithm) to work without any master coordination node. 7 | 8 | # Example 9 | 10 | ```java 11 | Node self = new Node("127.0.0.1", 45700); 12 | 13 | try (LocalMeshMapCluster cluster = new LocalMeshMapCluster(self, new File("sd")); 14 | MeshMap people = cluster.join()) { 15 | Person sam = people.get("Sam"); // Look through the cluster for Sam 16 | int numPeople = people.size(); // Get the number of entries across all nodes 17 | 18 | // etc... The full java.util.Map API is available! 19 | } 20 | ``` 21 | 22 | With an instance of `MeshMap`, applications can share information as easily as using get/put operations on the map. 23 | 24 | For more examples, see the [jamsesso.meshmap.examples](https://github.com/jamsesso/meshmap/tree/master/src/main/java/jamsesso/meshmap/examples) package. 25 | 26 | ## Interactive Demo 27 | 28 | To use the interactive demo, start by booting any number of `LocalWorkerNode` instances with the same cluster name. 29 | Next, run an instance of `InteractiveNode`. Both programs have the same argument format. The following script builds and starts a worker node and the interactive console. 30 | 31 | ``` 32 | ./gradlew build 33 | java -cp ./build/libs/meshmap-{VERSION}.jar jamsesso.meshmap.examples.LocalWorkerNode 45100 cluster1 34 | java -cp ./build/libs/meshmap-{VERSION}.jar jamsesso.meshmap.examples.InteractiveNode 45101 cluster1 35 | ``` 36 | 37 | # Do you need MeshMap? 38 | 39 | If you find yourself needed to iterate over all of the entries in a map, your use-case will probably negate the benefit of using MeshMap. 40 | 41 | If you agree with the following statements, MeshMap may be able to help you: 42 | 43 | - I want my data stored in memory at all times for high speed. 44 | - I have too much data to store in a single node. 45 | - My data will fit in cluster memory capacity (sum of each node's memory capacity). 46 | - Nodes need to share data between one another. 47 | - My query requirements are light and a database is overkill. 48 | - I do not want to maintain a Redis cluster. 49 | - If I go to production, I am willing to contribute bug fixes back to MeshMap. 50 | 51 | # Service Discovery & Healing 52 | 53 | Currently, there is only 1 bundled MeshMap service discovery mechanism available. Contributions for other service discovery mechanisms are welcome! 54 | 55 | | Service Discovery Strategy | When to Use? | 56 | |-|-| 57 | | LocalMeshMapCluster | All of the nodes in the cluster share a single filesystem | 58 | | ~~S3MeshMapCluster~~ (TODO) | Nodes are EC2 instances that share visibility to an S3 bucket | 59 | 60 | Because data is partitioned across the different nodes in the cluster, when a node joins or leaves the cluster the cluster needs to _heal_ itself. The healing process involves transferring data between the node that is joining or leaving and at most 1 other node in the cluster. When a node leaves the cluster, the data stored locally is transferred to another node determined by MeshMap. When a node joins the cluster, it transfers some of the data from at most 1 other node in the cluster to itself. 61 | 62 | # Performance 63 | 64 | Performance will mostly be bound by network conditions. I do not currently have any benchmarks to demonstrate. 65 | 66 | It is important to mention that it currently takes `O(N)` time to determine which node a map key lives on (where `N` is the number of nodes in the cluster). This **does not** mean that each node is contacted to determine if it contains a key. For example, during a `get` or `put` operation, only a single network call is made. The complexity for calculating which node a key lives on could be reduced to `O(log N)` in the future, but because typically `N < 25`, the benefits are thought to be negligible. 67 | 68 | **Note**: Some of the API calls are significantly more expensive than others. 69 | 70 | | API | Network Hits (Worst Case) | 71 | |-|-| 72 | | `size()` | `N-1` | 73 | | `isEmpty()` | `N-1` | 74 | | `containsKey(Object key)` | `1` | 75 | | `containsValue(Object value)` | `N-1` | 76 | | `get(Object k)` | `1` | 77 | | `put(K key, V value)` | `1` | 78 | | `remove(K key)` | `1` | 79 | | `putAll(Map m)` | `m.size()` | 80 | | `clear()` | `N-1` | 81 | | `keySet()` | `N-1` | 82 | | `values()` | `N-1` | 83 | | `entrySet()` | `N-1` | 84 | 85 | # Building 86 | 87 | MeshMap uses Gradle as a build system and includes the Gradle Wrapper. 88 | 89 | ``` 90 | ~$ ./gradlew build 91 | ``` 92 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save ( ) { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /src/main/java/jamsesso/meshmap/MeshMapImpl.java: -------------------------------------------------------------------------------- 1 | package jamsesso.meshmap; 2 | 3 | import lombok.Value; 4 | 5 | import java.io.IOException; 6 | import java.io.Serializable; 7 | import java.nio.ByteBuffer; 8 | import java.util.*; 9 | import java.util.concurrent.ConcurrentHashMap; 10 | import java.util.stream.Collectors; 11 | import java.util.stream.Stream; 12 | 13 | public class MeshMapImpl implements MeshMap, MessageHandler { 14 | private static final String TYPE_PUT = "PUT"; 15 | private static final String TYPE_GET = "GET"; 16 | private static final String TYPE_REMOVE = "REMOVE"; 17 | private static final String TYPE_CLEAR = "CLEAR"; 18 | private static final String TYPE_KEY_SET = "KEY_SET"; 19 | private static final String TYPE_SIZE = "SIZE"; 20 | private static final String TYPE_CONTAINS_KEY = "CONTAINS_KEY"; 21 | private static final String TYPE_CONTAINS_VALUE = "CONTAINS_VALUE"; 22 | private static final String TYPE_DUMP_ENTRIES = "DUMP_ENTRIES"; 23 | 24 | private final CachedMeshMapCluster cluster; 25 | private final MeshMapServer server; 26 | private final Node self; 27 | private final Map delegate; 28 | 29 | public MeshMapImpl(MeshMapCluster cluster, MeshMapServer server, Node self) { 30 | this.cluster = new CachedMeshMapCluster(cluster); 31 | this.server = server; 32 | this.self = self; 33 | this.delegate = new ConcurrentHashMap<>(); 34 | } 35 | 36 | @Override 37 | public Message handle(Message message) { 38 | switch (message.getType()) { 39 | case Message.TYPE_HI: 40 | case Message.TYPE_BYE: { 41 | cluster.clearCache(); 42 | return Message.ACK; 43 | } 44 | 45 | case TYPE_GET: { 46 | Object key = message.getPayload(Object.class); 47 | return new Message(TYPE_GET, delegate.get(key)); 48 | } 49 | 50 | case TYPE_PUT: { 51 | Entry entry = message.getPayload(Entry.class); 52 | delegate.put(entry.getKey(), entry.getValue()); 53 | return Message.ACK; 54 | } 55 | 56 | case TYPE_REMOVE: { 57 | Object key = message.getPayload(Object.class); 58 | return new Message(TYPE_REMOVE, delegate.remove(key)); 59 | } 60 | 61 | case TYPE_CLEAR: { 62 | delegate.clear(); 63 | return Message.ACK; 64 | } 65 | 66 | case TYPE_KEY_SET: { 67 | Object[] keys = delegate.keySet().toArray(); 68 | return new Message(TYPE_KEY_SET, keys); 69 | } 70 | 71 | case TYPE_SIZE: { 72 | return new Message(TYPE_SIZE, ByteBuffer.allocate(4).putInt(delegate.size()).array()); 73 | } 74 | 75 | case TYPE_CONTAINS_KEY: { 76 | Object key = message.getPayload(Object.class); 77 | return delegate.containsKey(key) ? Message.YES : Message.NO; 78 | } 79 | 80 | case TYPE_CONTAINS_VALUE: { 81 | Object value = message.getPayload(Object.class); 82 | return delegate.containsValue(value) ? Message.YES : Message.NO; 83 | } 84 | 85 | case TYPE_DUMP_ENTRIES: { 86 | Entry[] entries = delegate.entrySet().stream() 87 | .map(entry -> new Entry(entry.getKey(), entry.getValue())) 88 | .collect(Collectors.toList()) 89 | .toArray(new Entry[0]); 90 | 91 | return new Message(TYPE_DUMP_ENTRIES, entries); 92 | } 93 | 94 | default: { 95 | return Message.ACK; 96 | } 97 | } 98 | } 99 | 100 | @Override 101 | public int size() { 102 | Message sizeMsg = new Message(TYPE_SIZE); 103 | 104 | return delegate.size() + server.broadcast(sizeMsg).entrySet().stream() 105 | .map(Map.Entry::getValue) 106 | .filter(response -> TYPE_SIZE.equals(response.getType())) 107 | .mapToInt(Message::getPayloadAsInt) 108 | .sum(); 109 | } 110 | 111 | @Override 112 | public boolean isEmpty() { 113 | return size() == 0; 114 | } 115 | 116 | @Override 117 | public boolean containsKey(Object key) { 118 | Node target = getNodeForKey(key); 119 | 120 | if (target.equals(self)) { 121 | // Key lives on the current node. 122 | return delegate.containsKey(key); 123 | } 124 | 125 | Message containsKeyMsg = new Message(TYPE_CONTAINS_KEY, key); 126 | Message response; 127 | 128 | try { 129 | response = server.message(target, containsKeyMsg); 130 | } 131 | catch(IOException e) { 132 | throw new MeshMapRuntimeException(e); 133 | } 134 | 135 | return Message.YES.equals(response); 136 | } 137 | 138 | @Override 139 | public boolean containsValue(Object value) { 140 | if (delegate.containsValue(value)) { 141 | // Check locally first. 142 | return true; 143 | } 144 | 145 | Message containsValueMsg = new Message(TYPE_CONTAINS_VALUE, value); 146 | 147 | return server.broadcast(containsValueMsg).entrySet().stream() 148 | .map(Map.Entry::getValue) 149 | .anyMatch(Message.YES::equals); 150 | } 151 | 152 | @Override 153 | public V get(Object key) { 154 | return (V) get(key, getNodeForKey(key)); 155 | } 156 | 157 | @Override 158 | public V put(K key, V value) { 159 | put(key, value, getNodeForKey(key)); 160 | return value; 161 | } 162 | 163 | @Override 164 | public V remove(Object key) { 165 | return (V) remove(key, getNodeForKey(key)); 166 | } 167 | 168 | @Override 169 | public void putAll(Map m) { 170 | m.entrySet().parallelStream().forEach(entry -> put(entry.getKey(), entry.getValue())); 171 | } 172 | 173 | @Override 174 | public void clear() { 175 | Message clearMsg = new Message(TYPE_CLEAR); 176 | server.broadcast(clearMsg); 177 | delegate.clear(); 178 | } 179 | 180 | @Override 181 | public Set keySet() { 182 | return cluster.getAllNodes().parallelStream() 183 | .map(this::keySet) 184 | .flatMap(Stream::of) 185 | .map(object -> (K) object) 186 | .collect(Collectors.toSet()); 187 | } 188 | 189 | @Override 190 | public Collection values() { 191 | return entrySet().stream() 192 | .map(Map.Entry::getValue) 193 | .collect(Collectors.toList()); 194 | } 195 | 196 | @Override 197 | public Set> entrySet() { 198 | Message dumpEntriesMsg = new Message(TYPE_DUMP_ENTRIES); 199 | Set> entries = new HashSet<>(); 200 | 201 | for (Map.Entry localEntry : delegate.entrySet()) { 202 | entries.add(new TypedEntry<>((K) localEntry.getKey(), (V) localEntry.getValue())); 203 | } 204 | 205 | for (Map.Entry response : server.broadcast(dumpEntriesMsg).entrySet()) { 206 | Entry[] remoteEntries = response.getValue().getPayload(Entry[].class); 207 | 208 | for (Entry remoteEntry : remoteEntries) { 209 | entries.add(new TypedEntry<>((K) remoteEntry.getKey(), (V) remoteEntry.getValue())); 210 | } 211 | } 212 | 213 | return entries; 214 | } 215 | 216 | @Override 217 | public String toString() { 218 | return "MeshMapImpl(Local)[" + String.join(", ", delegate.entrySet().stream() 219 | .map(entry -> entry.getKey() + ":" + entry.getValue()) 220 | .collect(Collectors.toList()).toArray(new String[0])) + "]"; 221 | } 222 | 223 | public void open() throws MeshMapException { 224 | Node successor = getSuccessorNode(); 225 | 226 | // If there is no successor, there is nothing to do. 227 | if (successor == null) { 228 | return; 229 | } 230 | 231 | // Ask the successor for their key set. 232 | Object[] keySet = keySet(successor); 233 | 234 | // Transfer the keys from the successor node that should live on this node. 235 | List keysToTransfer = Stream.of(keySet) 236 | .filter(key -> { 237 | int hash = key.hashCode() & Integer.MAX_VALUE; 238 | 239 | if (self.getId() > successor.getId()) { 240 | // The successor is the first node (circular node list) 241 | return hash <= self.getId() && hash > successor.getId(); 242 | } 243 | 244 | return hash <= self.getId(); 245 | }) 246 | .collect(Collectors.toList()); 247 | 248 | // Store the values on the current node. 249 | keysToTransfer.forEach(key -> delegate.put(key, get(key, successor))); 250 | 251 | // Delete the keys from the remote node now that the keys are transferred. 252 | keysToTransfer.forEach(key -> remove(key, successor)); 253 | } 254 | 255 | @Override 256 | public void close() throws Exception { 257 | Node successor = getSuccessorNode(); 258 | 259 | // If there is no successor, there is nothing to do. 260 | if (successor == null) { 261 | return; 262 | } 263 | 264 | // Transfer the data from this node to the successor node. 265 | delegate.forEach((key, value) -> put(key, value, successor)); 266 | } 267 | 268 | private Node getNodeForKey(Object key) { 269 | int hash = key.hashCode() & Integer.MAX_VALUE; 270 | List nodes = cluster.getAllNodes(); 271 | 272 | for (Node node : nodes) { 273 | if (hash <= node.getId()) { 274 | return node; 275 | } 276 | } 277 | 278 | return nodes.get(0); 279 | } 280 | 281 | private Node getSuccessorNode() { 282 | List nodes = cluster.getAllNodes(); 283 | 284 | if (nodes.size() <= 1) { 285 | return null; 286 | } 287 | 288 | int selfIndex = Collections.binarySearch(nodes, self, Comparator.comparingInt(Node::getId)); 289 | int successorIndex = selfIndex + 1; 290 | 291 | // Find the successor node. 292 | if (successorIndex > nodes.size() - 1) { 293 | return nodes.get(0); 294 | } 295 | else { 296 | return nodes.get(successorIndex); 297 | } 298 | } 299 | 300 | private Object get(Object key, Node target) { 301 | if (target.equals(self)) { 302 | // Value is stored on the local server. 303 | return delegate.get(key); 304 | } 305 | 306 | Message getMsg = new Message(TYPE_GET, key); 307 | Message response; 308 | 309 | try { 310 | response = server.message(target, getMsg); 311 | } 312 | catch(IOException e) { 313 | throw new MeshMapRuntimeException(e); 314 | } 315 | 316 | if (!TYPE_GET.equals(response.getType())) { 317 | throw new MeshMapRuntimeException("Unexpected response from remote node: " + response); 318 | } 319 | 320 | return response.getPayload(Object.class); 321 | } 322 | 323 | private Object put(Object key, Object value, Node target) { 324 | if (target.equals(self)) { 325 | // Value is stored on the local server. 326 | return delegate.put(key, value); 327 | } 328 | 329 | Message putMsg = new Message(TYPE_PUT, new Entry(key, value)); 330 | Message response; 331 | 332 | try { 333 | response = server.message(target, putMsg); 334 | } 335 | catch(IOException e) { 336 | throw new MeshMapRuntimeException(e); 337 | } 338 | 339 | if (!Message.ACK.equals(response)) { 340 | throw new MeshMapRuntimeException("Unexpected response from remote node: " + response); 341 | } 342 | 343 | return value; 344 | } 345 | 346 | private Object remove(Object key, Node target) { 347 | if (target.equals(self)) { 348 | // Value is stored on the local server. 349 | return delegate.remove(key); 350 | } 351 | 352 | Message removeMsg = new Message(TYPE_REMOVE, key); 353 | Message response; 354 | 355 | try { 356 | response = server.message(target, removeMsg); 357 | } 358 | catch(IOException e) { 359 | throw new MeshMapRuntimeException(e); 360 | } 361 | 362 | if (!TYPE_REMOVE.equals(response.getType())) { 363 | throw new MeshMapRuntimeException("Unexpected response from remote node: " + response); 364 | } 365 | 366 | return response.getPayload(Object.class); 367 | } 368 | 369 | private Object[] keySet(Node target) { 370 | if (target.equals(self)) { 371 | // Key is on local server. 372 | return delegate.keySet().toArray(); 373 | } 374 | 375 | Message keySetMsg = new Message(TYPE_KEY_SET); 376 | 377 | try { 378 | Message response = server.message(target, keySetMsg); 379 | return response.getPayload(Object[].class); 380 | } 381 | catch(IOException e) { 382 | throw new MeshMapRuntimeException(e); 383 | } 384 | } 385 | 386 | @Value 387 | private static class Entry implements Serializable { 388 | Object key; 389 | Object value; 390 | } 391 | 392 | @Value 393 | private static class TypedEntry implements Map.Entry { 394 | K key; 395 | V value; 396 | 397 | @Override 398 | public V setValue(V value) { 399 | throw new UnsupportedOperationException(); 400 | } 401 | } 402 | } 403 | --------------------------------------------------------------------------------