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}