001package org.avaje.ebean.ignite; 002 003import com.avaje.ebean.BackgroundExecutor; 004import com.avaje.ebean.cache.ServerCache; 005import com.avaje.ebean.cache.ServerCacheFactory; 006import com.avaje.ebean.cache.ServerCacheOptions; 007import com.avaje.ebean.cache.ServerCacheType; 008import com.avaje.ebean.config.ServerConfig; 009import com.avaje.ebeaninternal.server.cache.DefaultServerCache; 010import org.apache.ignite.Ignite; 011import org.apache.ignite.IgniteCache; 012import org.apache.ignite.IgniteMessaging; 013import org.apache.ignite.Ignition; 014import org.apache.ignite.configuration.IgniteConfiguration; 015import org.apache.ignite.lang.IgniteBiPredicate; 016import org.apache.ignite.logger.slf4j.Slf4jLogger; 017import org.avaje.ebean.ignite.config.ConfigManager; 018import org.avaje.ebean.ignite.config.ConfigPair; 019import org.avaje.ebean.ignite.config.ConfigXmlReader; 020import org.avaje.ebean.ignite.config.L2Configuration; 021import org.avaje.ignite.IgniteConfigBuilder; 022import org.slf4j.Logger; 023import org.slf4j.LoggerFactory; 024 025import java.io.File; 026import java.util.Properties; 027import java.util.UUID; 028import java.util.concurrent.ConcurrentHashMap; 029 030/** 031 * Factory for creating L2 server caches with Apache Ignite. 032 * <p> 033 * The L2 Query cache is effectively an always a near cache and we use Ignite to send/receive 034 * invalidation messages for the query caches on all the members of the cluster. 035 * </p> 036 * <p> 037 * All the 'Bean' caches will typically be partitioned with an optional 'near' cache option or replicated. 038 * Replicated caches ought to be a good choice for small cardinality/stable bean types (like countries, 039 * currencies etc). 040 * </p> 041 */ 042public class IgCacheFactory implements ServerCacheFactory { 043 044 private static final Logger queryLogger = LoggerFactory.getLogger("org.avaje.ebean.cache.QUERY"); 045 046 private static final Logger logger = LoggerFactory.getLogger("org.avaje.ebean.cache.CACHE"); 047 048 private static final String QC_INVALIDATE = "L2QueryCacheInvalidate"; 049 050 private final ConcurrentHashMap<String, IgQueryCache> queryCaches; 051 052 private final ConfigManager configManager; 053 054 private final BackgroundExecutor executor; 055 056 private Ignite ignite; 057 058 private IgniteMessaging messaging; 059 060 public IgCacheFactory(ServerConfig serverConfig, BackgroundExecutor executor) { 061 this.executor = executor; 062 this.queryCaches = new ConcurrentHashMap<>(); 063 this.configManager = new ConfigManager(readConfiguration()); 064 065 // programmatically set into ServerConfig - typical DI setup 066 IgniteConfiguration configuration = (IgniteConfiguration) serverConfig.getServiceObject("igniteConfiguration"); 067 if (configuration == null) { 068 Properties properties = serverConfig.getProperties(); 069 if (properties != null) { 070 configuration = new IgniteConfigBuilder("ignite", properties).build(); 071 } else { 072 configuration = new IgniteConfiguration(); 073 } 074 } 075 076 if (configuration.getGridLogger() == null) { 077 configuration.setGridLogger(new Slf4jLogger(logger)); 078 } 079 080 logger.debug("Starting Ignite"); 081 ignite = Ignition.start(configuration); 082 083 messaging = ignite.message(ignite.cluster().forRemotes()); 084 messaging.localListen(QC_INVALIDATE, new QueryCacheInvalidateListener()); 085 } 086 087 /** 088 * Read the L2 cache configuration. 089 */ 090 private L2Configuration readConfiguration() { 091 092 // check system property first 093 String config = System.getProperty("ebeanIgniteConfig"); 094 if (config != null) { 095 File file = new File(config); 096 if (!file.exists()) { 097 throw new IllegalStateException("ebean ignite configuration not found at " + config); 098 } 099 return ConfigXmlReader.read(file); 100 } 101 102 // look for local configuration external to the application 103 File file = new File("ebean-ignite-config.xml"); 104 if (file.exists()) { 105 return ConfigXmlReader.read(file); 106 } 107 108 // look for configuration inside the application 109 return ConfigXmlReader.read("/ebean-ignite-config.xml"); 110 } 111 112 private class QueryCacheInvalidateListener implements IgniteBiPredicate<UUID, String> { 113 @Override 114 public boolean apply(UUID uuid, String key) { 115 queryCacheInvalidate(key); 116 return true; 117 } 118 } 119 120 @Override 121 public ServerCache createCache(ServerCacheType type, String key, ServerCacheOptions options) { 122 123 logger.debug("create cache - type:{} key:{}", type, key); 124 switch (type) { 125 case QUERY: 126 return createQueryCache(key, options); 127 128 default: 129 return createNormalCache(type, key); 130 } 131 } 132 133 private ServerCache createNormalCache(ServerCacheType type, String key) { 134 135 ConfigPair pair = configManager.getConfig(type, key); 136 137 pair.setName(fullName(type, key)); 138 139 IgniteCache cache; 140 if (pair.hasNearCache()) { 141 cache = ignite.getOrCreateCache(pair.getMain(), pair.getNear()); 142 143 } else { 144 cache = ignite.getOrCreateCache(pair.getMain()); 145 } 146 147 return new IgCache(cache); 148 } 149 150 /** 151 * Return the full cache name (JMX safe name). 152 */ 153 private String fullName(ServerCacheType type, String key) { 154 return type.name() + "-" + key; 155 } 156 157 /** 158 * Create a local/near query cache. 159 */ 160 private ServerCache createQueryCache(String key, ServerCacheOptions options) { 161 synchronized (this) { 162 IgQueryCache cache = queryCaches.get(key); 163 if (cache == null) { 164 cache = new IgQueryCache(key, options); 165 cache.periodicTrim(executor); 166 queryCaches.put(key, cache); 167 } 168 return cache; 169 } 170 } 171 172 /** 173 * Local only cache implementation with no serialisation requirements. 174 * <p> 175 * Uses Ignite topic to invalidate across the cluster. 176 * </p> 177 */ 178 private class IgQueryCache extends DefaultServerCache { 179 180 IgQueryCache(String name, ServerCacheOptions options) { 181 super(name, options); 182 } 183 184 @Override 185 public void clear() { 186 super.clear(); 187 sendQueryCacheInvalidation(name); 188 } 189 190 /** 191 * Process the invalidation message coming from the cluster. 192 */ 193 private void invalidate() { 194 queryLogger.debug(" CLEAR {}(*) - cluster invalidate", name); 195 super.clear(); 196 } 197 } 198 199 /** 200 * Send the invalidation message to all members of the cluster. 201 */ 202 private void sendQueryCacheInvalidation(String key) { 203 messaging.send(QC_INVALIDATE, key); 204 } 205 206 /** 207 * Clear the query cache if we have it. 208 */ 209 private void queryCacheInvalidate(String key) { 210 IgQueryCache queryCache = queryCaches.get(key); 211 if (queryCache != null) { 212 queryCache.invalidate(); 213 } 214 } 215 216}