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

com.github.lontime.shaded.org.redisson.RedissonReliableTopic Maven / Gradle / Ivy

The newest version!
/**
 * Copyright (c) 2013-2021 Nikita Koksharov
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.lontime.shaded.org.redisson;

import io.netty.buffer.ByteBufUtil;
import io.netty.util.Timeout;
import com.github.lontime.shaded.org.redisson.api.RFuture;
import com.github.lontime.shaded.org.redisson.api.RReliableTopic;
import com.github.lontime.shaded.org.redisson.api.StreamMessageId;
import com.github.lontime.shaded.org.redisson.api.listener.MessageListener;
import com.github.lontime.shaded.org.redisson.client.codec.Codec;
import com.github.lontime.shaded.org.redisson.client.codec.StringCodec;
import com.github.lontime.shaded.org.redisson.client.protocol.RedisCommands;
import com.github.lontime.shaded.org.redisson.codec.CompositeCodec;
import com.github.lontime.shaded.org.redisson.command.CommandAsyncExecutor;
import com.github.lontime.shaded.org.redisson.misc.CompletableFutureWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

/**
 *
 * @author Nikita Koksharov
 *
 */
public class RedissonReliableTopic extends RedissonExpirable implements RReliableTopic {

    private static final Logger log = LoggerFactory.getLogger(RedissonReliableTopic.class);

    private static class Entry {

        private final Class type;
        private final MessageListener listener;

        Entry(Class type, MessageListener listener) {
            this.type = type;
            this.listener = listener;
        }

        public Class getType() {
            return type;
        }

        public MessageListener getListener() {
            return listener;
        }
    }

    private final Map listeners = new ConcurrentHashMap<>();
    private final AtomicReference subscriberId = new AtomicReference<>();
    private volatile RFuture>> readFuture;
    private volatile Timeout timeoutTask;

    public RedissonReliableTopic(Codec codec, CommandAsyncExecutor commandExecutor, String name) {
        super(codec, commandExecutor, name);
    }

    public RedissonReliableTopic(CommandAsyncExecutor commandExecutor, String name) {
        super(commandExecutor, name);
    }

    private String getSubscribersName() {
        return suffixName(getRawName(), "subscribers");
    }

    private String getMapName() {
        return suffixName(getRawName(), "map");
    }

    private String getCounter() {
        return suffixName(getRawName(), "counter");
    }

    private String getTimeout() {
        return suffixName(getRawName(), "timeout");
    }

    @Override
    public long publish(Object message) {
        return get(publishAsync(message));
    }

    @Override
    public  String addListener(Class type, MessageListener listener) {
        return get(addListenerAsync(type, listener));
    }

    @Override
    public void removeListener(String... listenerIds) {
        get(removeListenerAsync(listenerIds));
    }

    @Override
    public void removeAllListeners() {
        get(removeAllListenersAsync());
    }

    public RFuture removeAllListenersAsync() {
        listeners.clear();
        return removeSubscriber();
    }

    @Override
    public long size() {
        return get(sizeAsync());
    }

    public RFuture sizeAsync() {
        return commandExecutor.readAsync(getRawName(), StringCodec.INSTANCE, RedisCommands.XLEN, getRawName());
    }

    @Override
    public int countListeners() {
        return listeners.size();
    }

    @Override
    public RFuture publishAsync(Object message) {
        return commandExecutor.evalWriteAsync(getRawName(), StringCodec.INSTANCE, RedisCommands.EVAL_LONG,
                "redis.call('xadd', KEYS[1], '*', 'm', ARGV[1]); "
                        + "return redis.call('zcard', KEYS[2]); ",
                Arrays.asList(getRawName(), getSubscribersName()), encode(message));
    }

    protected String generateId() {
        byte[] id = new byte[16];
        ThreadLocalRandom.current().nextBytes(id);
        return ByteBufUtil.hexDump(id);
    }

    @Override
    public  RFuture addListenerAsync(Class type, MessageListener listener) {
        String id = generateId();
        listeners.put(id, new Entry(type, listener));

        if (subscriberId.get() != null) {
            return new CompletableFutureWrapper<>(id);
        }

        if (subscriberId.compareAndSet(null, id)) {
            renewExpiration();

            StreamMessageId startId = StreamMessageId.ALL;

            RFuture addFuture = commandExecutor.evalWriteNoRetryAsync(getRawName(), StringCodec.INSTANCE, RedisCommands.EVAL_VOID,
                    "local value = redis.call('incr', KEYS[3]); "
                            + "redis.call('zadd', KEYS[4], ARGV[3], ARGV[2]); "
                            + "redis.call('zadd', KEYS[1], value, ARGV[2]); "
                            + "redis.call('hset', KEYS[2], ARGV[2], ARGV[1]); ",
                    Arrays.asList(getSubscribersName(), getMapName(), getCounter(), getTimeout()),
                    startId, id, System.currentTimeMillis() + commandExecutor.getConnectionManager().getCfg().getReliableTopicWatchdogTimeout());
            CompletionStage f = addFuture.thenApply(r -> {
                poll(id, startId);
                return id;
            });

            return new CompletableFutureWrapper<>(f);
        }

        return new CompletableFutureWrapper<>(id);
    }

    private void poll(String id, StreamMessageId startId) {
        readFuture = commandExecutor.readAsync(getRawName(), new CompositeCodec(StringCodec.INSTANCE, codec),
                RedisCommands.XREAD_BLOCKING_SINGLE, "BLOCK", 0, "STREAMS", getRawName(), startId);
        readFuture.whenComplete((res, ex) -> {
            if (readFuture.isCancelled()) {
                return;
            }
            if (ex != null) {
                if (ex instanceof RedissonShutdownException) {
                    return;
                }

                log.error(ex.getMessage(), ex);

                commandExecutor.getConnectionManager().newTimeout(task -> {
                    poll(id, startId);
                }, 1, TimeUnit.SECONDS);
                return;
            }

            commandExecutor.getConnectionManager().getExecutor().execute(() -> {
                res.values().forEach(entry -> {
                    Object m = entry.get("m");
                    listeners.values().forEach(e -> {
                        if (e.getType().isInstance(m)) {
                            ((MessageListener) e.getListener()).onMessage(getRawName(), m);
                        }
                    });
                });
            });

            if (listeners.isEmpty()) {
                return;
            }

            StreamMessageId lastId = res.keySet().stream().skip(res.size() - 1).findFirst().get();
            long time = System.currentTimeMillis();
            RFuture updateFuture = commandExecutor.evalWriteAsync(getRawName(), StringCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                        "local r = redis.call('zscore', KEYS[2], ARGV[2]); "
                            + "if r ~= false then "
                                + "local value = redis.call('incr', KEYS[4]); "
                                + "redis.call('zadd', KEYS[2], value, ARGV[2]); "
                                + "redis.call('hset', KEYS[3], ARGV[2], ARGV[1]); "
                            + "end; "

                            + "local t = redis.call('zrange', KEYS[5], 0, 0, 'WITHSCORES'); "
                            + "if #t == 2 and tonumber(t[2]) < tonumber(ARGV[3]) then "
                                + "redis.call('hdel', KEYS[3], t[1]); "
                                + "redis.call('zrem', KEYS[2], t[1]); "
                                + "redis.call('zrem', KEYS[5], t[1]); "
                            + "end; "

                            + "local v = redis.call('zrange', KEYS[2], 0, 0); "
                            + "local score = redis.call('hget', KEYS[3], v[1]); "
                            + "local range = redis.call('xrange', KEYS[1], score, '+'); "
                            + "if #range == 0 then "
                                + "redis.call('del', KEYS[1]); "
                            + "elseif #range == 1 and range[1][1] == score then "
                                + "redis.call('del', KEYS[1]); "
                            + "else "
                                + "redis.call('xtrim', KEYS[1], 'maxlen', #range); "
                            + "end;"
                            + "return r ~= false; ",
                    Arrays.asList(getRawName(), getSubscribersName(), getMapName(), getCounter(), getTimeout()),
                    lastId, id, time);
            updateFuture.whenComplete((re, exc) -> {
                if (exc != null) {
                    if (exc instanceof RedissonShutdownException) {
                        return;
                    }
                    log.error("Unable to update subscriber status", exc);
                    return;
                }

                if (!re || listeners.isEmpty()) {
                    return;
                }

                poll(id, lastId);
            });

        });
    }

    @Override
    public RFuture deleteAsync() {
        return deleteAsync(getRawName(), getSubscribersName(), getMapName(), getCounter(), getTimeout());
    }

    @Override
    public RFuture sizeInMemoryAsync() {
        return super.sizeInMemoryAsync(Arrays.asList(getRawName(), getSubscribersName(), getMapName(), getCounter(), getTimeout()));
    }

    @Override
    public RFuture expireAsync(long timeToLive, TimeUnit timeUnit, String param, String... keys) {
        return super.expireAsync(timeToLive, timeUnit, param,
                                    getRawName(), getSubscribersName(), getMapName(), getCounter(), getTimeout());
    }

    @Override
    protected RFuture expireAtAsync(long timestamp, String param, String... keys) {
        return super.expireAtAsync(timestamp, param, getRawName(), getSubscribersName(), getMapName(), getCounter(), getTimeout());
    }

    @Override
    public RFuture clearExpireAsync() {
        return clearExpireAsync(getRawName(), getSubscribersName(), getMapName(), getCounter(), getTimeout());
    }

    @Override
    public RFuture removeListenerAsync(String... listenerIds) {
        listeners.keySet().removeAll(Arrays.asList(listenerIds));

        if (listeners.isEmpty()) {
            return removeSubscriber();
        }
        return new CompletableFutureWrapper<>((Void) null);
    }

    private RFuture removeSubscriber() {
        readFuture.cancel(false);
        timeoutTask.cancel();

        String id = subscriberId.getAndSet(null);
        return commandExecutor.evalWriteAsync(getRawName(), StringCodec.INSTANCE, RedisCommands.EVAL_VOID,
                "redis.call('zrem', KEYS[3], ARGV[1]); "
                      + "redis.call('zrem', KEYS[1], ARGV[1]); "
                      + "redis.call('hdel', KEYS[2], ARGV[1]); ",
                Arrays.asList(getSubscribersName(), getMapName(), getTimeout()),
                id);
    }

    @Override
    public int countSubscribers() {
        return get(countSubscribersAsync());
    }

    @Override
    public RFuture countSubscribersAsync() {
        return commandExecutor.readAsync(getRawName(), StringCodec.INSTANCE, RedisCommands.ZCARD_INT, getSubscribersName());
    }

    private void renewExpiration() {
        timeoutTask = commandExecutor.getConnectionManager().newTimeout(t -> {
            RFuture future = commandExecutor.evalWriteAsync(getRawName(), StringCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                   "if redis.call('zscore', KEYS[1], ARGV[2]) == false then "
                         + "return 0; "
                      + "end; "
                      + "redis.call('zadd', KEYS[1], ARGV[1], ARGV[2]); "
                      + "return 1; ",
                Arrays.asList(getTimeout()),
                System.currentTimeMillis() + commandExecutor.getConnectionManager().getCfg().getReliableTopicWatchdogTimeout(), subscriberId.get());
            future.whenComplete((res, e) -> {
                if (e != null) {
                    log.error("Can't update reliable topic " + getRawName() + " expiration time", e);
                    return;
                }

                if (res) {
                    // reschedule itself
                    renewExpiration();
                }
            });
        }, commandExecutor.getConnectionManager().getCfg().getReliableTopicWatchdogTimeout() / 3, TimeUnit.MILLISECONDS);
    }


}