Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/checkstyle/checkstyle.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<module name="AvoidStarImport"/>
<module name="NeedBraces"/>
<module name="EmptyBlock"/>
<module name="OverloadMethodsDeclarationOrder"/>
</module>

<module name="LineLength">
Expand Down
9 changes: 9 additions & 0 deletions src/main/java/org/cache/cluster/ClusterInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.cache.cluster;

import java.util.List;

public record ClusterInfo(
Integer replicationFactor,
List<CacheNode> nodes
) {
}
4 changes: 3 additions & 1 deletion src/main/java/org/cache/config/CacheConfig.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.cache.config;

import org.cache.cluster.CacheNode;
import org.cache.cluster.ClusterInfo;
import org.cache.eviction.EvictionPolicy;
import org.cache.protocol.codec.KeyCodec;

Expand All @@ -9,6 +10,7 @@ public record CacheConfig(
long defaultTtlMillis,
KeyCodec<Object> keyCodec,
EvictionPolicy<Object> evictionPolicy,
CacheNode cacheNode
CacheNode cacheNode,
ClusterInfo clusterInfo
) {
}
12 changes: 12 additions & 0 deletions src/main/java/org/cache/config/CacheConfigException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.cache.config;

public class CacheConfigException extends IllegalArgumentException {

public CacheConfigException(String message) {
super(message);
}

public CacheConfigException(String message, Throwable cause) {
super(message, cause);
}
}
135 changes: 120 additions & 15 deletions src/main/java/org/cache/config/CacheConfigLoader.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.cache.config;

import org.cache.cluster.CacheNode;
import org.cache.cluster.ClusterInfo;
import org.cache.eviction.EvictionPolicy;
import org.cache.eviction.EvictionPolicyType;
import org.cache.eviction.LruEvictionPolicy;
Expand All @@ -12,8 +13,12 @@
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;

public final class CacheConfigLoader {

Expand All @@ -27,6 +32,7 @@ public final class CacheConfigLoader {
private static final int DEFAULT_HTTP_PORT = 8080;
private static final int DEFAULT_TCP_PORT = 2020;
private static final int DEFAULT_CLUSTER_PORT = 10001;
private static final String CLUSTER_NODES = ConfigKey.merge(ConfigKey.CLUSTER, ConfigKey.NODES);

private final String configFile;

Expand All @@ -42,15 +48,17 @@ public CacheConfig load() {
Properties properties = loadProperties();

return new CacheConfig(
getInt(properties, ConfigKey.CAPACITY, DEFAULT_CAPACITY),
getInt(properties, ConfigKey.CAPACITY.getPropertyName(), DEFAULT_CAPACITY),
getLong(properties, ConfigKey.DEFAULT_TTL_MILLIS, DEFAULT_TTL_MILLIS),
createKeyCodec(KeyType.from(getString(properties, ConfigKey.KEY_TYPE, DEFAULT_KEY_TYPE))),
createKeyCodec(KeyType.from(getString(properties, ConfigKey.KEY_TYPE.getPropertyName(),
DEFAULT_KEY_TYPE))),
createEvictionPolicy(EvictionPolicyType.from(getString(
properties,
ConfigKey.EVICTION_POLICY,
ConfigKey.EVICTION_POLICY.getPropertyName(),
DEFAULT_EVICTION_POLICY
))),
buildCacheNode(properties)
buildCacheNode(properties),
buildClusterInfo(properties)
);
}

Expand All @@ -71,14 +79,91 @@ private Properties loadProperties() {

private CacheNode buildCacheNode(Properties properties) {
return new CacheNode(
getString(properties, ConfigKey.NODE_ID, DEFAULT_NODE_ID),
getString(properties, ConfigKey.NODE_HOST, DEFAULT_NODE_HOST),
getInt(properties, ConfigKey.NODE_HTTP_PORT, DEFAULT_HTTP_PORT),
getInt(properties, ConfigKey.NODE_TCP_PORT, DEFAULT_TCP_PORT),
getInt(properties, ConfigKey.NODE_CLUSTER_PORT, DEFAULT_CLUSTER_PORT)
getString(properties, ConfigKey.merge(ConfigKey.NODE, ConfigKey.ID), DEFAULT_NODE_ID),
getString(properties, ConfigKey.merge(ConfigKey.NODE, ConfigKey.HOST), DEFAULT_NODE_HOST),
getInt(properties, ConfigKey.merge(ConfigKey.NODE, ConfigKey.HTTP_PORT), DEFAULT_HTTP_PORT),
getInt(properties, ConfigKey.merge(ConfigKey.NODE, ConfigKey.TCP_PORT), DEFAULT_TCP_PORT),
getInt(properties, ConfigKey.merge(ConfigKey.NODE, ConfigKey.CLUSTER_PORT), DEFAULT_CLUSTER_PORT)
);
}

private ClusterInfo buildClusterInfo(Properties properties) {
if (!hasClusterConfig(properties)) {
return null;
}

int replicationFactor = getInt(properties,
ConfigKey.merge(ConfigKey.CLUSTER, ConfigKey.REPLICATION_FACTOR), 1);

List<CacheNode> nodes = new ArrayList<>();
int index = 0;

while (true) {
String idKey = clusterNodeKey(index, ConfigKey.ID);
String id = properties.getProperty(idKey);

if (id == null) {
break;
}

CacheNode node = new CacheNode(
getString(properties, idKey),
getString(properties, clusterNodeKey(index, ConfigKey.HOST)),
getInt(properties, clusterNodeKey(index, ConfigKey.HTTP_PORT)),
getInt(properties, clusterNodeKey(index, ConfigKey.TCP_PORT)),
getInt(properties, clusterNodeKey(index, ConfigKey.CLUSTER_PORT))
);

nodes.add(node);
index++;
}

validateClusterInfo(replicationFactor, nodes);

return new ClusterInfo(replicationFactor, nodes);
}

private void validateClusterInfo(int replicationFactor, List<CacheNode> nodes) {
if (replicationFactor < 1) {
throw new CacheConfigException("Cluster replication factor must be at least 1");
}

if (replicationFactor > nodes.size()) {
throw new CacheConfigException("Cluster replication factor must not exceed number of active nodes");
}

Set<String> nodeIds = new HashSet<>();
Set<String> hostPorts = new HashSet<>();

for (CacheNode node : nodes) {
if (!nodeIds.add(node.id())) {
throw new CacheConfigException("Cluster node ids must be unique: " + node.id());
}

addHostPort(hostPorts, node.host(), node.httpPort());
addHostPort(hostPorts, node.host(), node.tcpPort());
addHostPort(hostPorts, node.host(), node.clusterPort());
}
}

private void addHostPort(Set<String> hostPorts, String host, int port) {
String hostPort = host + ":" + port;
if (!hostPorts.add(hostPort)) {
throw new CacheConfigException("Cluster node host-port combinations must be unique: " + hostPort);
}
}

private boolean hasClusterConfig(Properties properties) {
return properties.stringPropertyNames()
.stream()
.anyMatch(key -> key.equals(ConfigKey.CLUSTER.getPropertyName())
|| key.startsWith(ConfigKey.CLUSTER.getPropertyName() + "."));
}

private String clusterNodeKey(int index, ConfigKey field) {
return CLUSTER_NODES + "[" + index + "]." + field.getPropertyName();
}

@SuppressWarnings("unchecked")
private KeyCodec<Object> createKeyCodec(KeyType keyType) {
return switch (keyType) {
Expand All @@ -94,25 +179,44 @@ private EvictionPolicy<Object> createEvictionPolicy(EvictionPolicyType policy) {
};
}

private String getString(Properties properties, ConfigKey key, String defaultValue) {
String value = properties.getProperty(key.getPropertyName());
private String getString(Properties properties, String key, String defaultValue) {
String value = properties.getProperty(key);
if (value == null || value.isBlank()) {
return defaultValue;
}

return value.trim();
}

private int getInt(Properties properties, ConfigKey key, int defaultValue) {
String value = properties.getProperty(key.getPropertyName());
private String getString(Properties properties, String key) {
String value = properties.getProperty(key);
if (value == null || value.isBlank()) {
throw new CacheConfigException("Missing required configuration key: " + key);
}

return value.trim();
}

private int getInt(Properties properties, String key, int defaultValue) {
String value = properties.getProperty(key);
if (value == null || value.isBlank()) {
return defaultValue;
}

try {
return Integer.parseInt(value.trim());
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid integer value for configuration key '" + key.getPropertyName() + "': " + value, e);
throw new CacheConfigException("Invalid integer value for configuration key '" + key + "': " + value, e);
}
}

private int getInt(Properties properties, String key) {
String value = getString(properties, key);

try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
throw new CacheConfigException("Invalid integer value for configuration key '" + key + "': " + value, e);
}
}

Expand All @@ -125,7 +229,8 @@ private long getLong(Properties properties, ConfigKey key, long defaultValue) {
try {
return Long.parseLong(value.trim());
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid long value for configuration key '" + key.getPropertyName() + "': " + value, e);
throw new CacheConfigException("Invalid long value for configuration key '" + key.getPropertyName() +
"': " + value, e);
}
}
}
24 changes: 19 additions & 5 deletions src/main/java/org/cache/config/ConfigKey.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@ public enum ConfigKey {
DEFAULT_TTL_MILLIS("defaultTtlMillis"),
KEY_TYPE("key-type"),
EVICTION_POLICY("eviction-policy"),
NODE_ID("node.id"),
NODE_HOST("node.host"),
NODE_HTTP_PORT("node.http-port"),
NODE_TCP_PORT("node.tcp-port"),
NODE_CLUSTER_PORT("node.cluster-port");
NODE("node"),
CLUSTER("cluster"),
NODES("nodes"),
ID("id"),
HOST("host"),
HTTP_PORT("http-port"),
TCP_PORT("tcp-port"),
CLUSTER_PORT("cluster-port"),
REPLICATION_FACTOR("replication-factor");

private final String propertyName;

Expand All @@ -20,4 +24,14 @@ public enum ConfigKey {
public String getPropertyName() {
return propertyName;
}

public static String merge(ConfigKey first, ConfigKey... others) {
StringBuilder propertyName = new StringBuilder(first.getPropertyName());

for (ConfigKey key : others) {
propertyName.append('.').append(key.getPropertyName());
}

return propertyName.toString();
}
}
21 changes: 21 additions & 0 deletions src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,24 @@ node:
http-port: 8080
tcp-port: 2020
cluster-port: 10001

cluster:
replication-factor: 2
nodes:
- id: node-a
host: localhost
http-port: 8080
tcp-port: 2020
cluster-port: 10001

- id: node-b
host: localhost
http-port: 9001
tcp-port: 9002
cluster-port: 10002

- id: node-c
host: localhost
http-port: 9003
tcp-port: 9004
cluster-port: 10003
Loading
Loading