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

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

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

import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.time.Duration;
import java.util.Collection;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;

import org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream;
import org.apache.commons.lang3.SerializationUtils;
import org.apache.commons.lang3.time.StopWatch;
import org.jgroups.blocks.MethodCall;
import org.jgroups.blocks.RequestOptions;
import org.jgroups.blocks.RpcDispatcher;
import org.springframework.beans.factory.DisposableBean;

import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.lmax.disruptor.EventFactory;
import com.lmax.disruptor.EventTranslator;
import com.lmax.disruptor.WorkHandler;
import com.lmax.disruptor.dsl.Disruptor;
import com.turbospaces.cfg.ApplicationProperties;
import com.turbospaces.common.PlatformUtil;

import io.ebean.cache.ServerCacheNotification;
import io.ebean.cache.ServerCacheType;
import io.ebeaninternal.server.cache.CachedBeanData;
import io.ebeaninternal.server.cache.CachedManyIds;
import io.micrometer.core.instrument.MeterRegistry;
import io.vavr.CheckedRunnable;
import io.vavr.concurrent.Promise;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import reactor.blockhound.integration.DefaultBlockHoundIntegration;

@Slf4j
public class JGroupsBroadcastChannel implements BroadcastChannel, DisposableBean {
    private static final Duration TOO_LONG_WARN = Duration.ofSeconds(1);

    private final Disruptor disruptor;

    @SuppressWarnings("unchecked")
    public JGroupsBroadcastChannel(ApplicationProperties props, MeterRegistry meterRegistry, RpcDispatcher dispatcher) {
        //
        // ~ parallel workers (more thread just because it is it casts down to network stack )
        //
        int count = props.CACHE_BROADCAST_WORKERS.get();
        int ringBufferSize = props.CACHE_RING_BUFFER_SIZE.get();
        WorkHandler[] workers = new WorkHandler[count];
        for (int i = 0; i < workers.length; i++) {
            workers[i] = new WorkHandler() {
                @Override
                public void onEvent(MethodCallEvent event) throws Exception {
                    StopWatch outer = event.stopWatch;

                    //
                    // ~ broadcast over network
                    //
                    long now = System.currentTimeMillis();
                    if (dispatcher.getChannel().isOpen()) {
                        dispatcher.callRemoteMethods(null, event.call, RequestOptions.ASYNC());
                    }

                    outer.stop();
                    long time = outer.getTime(TimeUnit.MILLISECONDS);
                    if (time > TOO_LONG_WARN.toMillis()) {
                        log.error("{} took too long: {} own: {}ms", event.operation, outer, System.currentTimeMillis() - now);
                    }

                    event.promise.trySuccess(outer);
                }
            };
        }

        ThreadFactoryBuilder factory = new ThreadFactoryBuilder();
        factory.setDaemon(false);
        factory.setNameFormat("jgroups-rpc-dispatcher-%d");

        disruptor = new Disruptor<>(MethodCallEvent.FACTORY, ringBufferSize, factory.build());
        disruptor.handleEventsWithWorkerPool(workers);
        disruptor.start();
    }
    @Override
    public void destroy() throws Exception {
        disruptor.shutdown();
    }
    @Override
    public Promise broadcastPutAsync(String cacheKey, ServerCacheType type, String key, Object obj) throws Throwable {
        UnsynchronizedByteArrayOutputStream out = UnsynchronizedByteArrayOutputStream.builder().get();
        //
        // ~ based on cache type
        //
        byte[] data = switch (type) {
            case NATURAL_KEY -> SerializationUtils.serialize(Serializable.class.cast(obj));
            case BEAN -> PlatformUtil.serialize(out, CachedBeanData.class.cast(obj));
            case COLLECTION_IDS -> PlatformUtil.serialize(out, CachedManyIds.class.cast(obj));
            default -> throw new IllegalArgumentException("Unexpected cache type: " + type);
        };

        //
        // ~ dispatch event to ring buffer
        //
        Promise promise = Promise.make(MoreExecutors.directExecutor());
        DefaultBlockHoundIntegration.allowBlockingUnchecked(new CheckedRunnable() {
            @Override
            public void run() throws Throwable {
                disruptor.publishEvent(new EventTranslator() {
                    @Override
                    public void translateTo(MethodCallEvent event, long sequence) {
                        event.call = new MethodCall(JGroupsCacheManager.METHOD_ON_CACHE_PUT, cacheKey, SerializationUtils.serialize(key), data);
                        event.stopWatch = StopWatch.createStarted();
                        event.operation = "put(%s)".formatted(cacheKey);
                        event.promise = promise;
                    }
                });
            }
        });
        return promise;
    }
    @Override
    public Promise broadcastPutAllAsync(String cacheKey, ServerCacheType type, Map map) throws Throwable {
        //
        // ~ fast output stream
        //
        UnsynchronizedByteArrayOutputStream stream = UnsynchronizedByteArrayOutputStream.builder().get();
        try (ObjectOutputStream out = new ObjectOutputStream(stream)) {
            out.writeInt(map.size());
            for (Entry entry : map.entrySet()) {
                out.writeUTF(entry.getKey());
                switch (type) {
                    case NATURAL_KEY: {
                        out.writeObject(entry.getValue());
                        break;
                    }
                    case BEAN: {
                        CachedBeanData.class.cast(entry.getValue()).writeExternal(out);
                        break;
                    }
                    case COLLECTION_IDS: {
                        CachedManyIds.class.cast(entry.getValue()).writeExternal(out);
                        break;
                    }
                    default: {
                        throw new IllegalArgumentException("Unexpected cache type: " + type);
                    }
                }
            }
            out.flush();
        }

        //
        // ~ dispatch event to ring buffer
        //
        Promise promise = Promise.make(MoreExecutors.directExecutor());
        DefaultBlockHoundIntegration.allowBlockingUnchecked(new CheckedRunnable() {
            @Override
            public void run() throws Throwable {
                disruptor.publishEvent(new EventTranslator() {
                    @Override
                    public void translateTo(MethodCallEvent event, long sequence) {
                        event.call = new MethodCall(JGroupsCacheManager.METHOD_ON_CHANGE_PUT_ALL, cacheKey, stream.toByteArray());
                        event.stopWatch = StopWatch.createStarted();
                        event.operation = "putAll(%s)".formatted(cacheKey);
                        event.promise = promise;
                    }
                });
            }
        });
        return promise;
    }
    @Override
    public Promise broadcastRemoveAsync(String cacheKey, String key) throws Throwable {
        //
        // ~ dispatch event to ring buffer
        //
        Promise promise = Promise.make(MoreExecutors.directExecutor());
        DefaultBlockHoundIntegration.allowBlockingUnchecked(new CheckedRunnable() {
            @Override
            public void run() throws Throwable {
                disruptor.publishEvent(new EventTranslator() {
                    @Override
                    public void translateTo(MethodCallEvent event, long sequence) {
                        event.call = new MethodCall(JGroupsCacheManager.METHOD_ON_CHANGE_REMOVE, cacheKey, SerializationUtils.serialize(key));
                        event.stopWatch = StopWatch.createStarted();
                        event.operation = "remove(%s)".formatted(cacheKey);
                        event.promise = promise;
                    }
                });
            }
        });
        return promise;
    }
    @Override
    public Promise broadcastRemoveAllAsync(String cacheKey, Collection keys) throws Throwable {
        //
        // ~ fast output stream
        //
        UnsynchronizedByteArrayOutputStream stream = UnsynchronizedByteArrayOutputStream.builder().get();
        try (ObjectOutputStream out = new ObjectOutputStream(stream)) {
            out.writeInt(keys.size());
            for (String it : keys) {
                out.writeUTF(it);
            }
            out.flush();
        }

        //
        // ~ dispatch event to ring buffer
        //
        Promise promise = Promise.make(MoreExecutors.directExecutor());
        DefaultBlockHoundIntegration.allowBlockingUnchecked(new CheckedRunnable() {
            @Override
            public void run() throws Throwable {
                disruptor.publishEvent(new EventTranslator() {
                    @Override
                    public void translateTo(MethodCallEvent event, long sequence) {
                        event.call = new MethodCall(JGroupsCacheManager.METHOD_ON_CHANGE_REMOVE_ALL, cacheKey, stream.toByteArray());
                        event.stopWatch = StopWatch.createStarted();
                        event.operation = "removeAll(%s)".formatted(cacheKey);
                        event.promise = promise;
                    }
                });
            }
        });
        return promise;
    }
    @Override
    public Promise broadcastClearAllAsync(String cacheKey) {
        //
        // ~ dispatch event to ring buffer
        //
        Promise promise = Promise.make(MoreExecutors.directExecutor());
        DefaultBlockHoundIntegration.allowBlocking(new Runnable() {
            @Override
            public void run() {
                disruptor.publishEvent(new EventTranslator() {
                    @Override
                    public void translateTo(MethodCallEvent event, long sequence) {
                        event.call = new MethodCall(JGroupsCacheManager.METHOD_ON_CACHE_CLEAR, cacheKey);
                        event.stopWatch = StopWatch.createStarted();
                        event.operation = "clear(%s)".formatted(cacheKey);
                        event.promise = promise;
                    }
                });
            }
        });
        return promise;
    }
    @Override
    public Promise broadcastClearAllAsync() {
        //
        // ~ dispatch event to ring buffer
        //
        Promise promise = Promise.make(MoreExecutors.directExecutor());
        DefaultBlockHoundIntegration.allowBlocking(new Runnable() {
            @Override
            public void run() {
                disruptor.publishEvent(new EventTranslator() {
                    @Override
                    public void translateTo(MethodCallEvent event, long sequence) {
                        event.call = new MethodCall(JGroupsCacheManager.METHOD_ON_CACHE_CLEAR_ALL, true);
                        event.stopWatch = StopWatch.createStarted();
                        event.operation = "clearAll";
                        event.promise = promise;
                    }
                });
            }
        });
        return promise;
    }
    @Override
    public Promise broadcastAsync(ServerCacheNotification notification, String data) {
        //
        // ~ dispatch event to ring buffer
        //
        Promise promise = Promise.make(MoreExecutors.directExecutor());
        DefaultBlockHoundIntegration.allowBlocking(new Runnable() {
            @Override
            public void run() {
                disruptor.publishEvent(new EventTranslator() {
                    @Override
                    public void translateTo(MethodCallEvent event, long sequence) {
                        event.call = new MethodCall(JGroupsCacheManager.METHOD_ON_MODIFIED, data);
                        event.stopWatch = StopWatch.createStarted();
                        event.operation = "notify(%s)".formatted(data);
                        event.promise = promise;
                    }
                });
            }
        });
        return promise;
    }

    @NoArgsConstructor(access = AccessLevel.PRIVATE)
    private static class MethodCallEvent {
        public static final EventFactory FACTORY = new EventFactory() {
            @Override
            public MethodCallEvent newInstance() {
                return new MethodCallEvent();
            }
        };

        public Promise promise;
        public StopWatch stopWatch;
        public MethodCall call;
        public String operation;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy