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}