Add hazelcast cache store, revised the cache implementation (#1047)

* Add hazelcast cache store, revised the cache implementation

* Remove unsued import

Co-authored-by: Turgay Can <turgay.can@inomera.com>
pull/1665/head
Turgay Can 2020-08-29 09:28:55 +03:00 committed by GitHub
parent 8c948fd670
commit 437d4bd81b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 230 additions and 28 deletions

View File

@ -67,6 +67,7 @@ ext {
githubApiVersion = "1.84" githubApiVersion = "1.84"
powermockVersion = "1.6.6" powermockVersion = "1.6.6"
powermockApiMockito2 = "2.0.7" powermockApiMockito2 = "2.0.7"
hzVersion = "3.12"
} }
dependencies { dependencies {
@ -118,6 +119,7 @@ dependencies {
implementation "org.iq80.leveldb:leveldb:$levelDbVersion" implementation "org.iq80.leveldb:leveldb:$levelDbVersion"
implementation "redis.clients:jedis:$jedisVersion" implementation "redis.clients:jedis:$jedisVersion"
implementation "com.hazelcast:hazelcast-all:$hzVersion"
runtimeOnly "com.h2database:h2:$h2Version" runtimeOnly "com.h2database:h2:$h2Version"
runtimeOnly "mysql:mysql-connector-java" runtimeOnly "mysql:mysql-connector-java"

View File

@ -4,6 +4,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import run.halo.app.config.properties.HaloProperties;
import run.halo.app.utils.DateUtils; import run.halo.app.utils.DateUtils;
import java.util.Date; import java.util.Date;
@ -19,6 +20,8 @@ import java.util.concurrent.TimeUnit;
@Slf4j @Slf4j
public abstract class AbstractCacheStore<K, V> implements CacheStore<K, V> { public abstract class AbstractCacheStore<K, V> implements CacheStore<K, V> {
protected HaloProperties haloProperties;
/** /**
* Get cache wrapper by key. * Get cache wrapper by key.
* *

View File

@ -18,13 +18,13 @@ import java.util.concurrent.TimeUnit;
*/ */
@Slf4j @Slf4j
public abstract class AbstractStringCacheStore extends AbstractCacheStore<String, String> { public abstract class AbstractStringCacheStore extends AbstractCacheStore<String, String> {
protected Optional<CacheWrapper<String>> jsonToCacheWrapper(String json) { protected Optional<CacheWrapper<String>> jsonToCacheWrapper(String json) {
Assert.hasText(json, "json value must not be null"); Assert.hasText(json, "json value must not be null");
CacheWrapper<String> cacheWrapper = null; CacheWrapper<String> cacheWrapper = null;
try { try {
cacheWrapper = JsonUtils.jsonToObject(json, CacheWrapper.class); cacheWrapper = JsonUtils.jsonToObject(json, CacheWrapper.class);
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace();
log.debug("Failed to convert json to wrapper value bytes: [{}]", json, e); log.debug("Failed to convert json to wrapper value bytes: [{}]", json, e);
} }
return Optional.ofNullable(cacheWrapper); return Optional.ofNullable(cacheWrapper);

View File

@ -0,0 +1,130 @@
package run.halo.app.cache;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.hazelcast.client.HazelcastClient;
import com.hazelcast.client.config.ClientConfig;
import com.hazelcast.client.config.ClientConnectionStrategyConfig;
import com.hazelcast.client.config.ClientNetworkConfig;
import com.hazelcast.client.config.ConnectionRetryConfig;
import com.hazelcast.config.GroupConfig;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.core.IMap;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.Assert;
import run.halo.app.config.properties.HaloProperties;
import run.halo.app.utils.JsonUtils;
import javax.annotation.PostConstruct;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
/**
* hazelcast cache store
* Create by turgay can on 2020/08/27 10:28
*/
@Slf4j
public class HazelcastStore extends AbstractStringCacheStore {
private static final int ONE_SECOND_AS_MILLIS = 1000;
private static final String DEFAULT_MAP = "haloMap";
private HazelcastInstance hazelcastInstance;
public HazelcastStore(HaloProperties haloProperties) {
super.haloProperties = haloProperties;
}
@PostConstruct
public void init() {
if (hazelcastInstance != null) {
return;
}
try {
final ClientConfig config = new ClientConfig();
final GroupConfig groupConfig = config.getGroupConfig();
final String hazelcastGroupName = haloProperties.getHazelcastGroupName();
groupConfig.setName(hazelcastGroupName);
final ClientNetworkConfig network = config.getNetworkConfig();
final List<String> hazelcastMembers = haloProperties.getHazelcastMembers();
network.setAddresses(hazelcastMembers);
configureClientRetryPolicy(config);
log.info("Hazelcast client instance starting::GroupName={}::Members={}", hazelcastGroupName, hazelcastMembers);
this.hazelcastInstance = HazelcastClient.newHazelcastClient(config);
log.info("Hazelcast client instance started");
} catch (Exception ex) {
log.error("init hazelcast error ", ex);
}
}
private void configureClientRetryPolicy(ClientConfig config) {
ConnectionRetryConfig retryConfig = new ConnectionRetryConfig();
retryConfig.setEnabled(true);
retryConfig.setInitialBackoffMillis(haloProperties.getInitialBackoffSeconds() * ONE_SECOND_AS_MILLIS);
config.getConnectionStrategyConfig()
.setReconnectMode(ClientConnectionStrategyConfig.ReconnectMode.ON)
.setConnectionRetryConfig(retryConfig);
}
@Override
Optional<CacheWrapper<String>> getInternal(String key) {
Assert.hasText(key, "Cache key must not be blank");
final IMap<String, String> defaultHaloMap = getDefaultStringMap();
final String v = defaultHaloMap.get(key);
return StringUtils.isBlank(v) ? Optional.empty() : jsonToCacheWrapper(v);
}
@Override
void putInternal(String key, CacheWrapper<String> cacheWrapper) {
putInternalIfAbsent(key, cacheWrapper);
try {
getDefaultStringMap().set(key, JsonUtils.objectToJson(cacheWrapper));
Date ttl = cacheWrapper.getExpireAt();
if (ttl != null) {
getDefaultStringMap().setTtl(key, ttl.getTime(), TimeUnit.MILLISECONDS);
}
} catch (Exception e) {
log.warn("Put cache fail json2object key: [{}] value:[{}]", key, cacheWrapper);
}
}
@Override
Boolean putInternalIfAbsent(String key, CacheWrapper<String> cacheWrapper) {
Assert.hasText(key, "Cache key must not be blank");
Assert.notNull(cacheWrapper, "Cache wrapper must not be null");
try {
final IMap<String, String> defaultHaloMap = getDefaultStringMap();
if (defaultHaloMap.containsKey(key)) {
log.warn("Failed to put the cache, because the key: [{}] has been present already", key);
return false;
}
Date ttl = cacheWrapper.getExpireAt();
if (ttl != null) {
defaultHaloMap.set(key, JsonUtils.objectToJson(cacheWrapper), ttl.getTime(), TimeUnit.MILLISECONDS);
}
return true;
} catch (JsonProcessingException e) {
log.warn("Put cache fail json2object key: [{}] value:[{}]", key, cacheWrapper);
}
log.debug("Cache key: [{}], original cache wrapper: [{}]", key, cacheWrapper);
return false;
}
@Override
public void delete(String key) {
Assert.hasText(key, "Cache key must not be blank");
final IMap<String, String> defaultHaloMap = getDefaultStringMap();
defaultHaloMap.delete(key);
log.debug("Removed key: [{}]", key);
}
private IMap<String, String> getDefaultStringMap() {
return hazelcastInstance.getMap(DEFAULT_MAP);
}
}

View File

@ -4,7 +4,6 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.iq80.leveldb.*; import org.iq80.leveldb.*;
import org.iq80.leveldb.impl.Iq80DBFactory; import org.iq80.leveldb.impl.Iq80DBFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import run.halo.app.config.properties.HaloProperties; import run.halo.app.config.properties.HaloProperties;
@ -32,8 +31,9 @@ public class LevelCacheStore extends AbstractStringCacheStore {
private Timer timer; private Timer timer;
@Autowired public LevelCacheStore(HaloProperties haloProperties) {
private HaloProperties haloProperties; super.haloProperties = haloProperties;
}
@PostConstruct @PostConstruct
public void init() { public void init() {

View File

@ -39,8 +39,6 @@ public class RedisCacheStore extends AbstractStringCacheStore {
*/ */
private final Lock lock = new ReentrantLock(); private final Lock lock = new ReentrantLock();
protected HaloProperties haloProperties;
public RedisCacheStore(HaloProperties haloProperties) { public RedisCacheStore(HaloProperties haloProperties) {
this.haloProperties = haloProperties; this.haloProperties = haloProperties;
initRedis(); initRedis();
@ -74,19 +72,6 @@ public class RedisCacheStore extends AbstractStringCacheStore {
log.info("Initialized cache redis cluster: {}", REDIS.getClusterNodes()); log.info("Initialized cache redis cluster: {}", REDIS.getClusterNodes());
} }
protected JedisCluster redis() {
if (REDIS == null) {
synchronized (RedisCacheStore.class) {
if (REDIS != null) {
return REDIS;
}
initRedis();
return REDIS;
}
}
return REDIS;
}
@NotNull @NotNull
@Override @Override
Optional<CacheWrapper<String>> getInternal(@NotNull String key) { Optional<CacheWrapper<String>> getInternal(@NotNull String key) {

View File

@ -11,10 +11,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import run.halo.app.cache.AbstractStringCacheStore; import run.halo.app.cache.*;
import run.halo.app.cache.InMemoryCacheStore;
import run.halo.app.cache.LevelCacheStore;
import run.halo.app.cache.RedisCacheStore;
import run.halo.app.config.properties.HaloProperties; import run.halo.app.config.properties.HaloProperties;
import run.halo.app.utils.HttpClientUtils; import run.halo.app.utils.HttpClientUtils;
@ -33,7 +30,7 @@ import java.security.NoSuchAlgorithmException;
public class HaloConfiguration { public class HaloConfiguration {
@Autowired @Autowired
HaloProperties haloProperties; private HaloProperties haloProperties;
@Bean @Bean
public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) { public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {
@ -56,17 +53,19 @@ public class HaloConfiguration {
AbstractStringCacheStore stringCacheStore; AbstractStringCacheStore stringCacheStore;
switch (haloProperties.getCache()) { switch (haloProperties.getCache()) {
case "level": case "level":
stringCacheStore = new LevelCacheStore(); stringCacheStore = new LevelCacheStore(this.haloProperties);
break; break;
case "redis": case "redis":
stringCacheStore = new RedisCacheStore(this.haloProperties); stringCacheStore = new RedisCacheStore(this.haloProperties);
break; break;
case "hazelcast":
stringCacheStore = new HazelcastStore(this.haloProperties);
break;
case "memory": case "memory":
default: default:
//memory or default //memory or default
stringCacheStore = new InMemoryCacheStore(); stringCacheStore = new InMemoryCacheStore();
break; break;
} }
log.info("Halo cache store load impl : [{}]", stringCacheStore.getClass()); log.info("Halo cache store load impl : [{}]", stringCacheStore.getClass());
return stringCacheStore; return stringCacheStore;

View File

@ -6,6 +6,7 @@ import run.halo.app.model.enums.Mode;
import java.time.Duration; import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List;
import static run.halo.app.model.support.HaloConst.*; import static run.halo.app.model.support.HaloConst.*;
import static run.halo.app.utils.HaloUtils.ensureSuffix; import static run.halo.app.utils.HaloUtils.ensureSuffix;
@ -79,9 +80,18 @@ public class HaloProperties {
*/ */
private String cache = "memory"; private String cache = "memory";
private ArrayList<String> cacheRedisNodes = new ArrayList<>(); private List<String> cacheRedisNodes = new ArrayList<>();
private String cacheRedisPassword = ""; private String cacheRedisPassword = "";
/**
* hazelcast cache store impl
* memory
* level
*/
private List<String> hazelcastMembers = new ArrayList<>();
private String hazelcastGroupName;
private int initialBackoffSeconds = 5;
} }

View File

@ -52,3 +52,8 @@ logging:
halo: halo:
download-timeout: 5m download-timeout: 5m
cache: memory #hazelcast
##hazelcast configs##
#hazelcastMembers: 127.0.0.1:22055 #127.0.0.1:5701 #10.4.5.100:5701,10.4.5.101:5701,10.4.5.102:5701
#hazelcastGroupName: tt-mps-hz-cluster #halo-hz-cluster
#initialBackoffSeconds: 5

View File

@ -0,0 +1,68 @@
package run.halo.app.cache;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.core.IMap;
import org.apache.commons.lang3.time.DateUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Date;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.Mockito.when;
@Disabled("Due to project test run exclusion")
@ExtendWith(MockitoExtension.class)
class HazelcastStoreTest {
@InjectMocks
private HazelcastStore hazelcastStore;
@Mock
private HazelcastInstance hazelcastInstance;
private IMap<Object, Object> haloMap;
@BeforeEach
public void initEach() {
haloMap = hazelcastInstance.getMap("haloMap");
}
@Test
void should_getInternal_For_Key1() {
final DateTime createAt = DateUtil.date();
final Date expireAt = DateUtils.addMinutes(createAt, 5);
final String value = "{ \"data\": {\"name\": \"halo\"}, \"expireAt\": \"" + expireAt + "\", \"createAt\": \"" + createAt + "\" }";
when(haloMap.get("key1")).thenReturn(value);
final Optional<CacheWrapper<String>> optionalWrapperValue1 = hazelcastStore.getInternal("key1");
final CacheWrapper<String> wrapperValue1 = optionalWrapperValue1.get();
assertNotNull(optionalWrapperValue1);
assertEquals("{\"name\": \"halo\"}", wrapperValue1.getData());
assertEquals(DateUtil.formatDate(createAt), DateUtil.formatDate(wrapperValue1.getCreateAt()));
assertEquals(DateUtil.formatDate(expireAt), DateUtil.formatDate(wrapperValue1.getExpireAt()));
}
@Test
void putInternal() {
}
@Test
void putInternalIfAbsent() {
}
@Test
void delete() {
}
}