001package io.ebean.hazelcast; 002 003import com.hazelcast.client.HazelcastClient; 004import com.hazelcast.client.config.ClientConfig; 005import com.hazelcast.config.Config; 006import com.hazelcast.core.Hazelcast; 007import com.hazelcast.core.HazelcastInstance; 008import com.hazelcast.core.IMap; 009import com.hazelcast.core.ITopic; 010import io.ebean.BackgroundExecutor; 011import io.ebean.cache.ServerCache; 012import io.ebean.cache.ServerCacheConfig; 013import io.ebean.cache.ServerCacheFactory; 014import io.ebean.cache.ServerCacheNotification; 015import io.ebean.cache.ServerCacheNotify; 016import io.ebean.config.ServerConfig; 017import io.ebeaninternal.server.cache.DefaultServerCacheConfig; 018import io.ebeaninternal.server.cache.DefaultServerQueryCache; 019import org.slf4j.Logger; 020import org.slf4j.LoggerFactory; 021 022import java.util.Arrays; 023import java.util.HashSet; 024import java.util.Properties; 025import java.util.Set; 026import java.util.concurrent.ConcurrentHashMap; 027 028/** 029 * Factory for creating the various caches. 030 */ 031public class HzCacheFactory implements ServerCacheFactory { 032 033 /** 034 * This explicitly uses the common "io.ebean.cache" namespace. 035 */ 036 private static final Logger logger = LoggerFactory.getLogger("io.ebean.cache.HzCacheFactory"); 037 038 private final ConcurrentHashMap<String, HzQueryCache> queryCaches; 039 040 private final HazelcastInstance instance; 041 042 /** 043 * Topic used to broadcast query cache invalidation. 044 */ 045 private final ITopic<String> queryCacheInvalidation; 046 047 /** 048 * Topic used to broadcast table modifications. 049 */ 050 private final ITopic<String> tableModNotify; 051 052 private final BackgroundExecutor executor; 053 054 private ServerCacheNotify listener; 055 056 public HzCacheFactory(ServerConfig serverConfig, BackgroundExecutor executor) { 057 058 this.executor = executor; 059 this.queryCaches = new ConcurrentHashMap<>(); 060 061 if (System.getProperty("hazelcast.logging.type") == null) { 062 System.setProperty("hazelcast.logging.type", "slf4j"); 063 } 064 065 Object hazelcastInstance = serverConfig.getServiceObject("hazelcast"); 066 if (hazelcastInstance != null) { 067 instance = (HazelcastInstance) hazelcastInstance; 068 } else { 069 instance = createInstance(serverConfig); 070 } 071 072 tableModNotify = instance.getReliableTopic("tableModNotify"); 073 tableModNotify.addMessageListener(message -> processTableNotify(message.getMessageObject())); 074 075 queryCacheInvalidation = instance.getReliableTopic("queryCacheInvalidation"); 076 queryCacheInvalidation.addMessageListener(message -> processInvalidation(message.getMessageObject())); 077 } 078 079 /** 080 * Create a new HazelcastInstance based on configuration from serverConfig. 081 */ 082 private HazelcastInstance createInstance(ServerConfig serverConfig) { 083 Object configuration = serverConfig.getServiceObject("hazelcastConfiguration"); 084 if (configuration != null) { 085 // explicit configuration probably set via DI 086 if (configuration instanceof ClientConfig) { 087 return HazelcastClient.newHazelcastClient((ClientConfig) configuration); 088 } else if (configuration instanceof Config) { 089 return Hazelcast.newHazelcastInstance((Config) configuration); 090 } else { 091 throw new IllegalArgumentException("Invalid Hazelcast configuration type " + configuration.getClass()); 092 } 093 } else { 094 // implicit configuration via hazelcast-client.xml or hazelcast.xml 095 if (isServerMode(serverConfig.getProperties())) { 096 return Hazelcast.newHazelcastInstance(); 097 } else { 098 return HazelcastClient.newHazelcastClient(); 099 } 100 } 101 } 102 103 /** 104 * Return true if hazelcast should be used in server mode. 105 */ 106 private boolean isServerMode(Properties properties) { 107 return properties != null && properties.getProperty("ebean.hazelcast.servermode", "").equals("true"); 108 } 109 110 @Override 111 public ServerCacheNotify createCacheNotify(ServerCacheNotify listener) { 112 this.listener = listener; 113 return new HzServerCacheNotify(tableModNotify); 114 } 115 116 @Override 117 public ServerCache createCache(ServerCacheConfig config) { 118 if (config.isQueryCache()) { 119 return createQueryCache(config); 120 } else { 121 return createNormalCache(config); 122 } 123 } 124 125 private ServerCache createNormalCache(ServerCacheConfig config) { 126 127 String fullName = config.getType().name() + "-" + config.getCacheKey(); 128 logger.debug("get cache [{}]", fullName); 129 IMap<Object, Object> map = instance.getMap(fullName); 130 return new HzCache(map, config.getTenantProvider()); 131 } 132 133 private ServerCache createQueryCache(ServerCacheConfig config) { 134 135 synchronized (this) { 136 HzQueryCache cache = queryCaches.get(config.getCacheKey()); 137 if (cache == null) { 138 logger.debug("create query cache [{}]", config.getCacheKey()); 139 cache = new HzQueryCache(new DefaultServerCacheConfig(config)); 140 cache.periodicTrim(executor); 141 queryCaches.put(config.getCacheKey(), cache); 142 } 143 return cache; 144 } 145 } 146 147 /** 148 * Extends normal default implementation with notification of clear() to cluster. 149 */ 150 private class HzQueryCache extends DefaultServerQueryCache { 151 152 HzQueryCache(DefaultServerCacheConfig cacheConfig) { 153 super(cacheConfig); 154 } 155 156 @Override 157 public void clear() { 158 super.clear(); 159 sendInvalidation(name); 160 } 161 162 /** 163 * Process the invalidation message coming from the cluster. 164 */ 165 private void invalidate() { 166 super.clear(); 167 } 168 } 169 170 /** 171 * Send the invalidation message to all members of the cluster. 172 */ 173 private void sendInvalidation(String key) { 174 queryCacheInvalidation.publish(key); 175 } 176 177 /** 178 * Process a remote query cache invalidation. 179 */ 180 private void processInvalidation(String cacheName) { 181 HzQueryCache cache = queryCaches.get(cacheName); 182 if (cache != null) { 183 cache.invalidate(); 184 } 185 } 186 187 /** 188 * Process a remote dependent table modify event. 189 */ 190 private void processTableNotify(String rawMessage) { 191 192 if (logger.isDebugEnabled()) { 193 logger.debug("processTableNotify {}", rawMessage); 194 } 195 196 String[] split = rawMessage.split(","); 197 long modTimestamp = Long.parseLong(split[0]); 198 199 Set<String> tables = new HashSet<>(); 200 tables.addAll(Arrays.asList(split).subList(1, split.length)); 201 202 listener.notify(new ServerCacheNotification(modTimestamp, tables)); 203 } 204 205}