All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.turbospaces.ebean.JGroupsCacheManager Maven / Gradle / Ivy

There is a newer version: 2.0.33
Show newest version
package com.turbospaces.ebean;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.jgroups.blocks.MethodLookup;
import org.jgroups.blocks.RpcDispatcher;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;

import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.cache.Cache;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.netflix.archaius.api.Config;
import com.turbospaces.cache.BlockhoundCacheWrapper;
import com.turbospaces.cfg.ApplicationProperties;
import com.turbospaces.common.PlatformUtil;

import io.ebean.BackgroundExecutor;
import io.ebean.DatabaseBuilder;
import io.ebean.cache.QueryCacheEntryValidate;
import io.ebean.cache.ServerCache;
import io.ebean.cache.ServerCacheConfig;
import io.ebean.cache.ServerCacheFactory;
import io.ebean.cache.ServerCacheNotification;
import io.ebean.cache.ServerCacheNotify;
import io.ebean.cache.ServerCacheOptions;
import io.ebean.cache.ServerCacheStatistics;
import io.ebean.cache.ServerCacheType;
import io.ebean.config.CurrentTenantProvider;
import io.ebeaninternal.server.cache.DefaultServerCacheConfig;
import io.ebeaninternal.server.cache.DefaultServerQueryCache;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Tags;
import io.micrometer.core.instrument.binder.cache.GuavaCacheMetrics;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class JGroupsCacheManager implements MethodLookup, CacheManager, ServerCacheNotify, InitializingBean, DisposableBean, BeanNameAware {
    public static short METHOD_ON_CACHE_PUT = 1;
    public static short METHOD_ON_CHANGE_REMOVE = 2;
    public static short METHOD_ON_CACHE_CLEAR = 3;
    public static short METHOD_ON_MODIFIED = 4;
    public static short METHOD_ON_CACHE_CLEAR_ALL = 5;
    public static ImmutableMap METHODS;
    static {
        try {
            ImmutableMap.Builder b = ImmutableMap.builder();
            b.put(METHOD_ON_MODIFIED, JGroupsCacheManager.class.getMethod("onTablesModify", String.class));
            b.put(METHOD_ON_CACHE_PUT, JGroupsCacheManager.class.getMethod("onCachePut", String.class, byte[].class, byte[].class));
            b.put(METHOD_ON_CHANGE_REMOVE, JGroupsCacheManager.class.getMethod("onCacheRemove", String.class, byte[].class));
            b.put(METHOD_ON_CACHE_CLEAR, JGroupsCacheManager.class.getMethod("onCacheClear", String.class));
            b.put(METHOD_ON_CACHE_CLEAR_ALL, JGroupsCacheManager.class.getMethod("onCacheClearAll", boolean.class));

            METHODS = b.build();
        } catch (NoSuchMethodException err) {
            throw new Error(err);
        }
    }

    private final ApplicationProperties props;
    private final MeterRegistry meterRegistry;
    private final JGroupsBroadcastChannel broadcast;
    private final ConcurrentMap caches;
    private final ScheduledExecutorService timer;
    private String beanName;
    private BackgroundExecutor executor;
    private ServerCacheNotify notify;

    public JGroupsCacheManager(ApplicationProperties props, MeterRegistry meterRegistry, RpcDispatcher dispatcher) {
        this.props = Objects.requireNonNull(props);
        this.meterRegistry = Objects.requireNonNull(meterRegistry);
        this.broadcast = new JGroupsBroadcastChannel(props, meterRegistry, dispatcher);
        this.caches = Maps.newConcurrentMap();

        //
        // ~ just one thread
        //
        this.timer = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r);
                t.setDaemon(true);
                if (StringUtils.isNotEmpty(beanName)) {
                    t.setName(beanName);
                }
                return t;
            }
        });
    }
    @Override
    public void setBeanName(String name) {
        this.beanName = Objects.requireNonNull(name);
    }
    @Override
    public void afterPropertiesSet() throws Exception {
        broadcast.afterPropertiesSet();

        long intervalMillis = props.CACHE_METRICS_REPORT_INTERVAL.get().toMillis();
        timer.scheduleWithFixedDelay(() -> caches.forEach((key, value) -> {
            if (key.endsWith(ServerCacheType.QUERY.code())) {
                ServerCacheStatistics stats = value.statistics(true);
                meterRegistry.counter("query_cache.hits", "name", key).increment(stats.getHitCount());
                meterRegistry.counter("query_cache.puts", "name", key).increment(stats.getPutCount());
                meterRegistry.counter("query_cache.evicts", "name", key).increment(stats.getEvictCount());
                meterRegistry.gauge("query_cache.size", Tags.of("name", key), stats.getSize());
                meterRegistry.gauge("query_cache.maxSize", Tags.of("name", key), stats.getMaxSize());
                meterRegistry.gauge("query_cache.hit_ratio", Tags.of("name", key), stats.getHitRatio());
                meterRegistry.counter("query_cache.miss_count", "name", key).increment(stats.getMissCount());
                meterRegistry.counter("query_cache.ttl_count", "name", key).increment(stats.getTtlCount());
                meterRegistry.counter("query_cache.remove_count", "name", key).increment(stats.getRemoveCount());
                meterRegistry.counter("query_cache.clear_count", "name", key).increment(stats.getClearCount());
                meterRegistry.counter("query_cache.idle_count", "name", key).increment(stats.getIdleCount());
            }
        }), intervalMillis, intervalMillis, TimeUnit.MILLISECONDS);

        //
        // ~ usage w/ JPA framework in stand alone mode
        //
        if (Objects.isNull(executor)) {
            setBackgroundExecutor(new BackgroundExecutor() {
                @Override
                public Future submit(Runnable task) {
                    return timer.submit(task);
                }
                @Override
                public  Future submit(Callable task) {
                    return timer.submit(task);
                }
                @Override
                public ScheduledFuture scheduleWithFixedDelay(Runnable task, long initialDelay, long delay, TimeUnit unit) {
                    return timer.scheduleWithFixedDelay(task, initialDelay, delay, unit);
                }
                @Override
                public ScheduledFuture scheduleAtFixedRate(Runnable task, long initialDelay, long period, TimeUnit unit) {
                    return timer.scheduleAtFixedRate(task, initialDelay, period, unit);
                }
                @Override
                public  ScheduledFuture schedule(Callable task, long delay, TimeUnit unit) {
                    return timer.schedule(task, delay, unit);
                }
                @Override
                public ScheduledFuture schedule(Runnable task, long delay, TimeUnit unit) {
                    return timer.schedule(task, delay, unit);
                }
                @Override
                public void execute(Runnable task) {
                    timer.execute(task);
                }
            });
        }
    }
    @Override
    public void destroy() throws Exception {
        try {
            broadcast.destroy();
        } finally {
            PlatformUtil.shutdownExecutor(timer, props.APP_PLATFORM_GRACEFUL_SHUTDOWN_TIMEOUT.get());
        }
    }
    @Override
    public Method findMethod(short id) {
        return METHODS.get(id);
    }
    @Override
    public void setBackgroundExecutor(BackgroundExecutor executor) {
        this.executor = Objects.requireNonNull(executor);
    }
    @Override
    public ServerCacheFactory create(DatabaseBuilder databaseBuilder, BackgroundExecutor backgroundExecutor) {
        setBackgroundExecutor(backgroundExecutor);
        return this;
    }
    @Override
    public ServerCacheNotify createCacheNotify(ServerCacheNotify cacheNotify) {
        this.notify = Objects.requireNonNull(cacheNotify);
        return this;
    }
    @Override
    public void notify(ServerCacheNotification notification) {
        if (CollectionUtils.isNotEmpty(notification.getDependentTables())) {
            Set dependentTables = notification.getDependentTables();
            String line = Joiner.on(',').join(dependentTables);
            log.debug("Publish TableMods - {}", line);
            broadcast.broadcastAsync(notification, line);
        }
    }
    @Override
    public void onTablesModify(String line) {
        Iterable it = Splitter.on(',').omitEmptyStrings().split(line);
        ImmutableSet tables = ImmutableSet.copyOf(it);
        ServerCacheNotification notification = new ServerCacheNotification(tables);
        if (Objects.nonNull(notify)) {
            notify.notify(notification);
        }
    }
    @Override
    public void onCachePut(String cacheKey, byte[] id, byte[] value) throws Throwable {
        AbstractEbeanCache cache = (AbstractEbeanCache) caches.get(cacheKey);
        if (Objects.nonNull(cache)) {
            cache.onPut(id, value);
        }
    }
    @Override
    public void onCacheRemove(String cacheKey, byte[] id) throws Throwable {
        AbstractEbeanCache cache = (AbstractEbeanCache) caches.get(cacheKey);
        if (Objects.nonNull(cache)) {
            cache.onRemove(id);
        }
    }
    @Override
    public void onCacheClear(String cacheKey) throws Throwable {
        ServerCache cache = caches.get(cacheKey);
        if (Objects.nonNull(cache)) {
            if (cache instanceof ClearableCache) {
                ((ClearableCache) cache).onClear();
            }
        }
    }
    @Override
    public void onCacheClearAll(boolean preserveSimple) throws Throwable {
        onCacheClearAll();
    }
    @Override
    public void onCacheClearAll() throws Throwable {
        for (ServerCache cache : caches.values()) {
            if (cache instanceof ClearableCache) {
                ((ClearableCache) cache).onClear();
            }
        }
    }
    @Override
    public ServerCache getCache(String cacheKey) {
        return caches.get(cacheKey);
    }
    @Override
    public void clearAll() {
        try {
            onCacheClearAll();
        } catch (Throwable err) {
            log.error(err.getMessage(), err);
        } finally {
            broadcast.broadcastClearAllAsync();
        }
    }
    @Override
    public ServerCache createCache(ServerCacheConfig config) {
        String cacheKey = config.getCacheKey();
        String shortName = config.getShortName();

        ServerCacheOptions cacheOptions = config.getCacheOptions();
        ServerCacheType cacheType = config.getType();
        CurrentTenantProvider tenantProvider = config.getTenantProvider();
        QueryCacheEntryValidate queryCacheValidate = config.getQueryCacheEntryValidate();

        ServerCache cache = caches.get(cacheKey);
        if (Objects.isNull(cache)) {
            Config prefixedView = props.cfg().getPrefixedView(cacheKey);
            Map configMap = new HashMap<>();
            for (String key : prefixedView.keys()) {
                Object rawProperty = prefixedView.getRawProperty(key);
                configMap.put(key, rawProperty);
            }

            //
            // ~ optional cache settings
            //
            int maxTtl = prefixedView.getInteger(EbeanCacheConfigurer.MAX_TTL, cacheOptions.getMaxSecsToLive());
            int maxIdle = prefixedView.getInteger(EbeanCacheConfigurer.MAX_IDLE, cacheOptions.getMaxIdleSecs());
            int maxSize = prefixedView.getInteger(EbeanCacheConfigurer.MAX_SIZE, cacheOptions.getMaxSize());

            int trimFrequency = (int) props.APP_TIMER_INTERVAL.get().toSeconds();

            ServerCacheOptions options = new ServerCacheOptions();
            options.setMaxSecsToLive(maxTtl);
            options.setMaxIdleSecs(maxIdle);
            options.setMaxSize(maxSize);
            options.setTrimFrequency(trimFrequency);

            ServerCacheConfig scc = new ServerCacheConfig(
                    cacheType,
                    cacheKey,
                    shortName,
                    options,
                    tenantProvider,
                    queryCacheValidate //
            );
            List tags = Lists.newArrayList(
                    Tag.of("cacheType", cacheType.name().toLowerCase()),
                    Tag.of("shortName", shortName)//
            );

            if (config.isQueryCache()) {
                cache = new JgroupsServerQueryCache(props, new DefaultServerCacheConfig(scc), broadcast);
                ServerCache prev = caches.putIfAbsent(cacheKey, cache);
                if (Objects.nonNull(prev)) {
                    cache = prev;
                } else {
                    log.debug("created query cache: {} using cfg {}",
                            cacheKey,
                            ToStringBuilder.reflectionToString(options, ToStringStyle.SHORT_PREFIX_STYLE));
                }
            } else {
                //
                // ~ fail fast in development mode and raise sentry error in production mode otherwise
                //
                if (BooleanUtils.isFalse(prefixedView.containsKey(EbeanCacheConfigurer.CACHE_MODE_LOCAL))) {
                    if (props.APP_DEV_MODE.get()) {
                        throw new IllegalStateException("cache %s is not configured".formatted(cacheKey));
                    }
                    log.error("cache {} is not configured", cacheKey);
                }

                boolean localMode = prefixedView.getBoolean(EbeanCacheConfigurer.CACHE_MODE_LOCAL, false);

                ToStringStyle nameStyle = ToStringStyle.NO_CLASS_NAME_STYLE;
                Cache holder = LocalCache.cache(cacheKey, new DefaultServerCacheConfig(scc));
                BlockhoundCacheWrapper cacheWrapper = new BlockhoundCacheWrapper<>(holder);

                if (localMode) {
                    cache = new LocalEbeanCache(props, cacheKey, cacheWrapper, tenantProvider, scc, broadcast);
                    ServerCache prev = caches.putIfAbsent(cacheKey, cache);
                    if (Objects.nonNull(prev)) {
                        cache = prev;
                    } else {
                        new GuavaCacheMetrics<>(holder, cacheKey, tags).bindTo(meterRegistry);
                        log.debug("created local cache: {} using cfg {}", cacheKey, ToStringBuilder.reflectionToString(options, nameStyle));
                    }
                } else {
                    cache = new ReplicatedEbeanCache(props, cacheKey, cacheWrapper, tenantProvider, scc, broadcast);
                    ServerCache prev = caches.putIfAbsent(cacheKey, cache);
                    if (Objects.nonNull(prev)) {
                        cache = prev;
                    } else {
                        new GuavaCacheMetrics<>(holder, cacheKey, tags).bindTo(meterRegistry);
                        log.debug("created replicated cache: {} using cfg {}", cacheKey, ToStringBuilder.reflectionToString(options, nameStyle));
                    }
                }
            }

            //
            // ~ cleanUp
            //
            ServerCache tmp = Objects.requireNonNull(cache);
            executor.scheduleWithFixedDelay(new Runnable() {
                @Override
                public void run() {
                    if (tmp instanceof LocalEbeanCache) {
                        ((LocalEbeanCache) tmp).cleanUp();
                    } else if (tmp instanceof ReplicatedCache) {
                        ((ReplicatedCache) tmp).cleanUp();
                    } else if (tmp instanceof DefaultServerQueryCache) {
                        ((DefaultServerQueryCache) tmp).runEviction();
                    }
                }
            }, trimFrequency, trimFrequency, TimeUnit.SECONDS);
        }

        return cache;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy