From 437d4bd81bc828b4c9cc23f6a122e15726da0771 Mon Sep 17 00:00:00 2001 From: Turgay Can Date: Sat, 29 Aug 2020 09:28:55 +0300 Subject: [PATCH] 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 --- build.gradle | 2 + .../halo/app/cache/AbstractCacheStore.java | 3 + .../app/cache/AbstractStringCacheStore.java | 2 +- .../run/halo/app/cache/HazelcastStore.java | 130 ++++++++++++++++++ .../run/halo/app/cache/LevelCacheStore.java | 6 +- .../run/halo/app/cache/RedisCacheStore.java | 15 -- .../halo/app/config/HaloConfiguration.java | 13 +- .../app/config/properties/HaloProperties.java | 12 +- src/main/resources/application.yaml | 7 +- .../halo/app/cache/HazelcastStoreTest.java | 68 +++++++++ 10 files changed, 230 insertions(+), 28 deletions(-) create mode 100644 src/main/java/run/halo/app/cache/HazelcastStore.java create mode 100644 src/test/java/run/halo/app/cache/HazelcastStoreTest.java diff --git a/build.gradle b/build.gradle index 354faff07..5c168d138 100644 --- a/build.gradle +++ b/build.gradle @@ -67,6 +67,7 @@ ext { githubApiVersion = "1.84" powermockVersion = "1.6.6" powermockApiMockito2 = "2.0.7" + hzVersion = "3.12" } dependencies { @@ -118,6 +119,7 @@ dependencies { implementation "org.iq80.leveldb:leveldb:$levelDbVersion" implementation "redis.clients:jedis:$jedisVersion" + implementation "com.hazelcast:hazelcast-all:$hzVersion" runtimeOnly "com.h2database:h2:$h2Version" runtimeOnly "mysql:mysql-connector-java" diff --git a/src/main/java/run/halo/app/cache/AbstractCacheStore.java b/src/main/java/run/halo/app/cache/AbstractCacheStore.java index 5c4939a3c..ba3dca6d0 100644 --- a/src/main/java/run/halo/app/cache/AbstractCacheStore.java +++ b/src/main/java/run/halo/app/cache/AbstractCacheStore.java @@ -4,6 +4,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import run.halo.app.config.properties.HaloProperties; import run.halo.app.utils.DateUtils; import java.util.Date; @@ -19,6 +20,8 @@ import java.util.concurrent.TimeUnit; @Slf4j public abstract class AbstractCacheStore implements CacheStore { + protected HaloProperties haloProperties; + /** * Get cache wrapper by key. * diff --git a/src/main/java/run/halo/app/cache/AbstractStringCacheStore.java b/src/main/java/run/halo/app/cache/AbstractStringCacheStore.java index f3e1cc0a4..8497e5fc4 100644 --- a/src/main/java/run/halo/app/cache/AbstractStringCacheStore.java +++ b/src/main/java/run/halo/app/cache/AbstractStringCacheStore.java @@ -18,13 +18,13 @@ import java.util.concurrent.TimeUnit; */ @Slf4j public abstract class AbstractStringCacheStore extends AbstractCacheStore { + protected Optional> jsonToCacheWrapper(String json) { Assert.hasText(json, "json value must not be null"); CacheWrapper cacheWrapper = null; try { cacheWrapper = JsonUtils.jsonToObject(json, CacheWrapper.class); } catch (IOException e) { - e.printStackTrace(); log.debug("Failed to convert json to wrapper value bytes: [{}]", json, e); } return Optional.ofNullable(cacheWrapper); diff --git a/src/main/java/run/halo/app/cache/HazelcastStore.java b/src/main/java/run/halo/app/cache/HazelcastStore.java new file mode 100644 index 000000000..d9523ca37 --- /dev/null +++ b/src/main/java/run/halo/app/cache/HazelcastStore.java @@ -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 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> getInternal(String key) { + Assert.hasText(key, "Cache key must not be blank"); + final IMap defaultHaloMap = getDefaultStringMap(); + final String v = defaultHaloMap.get(key); + return StringUtils.isBlank(v) ? Optional.empty() : jsonToCacheWrapper(v); + } + + @Override + void putInternal(String key, CacheWrapper 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 cacheWrapper) { + Assert.hasText(key, "Cache key must not be blank"); + Assert.notNull(cacheWrapper, "Cache wrapper must not be null"); + try { + final IMap 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 defaultHaloMap = getDefaultStringMap(); + defaultHaloMap.delete(key); + log.debug("Removed key: [{}]", key); + } + + private IMap getDefaultStringMap() { + return hazelcastInstance.getMap(DEFAULT_MAP); + } +} diff --git a/src/main/java/run/halo/app/cache/LevelCacheStore.java b/src/main/java/run/halo/app/cache/LevelCacheStore.java index 82bf49415..fc880f3d6 100644 --- a/src/main/java/run/halo/app/cache/LevelCacheStore.java +++ b/src/main/java/run/halo/app/cache/LevelCacheStore.java @@ -4,7 +4,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import lombok.extern.slf4j.Slf4j; import org.iq80.leveldb.*; import org.iq80.leveldb.impl.Iq80DBFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import run.halo.app.config.properties.HaloProperties; @@ -32,8 +31,9 @@ public class LevelCacheStore extends AbstractStringCacheStore { private Timer timer; - @Autowired - private HaloProperties haloProperties; + public LevelCacheStore(HaloProperties haloProperties) { + super.haloProperties = haloProperties; + } @PostConstruct public void init() { diff --git a/src/main/java/run/halo/app/cache/RedisCacheStore.java b/src/main/java/run/halo/app/cache/RedisCacheStore.java index 9a196f16f..93aa8291c 100644 --- a/src/main/java/run/halo/app/cache/RedisCacheStore.java +++ b/src/main/java/run/halo/app/cache/RedisCacheStore.java @@ -39,8 +39,6 @@ public class RedisCacheStore extends AbstractStringCacheStore { */ private final Lock lock = new ReentrantLock(); - protected HaloProperties haloProperties; - public RedisCacheStore(HaloProperties haloProperties) { this.haloProperties = haloProperties; initRedis(); @@ -74,19 +72,6 @@ public class RedisCacheStore extends AbstractStringCacheStore { 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 @Override Optional> getInternal(@NotNull String key) { diff --git a/src/main/java/run/halo/app/config/HaloConfiguration.java b/src/main/java/run/halo/app/config/HaloConfiguration.java index 72dc97db7..ff32235a7 100644 --- a/src/main/java/run/halo/app/config/HaloConfiguration.java +++ b/src/main/java/run/halo/app/config/HaloConfiguration.java @@ -11,10 +11,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; import org.springframework.web.client.RestTemplate; -import run.halo.app.cache.AbstractStringCacheStore; -import run.halo.app.cache.InMemoryCacheStore; -import run.halo.app.cache.LevelCacheStore; -import run.halo.app.cache.RedisCacheStore; +import run.halo.app.cache.*; import run.halo.app.config.properties.HaloProperties; import run.halo.app.utils.HttpClientUtils; @@ -33,7 +30,7 @@ import java.security.NoSuchAlgorithmException; public class HaloConfiguration { @Autowired - HaloProperties haloProperties; + private HaloProperties haloProperties; @Bean public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) { @@ -56,17 +53,19 @@ public class HaloConfiguration { AbstractStringCacheStore stringCacheStore; switch (haloProperties.getCache()) { case "level": - stringCacheStore = new LevelCacheStore(); + stringCacheStore = new LevelCacheStore(this.haloProperties); break; case "redis": stringCacheStore = new RedisCacheStore(this.haloProperties); break; + case "hazelcast": + stringCacheStore = new HazelcastStore(this.haloProperties); + break; case "memory": default: //memory or default stringCacheStore = new InMemoryCacheStore(); break; - } log.info("Halo cache store load impl : [{}]", stringCacheStore.getClass()); return stringCacheStore; diff --git a/src/main/java/run/halo/app/config/properties/HaloProperties.java b/src/main/java/run/halo/app/config/properties/HaloProperties.java index 1aa3bf78e..cdc80690d 100644 --- a/src/main/java/run/halo/app/config/properties/HaloProperties.java +++ b/src/main/java/run/halo/app/config/properties/HaloProperties.java @@ -6,6 +6,7 @@ import run.halo.app.model.enums.Mode; import java.time.Duration; import java.util.ArrayList; +import java.util.List; import static run.halo.app.model.support.HaloConst.*; import static run.halo.app.utils.HaloUtils.ensureSuffix; @@ -79,9 +80,18 @@ public class HaloProperties { */ private String cache = "memory"; - private ArrayList cacheRedisNodes = new ArrayList<>(); + private List cacheRedisNodes = new ArrayList<>(); private String cacheRedisPassword = ""; + /** + * hazelcast cache store impl + * memory + * level + */ + private List hazelcastMembers = new ArrayList<>(); + private String hazelcastGroupName; + + private int initialBackoffSeconds = 5; } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index f2f586d9b..cae9be2e5 100755 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -51,4 +51,9 @@ logging: path: ${user.home}/.halo/logs halo: - download-timeout: 5m \ No newline at end of file + 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 diff --git a/src/test/java/run/halo/app/cache/HazelcastStoreTest.java b/src/test/java/run/halo/app/cache/HazelcastStoreTest.java new file mode 100644 index 000000000..ba7a8ee8b --- /dev/null +++ b/src/test/java/run/halo/app/cache/HazelcastStoreTest.java @@ -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 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> optionalWrapperValue1 = hazelcastStore.getInternal("key1"); + + final CacheWrapper 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() { + } +}