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

org.redisson.RedissonRemoteService Maven / Gradle / Ivy

Go to download

Easy Redis Java client and Real-Time Data Platform. Valkey compatible. Sync/Async/RxJava3/Reactive API. Client side caching. Over 50 Redis based Java objects and services: JCache API, Apache Tomcat, Hibernate, Spring, Set, Multimap, SortedSet, Map, List, Queue, Deque, Semaphore, Lock, AtomicLong, Map Reduce, Bloom filter, Scheduler, RPC

There is a newer version: 3.40.2
Show newest version
/**
 * Copyright (c) 2013-2024 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 org.redisson;

import org.redisson.api.*;
import org.redisson.client.codec.Codec;
import org.redisson.client.codec.LongCodec;
import org.redisson.client.codec.StringCodec;
import org.redisson.client.protocol.RedisCommands;
import org.redisson.command.CommandAsyncExecutor;
import org.redisson.executor.RemotePromise;
import org.redisson.misc.CompletableFutureWrapper;
import org.redisson.remote.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 
 * @author Nikita Koksharov
 *
 */
public class RedissonRemoteService extends BaseRemoteService implements RRemoteService {

    public static class Entry {
        
        RFuture future;
        final AtomicInteger counter;
        
        public Entry(int workers) {
            counter = new AtomicInteger(workers);
        }
        
        public void setFuture(RFuture future) {
            this.future = future;
        }
        
        public RFuture getFuture() {
            return future;
        }
        
        public AtomicInteger getCounter() {
            return counter;
        }
        
    }
    
    private static final Logger log = LoggerFactory.getLogger(RedissonRemoteService.class);

    private final Map, Entry> remoteMap = new ConcurrentHashMap<>();

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

    public String getRequestTasksMapName(Class remoteInterface) {
        String queue = getRequestQueueName(remoteInterface);
        return queue + ":tasks";
    }

    @Override
    protected CompletableFuture addAsync(String requestQueueName, RemoteServiceRequest request,
                                                  RemotePromise result) {
        RFuture future = commandExecutor.evalWriteNoRetryAsync(name, LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                  "redis.call('hset', KEYS[2], ARGV[1], ARGV[2]);"
                + "redis.call('rpush', KEYS[1], ARGV[1]); "
                + "return 1;",
                Arrays.asList(requestQueueName, requestQueueName + ":tasks"),
                request.getId(), encode(request));

        result.setAddFuture(future.toCompletableFuture());
        return future.toCompletableFuture();
    }

    @Override
    protected CompletableFuture removeAsync(String requestQueueName, String taskId) {
        RFuture f = commandExecutor.evalWriteNoRetryAsync(name, LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                "if redis.call('lrem', KEYS[1], 1, ARGV[1]) > 0 then "
                        + "redis.call('hdel', KEYS[2], ARGV[1]);" +
                           "return 1;" +
                       "end;"
                      + "return 0;",
              Arrays.asList(requestQueueName, requestQueueName + ":tasks"),
              taskId);
        return f.toCompletableFuture();
    }
        
    @Override
    public  void register(Class remoteInterface, T object) {
        register(remoteInterface, object, 1);
    }

    @Override
    public  void deregister(Class remoteInterface) {
        Entry entry = remoteMap.remove(remoteInterface);
        if (entry != null && entry.getFuture() != null) {
            entry.getFuture().cancel(false);
        }
    }
    
    @Override
    public int getPendingInvocations(Class remoteInterface) {
        String requestQueueName = getRequestQueueName(remoteInterface);
        RBlockingQueue requestQueue = getBlockingQueue(requestQueueName, StringCodec.INSTANCE);
        return requestQueue.size();
    }

    @Override
    public RFuture getPendingInvocationsAsync(Class remoteInterface) {
        String requestQueueName = getRequestQueueName(remoteInterface);
        RBlockingQueue requestQueue = getBlockingQueue(requestQueueName, StringCodec.INSTANCE);
        return requestQueue.sizeAsync();
    }

    @Override
    public int getFreeWorkers(Class remoteInterface) {
        Entry entry = remoteMap.get(remoteInterface);
        if (entry == null) {
            return 0;
        }
        return entry.getCounter().get();
    }
    
    @Override
    public  void register(Class remoteInterface, T object, int workers) {
        register(remoteInterface, object, workers, commandExecutor.getServiceManager().getExecutor());
    }

    @Override
    public  void register(Class remoteInterface, T object, int workers, ExecutorService executor) {
        if (workers < 1) {
            throw new IllegalArgumentException("executorsAmount can't be lower than 1");
        }

        if (remoteMap.putIfAbsent(remoteInterface, new Entry(workers)) != null) {
            return;
        }
        
        String requestQueueName = getRequestQueueName(remoteInterface);
        RBlockingQueue requestQueue = getBlockingQueue(requestQueueName, StringCodec.INSTANCE);
        subscribe(remoteInterface, requestQueue, executor, object);
    }

    @Override
    public  boolean tryExecute(Class remoteInterface, T object, long timeout, TimeUnit timeUnit) throws InterruptedException {
        return tryExecute(remoteInterface, object, commandExecutor.getServiceManager().getExecutor(), timeout, timeUnit);
    }

    @Override
    public  boolean tryExecute(Class remoteInterface, T object, ExecutorService executorService, long timeout, TimeUnit timeUnit) throws InterruptedException {
        String requestQueueName = getRequestQueueName(remoteInterface);
        RBlockingQueue requestQueue = getBlockingQueue(requestQueueName, StringCodec.INSTANCE);

        String requestId = requestQueue.poll(timeout, timeUnit);
        if (requestId == null) {
            return false;
        }

        RMap tasks = getMap(((RedissonObject) requestQueue).getRawName() + ":tasks");
        RFuture taskFuture = getTask(requestId, tasks);
        RemoteServiceRequest request = commandExecutor.getInterrupted(taskFuture);
        if (request == null) {
            throw new IllegalStateException("Task can't be found for request: " + requestId);
        }

        RFuture r = executeMethod(remoteInterface, requestQueue, executorService, request, object);
        commandExecutor.getInterrupted(r);
        return true;
    }

    @Override
    public  RFuture tryExecuteAsync(Class remoteInterface, T object, long timeout, TimeUnit timeUnit) {
        return tryExecuteAsync(remoteInterface, object, commandExecutor.getServiceManager().getExecutor(), timeout, timeUnit);
    }

    @Override
    public  RFuture tryExecuteAsync(Class remoteInterface, T object, ExecutorService executor, long timeout, TimeUnit timeUnit) {
        String requestQueueName = getRequestQueueName(remoteInterface);

        RBlockingQueue requestQueue = getBlockingQueue(requestQueueName, StringCodec.INSTANCE);
        RFuture pollFuture;
        if (timeout == -1) {
            pollFuture = requestQueue.pollAsync();
        } else {
            pollFuture = requestQueue.pollAsync(timeout, timeUnit);
        }
        CompletionStage f = pollFuture.thenCompose(requestId -> {
            if (requestId == null) {
                return CompletableFuture.completedFuture(false);
            }

            RMap tasks = getMap(((RedissonObject) requestQueue).getRawName() + ":tasks");
            RFuture taskFuture = getTask(requestId, tasks);
            return taskFuture.thenCompose(request -> {
                if (request == null) {
                    throw new CompletionException(new IllegalStateException("Task can't be found for request: " + requestId));
                }

                RFuture future = executeMethod(remoteInterface, requestQueue, executor, request, object);
                return future.thenApply(r -> true);
            });
        });
        return new CompletableFutureWrapper<>(f);
    }

    @Override
    public  RFuture tryExecuteAsync(Class remoteInterface, T object) {
        return tryExecuteAsync(remoteInterface, object, -1, null);
    }
    
    @SuppressWarnings("MethodLength")
    private  void subscribe(Class remoteInterface, RBlockingQueue requestQueue,
            ExecutorService executor, Object bean) {
        Entry entry = remoteMap.get(remoteInterface);
        if (entry == null) {
            return;
        }
        RFuture take = requestQueue.pollAsync(60, TimeUnit.SECONDS);
        entry.setFuture(take);
        take.whenComplete((requestId, e) -> {
                Entry entr = remoteMap.get(remoteInterface);
                if (entr == null) {
                    return;
                }
                
                if (e != null) {
                    if (commandExecutor.getServiceManager().isShuttingDown(e)) {
                        return;
                    }
                    log.error("Can't process the remote service request. A new attempt has been made.", e);
                    // re-subscribe after a failed takeAsync
                    commandExecutor.getServiceManager().newTimeout(task -> {
                        subscribe(remoteInterface, requestQueue, executor, bean);
                    }, commandExecutor.getServiceManager().getConfig().getRetryInterval(), TimeUnit.MILLISECONDS);

                    return;
                }

                // do not subscribe now, see
                // https://github.com/mrniko/redisson/issues/493
                // subscribe(remoteInterface, requestQueue);
                
                if (entry.getCounter().get() == 0) {
                    return;
                }
                
                if (entry.getCounter().decrementAndGet() > 0) {
                    subscribe(remoteInterface, requestQueue, executor, bean);
                }

                // poll method may return null value
                if (requestId == null) {
                    // Because the previous code is already -1, it must be +1 before returning, otherwise the counter will become 0 soon
                    resubscribe(remoteInterface, requestQueue, executor, bean);
                    return;
                }

                RMap tasks = getMap(((RedissonObject) requestQueue).getRawName() + ":tasks");
                RFuture taskFuture = getTask(requestId, tasks);
                taskFuture.whenComplete((request, exc) -> {
                    if (exc != null) {
                        if (commandExecutor.getServiceManager().isShuttingDown(exc)) {
                            return;
                        }
                        log.error("Can't process the remote service request with id {}. Try to increase 'retryInterval' and/or 'retryAttempts' settings", requestId, exc);
                            
                        // re-subscribe after a failed takeAsync
                        resubscribe(remoteInterface, requestQueue, executor, bean);
                        return;
                    }
                    
                    if (request == null) {
                        log.debug("Task can't be found for request: {}", requestId);
                        
                        // re-subscribe after a skipped ackTimeout
                        resubscribe(remoteInterface, requestQueue, executor, bean);
                        return;
                    }
                    
                    long elapsedTime = System.currentTimeMillis() - request.getDate();
                    // check the ack only if expected
                    if (request.getOptions().isAckExpected() && elapsedTime > request
                            .getOptions().getAckTimeoutInMillis()) {
                        log.debug("request: {} has been skipped due to ackTimeout. Elapsed time: {}ms", request.getId(), elapsedTime);
                        
                        // re-subscribe after a skipped ackTimeout
                        resubscribe(remoteInterface, requestQueue, executor, bean);
                        return;
                    }


                    // send the ack only if expected
                    if (request.getOptions().isAckExpected()) {
                        String responseName = getResponseQueueName(request.getExecutorId());
                        String ackName = getAckName(request.getId());
                                RFuture ackClientsFuture = commandExecutor.evalWriteNoRetryAsync(responseName,
                                        LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                                            "if redis.call('setnx', KEYS[1], 1) == 1 then " 
                                                + "redis.call('pexpire', KEYS[1], ARGV[1]);"
//                                                    + "redis.call('rpush', KEYS[2], ARGV[1]);"
//                                                    + "redis.call('pexpire', KEYS[2], ARGV[2]);" 
                                                + "return 1;" 
                                            + "end;" 
                                            + "return 0;",
                                        Arrays.asList(ackName),
                                        request.getOptions().getAckTimeoutInMillis());
//                                            Arrays.asList(ackName, responseName),
//                                            encode(new RemoteServiceAck(request.getId())), request.getOptions().getAckTimeoutInMillis());

                                ackClientsFuture.whenComplete((r, ex) -> {
                                    if (ex != null) {
                                        if (commandExecutor.getServiceManager().isShuttingDown(ex)) {
                                            return;
                                        }
                                        log.error("Can't send ack for request: {}. Try to increase 'retryInterval' and/or 'retryAttempts' settings", request, ex);

                                        // re-subscribe after a failed send (ack)
                                        resubscribe(remoteInterface, requestQueue, executor, bean);
                                        return;
                                    }

                                    if (!r) {
                                        resubscribe(remoteInterface, requestQueue, executor, bean);
                                        return;
                                    }
                                    

                                    RList list = new RedissonList<>(codec, commandExecutor, responseName, null);
                                    RFuture addFuture = list.addAsync(new RemoteServiceAck(request.getId()));
                                    addFuture.whenComplete((res, exce) -> {
                                        if (exce != null) {
                                            if (commandExecutor.getServiceManager().isShuttingDown(exce)) {
                                                return;
                                            }
                                            log.error("Can't send ack for request: {}. Try to increase 'retryInterval' and/or 'retryAttempts' settings", request, exce);

                                            // re-subscribe after a failed send (ack)
                                            resubscribe(remoteInterface, requestQueue, executor, bean);
                                            return;
                                        }

                                        if (!res) {
                                            resubscribe(remoteInterface, requestQueue, executor, bean);
                                            return;
                                        }
                                        
                                        executeMethod(remoteInterface, requestQueue, executor, request, bean);
                                    })
                                    .exceptionally(exack -> {
                                        if (commandExecutor.getServiceManager().isShuttingDown(exack)) {
                                            return null;
                                        }
                                        log.error("Can't send ack for request: {}", request, exack);
                                        return null;
                                    });
                                });
                    } else {
                        executeMethod(remoteInterface, requestQueue, executor, request, bean);
                    }
                })
                .exceptionally(exc -> {
                    if (commandExecutor.getServiceManager().isShuttingDown(exc)) {
                        return null;
                    }
                    log.error("Can't process the remote service request with id {}", requestId, exc);
                    return null;
                });
        });
    }

    private final Map methodsCache = new ConcurrentHashMap<>();

    private  RFuture executeMethod(Class remoteInterface, RBlockingQueue requestQueue,
            ExecutorService executor, RemoteServiceRequest request, Object bean) {

        RemoteServiceKey key = new RemoteServiceKey(remoteInterface, request.getMethodName(), request.getSignature());
        Method rm = methodsCache.computeIfAbsent(key, k -> Arrays.stream(k.getServiceInterface().getMethods())
                                                          .filter(m -> m.getName().equals(k.getMethodName())
                                                                         && Arrays.equals(getMethodSignature(m), k.getSignature()))
                                                          .findFirst().get());

        RemoteServiceMethod method = new RemoteServiceMethod(rm, bean);
        String responseName = getResponseQueueName(request.getExecutorId());

        CompletableFuture responsePromise = new CompletableFuture<>();
        CompletableFuture cancelRequestFuture = new CompletableFuture<>();
        scheduleCheck(cancelRequestMapName, request.getId(), cancelRequestFuture);

        responsePromise.whenComplete((result, e) -> {
            if (request.getOptions().isResultExpected()
                || result instanceof RemoteServiceCancelResponse) {

                long timeout = 60 * 1000;
                if (request.getOptions().getExecutionTimeoutInMillis() != null) {
                    timeout = request.getOptions().getExecutionTimeoutInMillis();
                }

                RBlockingQueueAsync queue = getBlockingQueue(responseName, codec);
                try {
                    RRemoteServiceResponse response;
                    if (result instanceof RemoteServiceResponse
                            && ((RemoteServiceResponse) result).getResult() instanceof Optional) {
                        Optional o = (Optional) ((RemoteServiceResponse) result).getResult();
                        response = new RemoteServiceResponse(result.getId(), o.orElse(null));
                    } else {
                        response = result;
                    }
                    RFuture clientsFuture = queue.putAsync(response);
                    queue.expireAsync(timeout, TimeUnit.MILLISECONDS);

                    clientsFuture.whenComplete((res, exc) -> {
                        if (exc != null) {
                            if (commandExecutor.getServiceManager().isShuttingDown(exc)) {
                                return;
                            }
                            log.error("Can't send response: {} for request: {}. Try to increase 'retryInterval' and/or 'retryAttempts' settings", response, request, exc);
                        }

                        resubscribe(remoteInterface, requestQueue, executor, method.getBean());
                    });
                } catch (Exception ex) {
                    log.error("Can't send response: {} for request: {}", result, request, ex);
                }
            } else {
                resubscribe(remoteInterface, requestQueue, executor, method.getBean());
            }
        });

        java.util.concurrent.Future submitFuture = executor.submit(() -> {
            if (commandExecutor.getServiceManager().isShuttingDown()) {
                return;
            }

            invokeMethod(request, method, cancelRequestFuture, responsePromise);
        });

        cancelRequestFuture.thenAccept(r -> {
            boolean res = submitFuture.cancel(r.isMayInterruptIfRunning());
            if (res) {
                RemoteServiceCancelResponse response = new RemoteServiceCancelResponse(request.getId(), true);
                if (!responsePromise.complete(response)) {
                    response = new RemoteServiceCancelResponse(request.getId(), false);
                }
                
                // could be removed not from future object
                if (r.isSendResponse()) {
                    RMap map = getMap(cancelResponseMapName);
                    map.fastPutAsync(request.getId(), response);
                    map.expireAsync(60, TimeUnit.SECONDS);
                }
            }
        });

        return new CompletableFutureWrapper<>(responsePromise);
    }

    protected  void invokeMethod(RemoteServiceRequest request, RemoteServiceMethod method,
                                    CompletableFuture cancelRequestFuture,
                                    CompletableFuture responsePromise) {
        try {
            Object result = method.getMethod().invoke(method.getBean(), request.getArgs());

            RemoteServiceResponse response = new RemoteServiceResponse(request.getId(), result);
            responsePromise.complete(response);
        } catch (Exception e) {
            RemoteServiceResponse response = new RemoteServiceResponse(request.getId(), e.getCause());
            responsePromise.complete(response);
            log.error("Can't execute: {}", request, e);
        }

        if (cancelRequestFuture != null) {
            cancelRequestFuture.cancel(false);
        }
    }

    private  void resubscribe(Class remoteInterface, RBlockingQueue requestQueue,
            ExecutorService executor, Object bean) {
        Entry entry = remoteMap.get(remoteInterface);
        if (entry != null && entry.getCounter().getAndIncrement() == 0) {
            // re-subscribe anyways after the method invocation
            subscribe(remoteInterface, requestQueue, executor, bean);
        }
    }

    protected RFuture getTask(String requestId, RMap tasks) {
        return tasks.removeAsync(requestId);
    }

}