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

com.github.lontime.shaded.org.redisson.RedissonDelayedQueue 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 java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;

import com.github.lontime.shaded.org.redisson.api.RDelayedQueue;
import com.github.lontime.shaded.org.redisson.api.RFuture;
import com.github.lontime.shaded.org.redisson.api.RTopic;
import com.github.lontime.shaded.org.redisson.client.codec.Codec;
import com.github.lontime.shaded.org.redisson.client.codec.LongCodec;
import com.github.lontime.shaded.org.redisson.client.protocol.RedisCommands;
import com.github.lontime.shaded.org.redisson.command.CommandAsyncExecutor;
import com.github.lontime.shaded.org.redisson.misc.CompletableFutureWrapper;

/**
 * 
 * @author Nikita Koksharov
 *
 * @param  value type
 */
public class RedissonDelayedQueue extends RedissonExpirable implements RDelayedQueue {

    private final QueueTransferService queueTransferService;
    private final String channelName;
    private final String queueName;
    private final String timeoutSetName;
    
    protected RedissonDelayedQueue(QueueTransferService queueTransferService, Codec codec, final CommandAsyncExecutor commandExecutor, String name) {
        super(codec, commandExecutor, name);
        channelName = prefixName("redisson_delay_queue_channel", getRawName());
        queueName = prefixName("redisson_delay_queue", getRawName());
        timeoutSetName = prefixName("redisson_delay_queue_timeout", getRawName());
        
        QueueTransferTask task = new QueueTransferTask(commandExecutor.getConnectionManager()) {
            
            @Override
            protected RFuture pushTaskAsync() {
                return commandExecutor.evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_LONG,
                        "local expiredValues = redis.call('zrangebyscore', KEYS[2], 0, ARGV[1], 'limit', 0, ARGV[2]); "
                      + "if #expiredValues > 0 then "
                          + "for i, v in ipairs(expiredValues) do "
                              + "local randomId, value = struct.unpack('dLc0', v);"
                              + "redis.call('rpush', KEYS[1], value);"
                              + "redis.call('lrem', KEYS[3], 1, v);"
                          + "end; "
                          + "redis.call('zrem', KEYS[2], unpack(expiredValues));"
                      + "end; "
                        // get startTime from scheduler queue head task
                      + "local v = redis.call('zrange', KEYS[2], 0, 0, 'WITHSCORES'); "
                      + "if v[1] ~= nil then "
                         + "return v[2]; "
                      + "end "
                      + "return nil;",
                      Arrays.asList(getRawName(), timeoutSetName, queueName),
                      System.currentTimeMillis(), 100);
            }
            
            @Override
            protected RTopic getTopic() {
                return RedissonTopic.createRaw(LongCodec.INSTANCE, commandExecutor, channelName);
            }
        };
        
        queueTransferService.schedule(queueName, task);
        
        this.queueTransferService = queueTransferService;
    }

    @Override
    public void offer(V e, long delay, TimeUnit timeUnit) {
        get(offerAsync(e, delay, timeUnit));
    }
    
    @Override
    public RFuture offerAsync(V e, long delay, TimeUnit timeUnit) {
        if (delay < 0) {
            throw new IllegalArgumentException("Delay can't be negative");
        }
        
        long delayInMs = timeUnit.toMillis(delay);
        long timeout = System.currentTimeMillis() + delayInMs;
     
        long randomId = ThreadLocalRandom.current().nextLong();
        return commandExecutor.evalWriteNoRetryAsync(getRawName(), codec, RedisCommands.EVAL_VOID,
                "local value = struct.pack('dLc0', tonumber(ARGV[2]), string.len(ARGV[3]), ARGV[3]);" 
              + "redis.call('zadd', KEYS[2], ARGV[1], value);"
              + "redis.call('rpush', KEYS[3], value);"
              // if new object added to queue head when publish its startTime 
              // to all scheduler workers 
              + "local v = redis.call('zrange', KEYS[2], 0, 0); "
              + "if v[1] == value then "
                 + "redis.call('publish', KEYS[4], ARGV[1]); "
              + "end;",
              Arrays.asList(getRawName(), timeoutSetName, queueName, channelName),
              timeout, randomId, encode(e));
    }

    @Override
    public boolean add(V e) {
        throw new UnsupportedOperationException("Use 'offer' method with timeout param");
    }

    @Override
    public boolean offer(V e) {
        throw new UnsupportedOperationException("Use 'offer' method with timeout param");
    }

    @Override
    public V remove() {
        V value = poll();
        if (value == null) {
            throw new NoSuchElementException();
        }
        return value;
    }

    @Override
    public V poll() {
        return get(pollAsync());
    }

    @Override
    public V element() {
        V value = peek();
        if (value == null) {
            throw new NoSuchElementException();
        }
        return value;
    }

    @Override
    public V peek() {
        return get(peekAsync());
    }

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

    @Override
    public boolean isEmpty() {
        return size() == 0;
    }

    @Override
    public boolean contains(Object o) {
        return get(containsAsync(o));
    }

    V getValue(int index) {
        return (V) get(commandExecutor.evalReadAsync(getRawName(), codec, RedisCommands.EVAL_OBJECT,
                "local v = redis.call('lindex', KEYS[1], ARGV[1]); "
              + "if v ~= false then "
                  + "local randomId, value = struct.unpack('dLc0', v);"
                  + "return value; "
              + "end "
              + "return nil;",
              Arrays.asList(queueName), index));
    }
    
    void remove(int index) {
        get(commandExecutor.evalWriteAsync(getRawName(), null, RedisCommands.EVAL_VOID,
                "local v = redis.call('lindex', KEYS[1], ARGV[1]);" + 
                "if v ~= false then " + 
                   "local randomId, value = struct.unpack('dLc0', v);" + 
                   "redis.call('lrem', KEYS[1], 1, v);" + 
                   "redis.call('zrem', KEYS[2], v);" +
                "end; ",
                Arrays.asList(queueName, timeoutSetName), index));
    }
    
    @Override
    public Iterator iterator() {
        return new Iterator() {

            private V nextCurrentValue;
            private V currentValueHasRead;
            private int currentIndex = -1;
            private boolean hasBeenModified = true;

            @Override
            public boolean hasNext() {
                V val = RedissonDelayedQueue.this.getValue(currentIndex+1);
                if (val != null) {
                    nextCurrentValue = val;
                }
                return val != null;
            }

            @Override
            public V next() {
                if (nextCurrentValue == null && !hasNext()) {
                    throw new NoSuchElementException("No such element at index " + currentIndex);
                }
                currentIndex++;
                currentValueHasRead = nextCurrentValue;
                nextCurrentValue = null;
                hasBeenModified = false;
                return currentValueHasRead;
            }

            @Override
            public void remove() {
                if (currentValueHasRead == null) {
                    throw new IllegalStateException("Neither next nor previous have been called");
                }
                if (hasBeenModified) {
                    throw new IllegalStateException("Element been already deleted");
                }
                RedissonDelayedQueue.this.remove(currentIndex);
                currentIndex--;
                hasBeenModified = true;
                currentValueHasRead = null;
            }

        };
    }

    @Override
    public Object[] toArray() {
        List list = readAll();
        return list.toArray();
    }
    
    @Override
    public  T[] toArray(T[] a) {
        List list = readAll();
        return list.toArray(a);
    }

    @Override
    public List readAll() {
        return get(readAllAsync());
    }

    @Override
    public List poll(int limit) {
        return get(pollAsync(limit));
    }

    @Override
    public RFuture> readAllAsync() {
        return commandExecutor.evalReadAsync(getRawName(), codec, RedisCommands.EVAL_LIST,
                "local result = {}; " +
                "local items = redis.call('lrange', KEYS[1], 0, -1); "
              + "for i, v in ipairs(items) do "
                   + "local randomId, value = struct.unpack('dLc0', v); "
                   + "table.insert(result, value);"
              + "end; "
              + "return result; ",
           Collections.singletonList(queueName));
    }

    @Override
    public RFuture> pollAsync(int limit) {
        return commandExecutor.evalWriteNoRetryAsync(getRawName(), codec, RedisCommands.EVAL_LIST,
                   "local result = {};"
                 + "for i = 1, ARGV[1], 1 do " +
                       "local v = redis.call('lpop', KEYS[1]);" +
                       "if v ~= false then " +
                           "redis.call('zrem', KEYS[2], v); " +
                           "local randomId, value = struct.unpack('dLc0', v);" +
                           "table.insert(result, value);" +
                       "else " +
                           "return result;" +
                       "end;" +
                   "end; " +
                   "return result;",
                Arrays.asList(queueName, timeoutSetName), limit);
    }

    @Override
    public boolean remove(Object o) {
        return get(removeAsync(o));
    }

    @Override
    public RFuture removeAsync(Object o) {
        return removeAsync(o, 1);
    }

    protected RFuture removeAsync(Object o, int count) {
        return commandExecutor.evalWriteAsync(getRawName(), codec, RedisCommands.EVAL_BOOLEAN,
                "local s = redis.call('llen', KEYS[1]);" +
                "for i = 0, s-1, 1 do "
                    + "local v = redis.call('lindex', KEYS[1], i);"
                    + "local randomId, value = struct.unpack('dLc0', v);"
                    + "if ARGV[1] == value then "
                        + "redis.call('zrem', KEYS[2], v);"
                        + "redis.call('lrem', KEYS[1], 1, v);"
                        + "return 1;"
                    + "end; "
               + "end;" +
               "return 0;",
        Arrays.asList(queueName, timeoutSetName), encode(o));
    }

    @Override
    public RFuture containsAllAsync(Collection c) {
        if (c.isEmpty()) {
            return new CompletableFutureWrapper<>(true);
        }

        return commandExecutor.evalReadAsync(getRawName(), codec, RedisCommands.EVAL_BOOLEAN,
                "local s = redis.call('llen', KEYS[1]);" +
                "for i = 0, s-1, 1 do "
                    + "local v = redis.call('lindex', KEYS[1], i);"
                    + "local randomId, value = struct.unpack('dLc0', v);"
                    
                    + "for j = 1, #ARGV, 1 do "
                        + "if value == ARGV[j] then "
                          + "table.remove(ARGV, j) "
                        + "end; "
                    + "end; "
               + "end;" +
               "return #ARGV == 0 and 1 or 0;",
                Collections.singletonList(queueName), encode(c).toArray());
    }

    @Override
    public boolean containsAll(Collection c) {
        return get(containsAllAsync(c));
    }

    @Override
    public boolean addAll(Collection c) {
        throw new UnsupportedOperationException("Use 'offer' method with timeout param");
    }

    @Override
    public RFuture removeAllAsync(Collection c) {
        if (c.isEmpty()) {
            return new CompletableFutureWrapper<>(false);
        }

        return commandExecutor.evalWriteAsync(getRawName(), codec, RedisCommands.EVAL_BOOLEAN,
                "local result = 0;" + 
                "local s = redis.call('llen', KEYS[1]);" + 
                "local i = 0;" +
                "while i < s do "
                    + "local v = redis.call('lindex', KEYS[1], i);"
                    + "local randomId, value = struct.unpack('dLc0', v);"
                    
                    + "for j = 1, #ARGV, 1 do "
                        + "if value == ARGV[j] then "
                            + "result = 1; "
                            + "i = i - 1; "
                            + "s = s - 1; "
                            + "redis.call('zrem', KEYS[2], v);"
                            + "redis.call('lrem', KEYS[1], 0, v); "
                            + "break; "
                        + "end; "
                    + "end; "
                    + "i = i + 1;"
               + "end; " 
               + "return result;",
               Arrays.asList(queueName, timeoutSetName), encode(c).toArray());
    }

    @Override
    public boolean removeAll(Collection c) {
        return get(removeAllAsync(c));
    }

    @Override
    public boolean retainAll(Collection c) {
        return get(retainAllAsync(c));
    }

    @Override
    public RFuture retainAllAsync(Collection c) {
        if (c.isEmpty()) {
            return deleteAsync();
        }

        return commandExecutor.evalWriteAsync(getRawName(), codec, RedisCommands.EVAL_BOOLEAN,
                     "local changed = 0; " +
                     "local items = redis.call('lrange', KEYS[1], 0, -1); "
                   + "local i = 1; "
                   + "while i <= #items do "
                        + "local randomId, element = struct.unpack('dLc0', items[i]); "
                        + "local isInAgrs = false; "
                        + "for j = 1, #ARGV, 1 do "
                            + "if ARGV[j] == element then "
                                + "isInAgrs = true; "
                                + "break; "
                            + "end; "
                        + "end; "
                        + "if isInAgrs == false then "
                            + "redis.call('LREM', KEYS[1], 0, items[i]) "
                            + "changed = 1; "
                        + "end; "
                        + "i = i + 1; "
                   + "end; "
                   + "return changed; ",
                Collections.singletonList(queueName), encode(c).toArray());
    }  

    @Override
    public void clear() {
        delete();
    }
    
    @Override
    public RFuture deleteAsync() {
        return deleteAsync(queueName, timeoutSetName);
    }
    
    @Override
    public RFuture sizeInMemoryAsync() {
        List keys = Arrays.asList(queueName, timeoutSetName);
        return super.sizeInMemoryAsync(keys);
    }

    @Override
    public RFuture expireAsync(long timeToLive, TimeUnit timeUnit, String param, String... keys) {
        return super.expireAsync(timeToLive, timeUnit, param, queueName, timeoutSetName);
    }

    @Override
    protected RFuture expireAtAsync(long timestamp, String param, String... keys) {
        return super.expireAtAsync(timestamp, param, queueName, timeoutSetName);
    }

    @Override
    public RFuture clearExpireAsync() {
        return clearExpireAsync(queueName, timeoutSetName);
    }

    @Override
    public RFuture peekAsync() {
        return commandExecutor.evalReadAsync(getRawName(), codec, RedisCommands.EVAL_OBJECT,
                "local v = redis.call('lindex', KEYS[1], 0); "
              + "if v ~= false then "
                  + "local randomId, value = struct.unpack('dLc0', v);"
                  + "return value; "
              + "end "
              + "return nil;",
              Arrays.asList(queueName));
    }

    @Override
    public RFuture pollAsync() {
        return commandExecutor.evalWriteNoRetryAsync(getRawName(), codec, RedisCommands.EVAL_OBJECT,
                  "local v = redis.call('lpop', KEYS[1]); "
                + "if v ~= false then "
                    + "redis.call('zrem', KEYS[2], v); "
                    + "local randomId, value = struct.unpack('dLc0', v);"
                    + "return value; "
                + "end "
                + "return nil;",
                Arrays.asList(queueName, timeoutSetName));
    }

    @Override
    public RFuture offerAsync(V e) {
        throw new UnsupportedOperationException("Use 'offer' method with timeout param");
    }

    @Override
    public RFuture pollLastAndOfferFirstToAsync(String queueName) {
        return commandExecutor.evalWriteNoRetryAsync(getRawName(), codec, RedisCommands.EVAL_OBJECT,
                "local v = redis.call('rpop', KEYS[1]); "
              + "if v ~= false then "
                  + "redis.call('zrem', KEYS[2], v); "
                  + "local randomId, value = struct.unpack('dLc0', v);"
                  + "redis.call('lpush', KEYS[3], value); "
                  + "return value; "
              + "end "
              + "return nil;",
              Arrays.asList(this.queueName, timeoutSetName, queueName));
    }

    @Override
    public RFuture containsAsync(Object o) {
        return commandExecutor.evalReadAsync(getRawName(), codec, RedisCommands.EVAL_BOOLEAN,
                        "local s = redis.call('llen', KEYS[1]);" +
                        "for i = 0, s-1, 1 do "
                            + "local v = redis.call('lindex', KEYS[1], i);"
                            + "local randomId, value = struct.unpack('dLc0', v);"
                            + "if ARGV[1] == value then "
                                + "return 1;"
                            + "end; "
                       + "end;" +
                       "return 0;",
                Collections.singletonList(queueName), encode(o));
    }

    @Override
    public RFuture sizeAsync() {
        return commandExecutor.readAsync(getRawName(), codec, RedisCommands.LLEN_INT, queueName);
    }

    @Override
    public RFuture addAsync(V e) {
        throw new UnsupportedOperationException("Use 'offer' method with timeout param");
    }

    @Override
    public RFuture addAllAsync(Collection c) {
        throw new UnsupportedOperationException("Use 'offer' method with timeout param");
    }

    @Override
    public V pollLastAndOfferFirstTo(String dequeName) {
        return get(pollLastAndOfferFirstToAsync(dequeName));
    }

    @Override
    public void destroy() {
        queueTransferService.remove(queueName);
    }
    
}