org.redisson.executor.TasksRunnerService Maven / Gradle / Ivy
Show all versions of redisson-all Show documentation
/**
* 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.executor;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.Unpooled;
import org.redisson.RedissonExecutorService;
import org.redisson.RedissonShutdownException;
import org.redisson.api.RFuture;
import org.redisson.api.RedissonClient;
import org.redisson.api.RemoteInvocationOptions;
import org.redisson.cache.LRUCacheMap;
import org.redisson.client.RedisException;
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.codec.CustomObjectInputStream;
import org.redisson.command.CommandAsyncExecutor;
import org.redisson.executor.params.*;
import org.redisson.misc.Hash;
import org.redisson.misc.HashValue;
import org.redisson.misc.Injector;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInput;
import java.util.Arrays;
import java.util.Date;
import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
/**
* Executor service runs Callable and Runnable tasks.
*
* @author Nikita Koksharov
*
*/
public class TasksRunnerService implements RemoteExecutorService {
private static final Map CODECS = new LRUCacheMap<>(500, 0, 0);
private final Codec codec;
private final String name;
private final CommandAsyncExecutor commandExecutor;
private final RedissonClient redisson;
private String tasksCounterName;
private String statusName;
private String terminationTopicName;
private String tasksName;
private String schedulerQueueName;
private String schedulerChannelName;
private String tasksRetryIntervalName;
private String tasksExpirationTimeName;
private TasksInjector tasksInjector;
public TasksRunnerService(CommandAsyncExecutor commandExecutor, RedissonClient redisson, Codec codec, String name) {
this.commandExecutor = commandExecutor;
this.name = name;
this.redisson = redisson;
this.codec = codec;
}
public void setTasksInjector(TasksInjector tasksInjector) {
this.tasksInjector = tasksInjector;
}
public void setTasksExpirationTimeName(String tasksExpirationTimeName) {
this.tasksExpirationTimeName = tasksExpirationTimeName;
}
public void setTasksRetryIntervalName(String tasksRetryInterval) {
this.tasksRetryIntervalName = tasksRetryInterval;
}
public void setSchedulerQueueName(String schedulerQueueName) {
this.schedulerQueueName = schedulerQueueName;
}
public void setSchedulerChannelName(String schedulerChannelName) {
this.schedulerChannelName = schedulerChannelName;
}
public void setTasksName(String tasksName) {
this.tasksName = tasksName;
}
public void setTasksCounterName(String tasksCounterName) {
this.tasksCounterName = tasksCounterName;
}
public void setStatusName(String statusName) {
this.statusName = statusName;
}
public void setTerminationTopicName(String terminationTopicName) {
this.terminationTopicName = terminationTopicName;
}
@Override
public void scheduleAtFixedRate(ScheduledAtFixedRateParameters params) {
long start = System.nanoTime();
executeRunnable(params, false);
if (!redisson.getMap(tasksName, StringCodec.INSTANCE).containsKey(params.getRequestId())) {
return;
}
long spent = params.getSpentTime()
+ TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
long newStartTime = System.currentTimeMillis() + Math.max(params.getPeriod() - spent, 0);
params.setStartTime(newStartTime);
spent = Math.max(spent - params.getPeriod(), 0);
params.setSpentTime(spent);
asyncScheduledServiceAtFixed(params.getExecutorId(), params.getRequestId()).scheduleAtFixedRate(params);
}
@Override
public void schedule(ScheduledCronExpressionParameters params) {
CronExpression expression = new CronExpression(params.getCronExpression());
expression.setTimeZone(TimeZone.getTimeZone(params.getTimezone()));
Date nextStartDate = expression.getNextValidTimeAfter(new Date());
executeRunnable(params, nextStartDate == null);
if (nextStartDate == null || !redisson.getMap(tasksName, StringCodec.INSTANCE).containsKey(params.getRequestId())) {
return;
}
params.setStartTime(nextStartDate.getTime());
asyncScheduledServiceAtFixed(params.getExecutorId(), params.getRequestId()).schedule(params);
}
/**
* Creates RemoteExecutorServiceAsync with special executor which overrides requestId generation
* and uses current requestId. Because recurring tasks should use the same requestId.
*
* @return
*/
private RemoteExecutorServiceAsync asyncScheduledServiceAtFixed(String executorId, String requestId) {
ScheduledTasksService scheduledRemoteService = new ScheduledTasksService(codec, name, commandExecutor, executorId);
scheduledRemoteService.setTerminationTopicName(terminationTopicName);
scheduledRemoteService.setTasksCounterName(tasksCounterName);
scheduledRemoteService.setStatusName(statusName);
scheduledRemoteService.setSchedulerQueueName(schedulerQueueName);
scheduledRemoteService.setSchedulerChannelName(schedulerChannelName);
scheduledRemoteService.setTasksName(tasksName);
scheduledRemoteService.setRequestId(requestId);
scheduledRemoteService.setTasksExpirationTimeName(tasksExpirationTimeName);
scheduledRemoteService.setTasksRetryIntervalName(tasksRetryIntervalName);
RemoteExecutorServiceAsync asyncScheduledServiceAtFixed = scheduledRemoteService.get(RemoteExecutorServiceAsync.class, RemoteInvocationOptions.defaults().noAck().noResult());
return asyncScheduledServiceAtFixed;
}
@Override
public void scheduleWithFixedDelay(ScheduledWithFixedDelayParameters params) {
executeRunnable(params, false);
if (!redisson.getMap(tasksName, StringCodec.INSTANCE).containsKey(params.getRequestId())) {
return;
}
long newStartTime = System.currentTimeMillis() + params.getDelay();
params.setStartTime(newStartTime);
asyncScheduledServiceAtFixed(params.getExecutorId(), params.getRequestId()).scheduleWithFixedDelay(params);
}
@Override
public Object scheduleCallable(ScheduledParameters params) {
return executeCallable(params);
}
@Override
public void scheduleRunnable(ScheduledParameters params) {
executeRunnable(params);
}
@Override
public Object executeCallable(TaskParameters params) {
Object res;
try {
RFuture future = renewRetryTime(params.getRequestId());
future.toCompletableFuture().get();
Callable> callable = decode(params);
res = callable.call();
} catch (RedissonShutdownException e) {
throw e;
} catch (RedisException e) {
finish(params.getRequestId(), true);
throw e;
} catch (ExecutionException e) {
finish(params.getRequestId(), true);
if (e.getCause() instanceof RuntimeException) {
throw (RuntimeException) e.getCause();
} else {
throw new IllegalArgumentException(e.getCause());
}
} catch (Exception e) {
finish(params.getRequestId(), true);
throw new IllegalArgumentException(e);
}
finish(params.getRequestId(), true);
return res;
}
protected void scheduleRetryTimeRenewal(String requestId, Long retryInterval) {
if (retryInterval == null) {
return;
}
commandExecutor.getServiceManager().newTimeout(timeout -> renewRetryTime(requestId),
Math.max(1000, retryInterval / 2), TimeUnit.MILLISECONDS);
}
protected RFuture renewRetryTime(String requestId) {
RFuture future = commandExecutor.evalWriteAsync(name, LongCodec.INSTANCE, RedisCommands.EVAL_LONG,
// check if executor service not in shutdown state
"local name = ARGV[2];"
+ "local scheduledName = ARGV[2];"
+ "if string.sub(scheduledName, 1, 3) ~= 'ff:' then "
+ "scheduledName = 'ff:' .. scheduledName; "
+ "else "
+ "name = string.sub(name, 4, string.len(name)); "
+ "end;"
+ "local retryInterval = redis.call('get', KEYS[4]);"
+ "if redis.call('exists', KEYS[1]) == 0 and retryInterval ~= false and redis.call('hexists', KEYS[5], name) == 1 then "
+ "local startTime = tonumber(ARGV[1]) + tonumber(retryInterval);"
+ "redis.call('zadd', KEYS[2], startTime, scheduledName);"
+ "local v = redis.call('zrange', KEYS[2], 0, 0); "
// if new task added to queue head then publish its startTime
// to all scheduler workers
+ "if v[1] == ARGV[2] then "
+ "redis.call('publish', KEYS[3], startTime); "
+ "end;"
+ "return retryInterval; "
+ "end;"
+ "return nil;",
Arrays.asList(statusName, schedulerQueueName, schedulerChannelName, tasksRetryIntervalName, tasksName),
System.currentTimeMillis(), requestId);
future.whenComplete((res, e) -> {
if (e != null) {
scheduleRetryTimeRenewal(requestId, 10000L);
return;
}
if (res != null) {
scheduleRetryTimeRenewal(requestId, res);
}
});
return future;
}
private HashValue hash(ClassLoader classLoader, String className) throws IOException {
String classAsPath = className.replace('.', '/') + ".class";
InputStream classStream = classLoader.getResourceAsStream(classAsPath);
if (classStream == null) {
return HashValue.EMPTY;
}
ByteBuf out = ByteBufAllocator.DEFAULT.buffer();
out.writeBytes(classStream, classStream.available());
HashValue hash = new HashValue(Hash.hash128(out));
out.release();
return hash;
}
@SuppressWarnings("unchecked")
private T decode(TaskParameters params) {
ByteBuf classBodyBuf = Unpooled.wrappedBuffer(params.getClassBody());
ByteBuf stateBuf = Unpooled.wrappedBuffer(params.getState());
try {
HashValue hash = new HashValue(Hash.hash128(classBodyBuf));
Codec classLoaderCodec = CODECS.get(hash);
if (classLoaderCodec == null) {
HashValue v = hash(codec.getClassLoader(), params.getClassName());
if (v.equals(hash)) {
classLoaderCodec = codec;
} else {
RedissonClassLoader cl = new RedissonClassLoader(codec.getClassLoader());
cl.loadClass(params.getClassName(), params.getClassBody());
classLoaderCodec = this.codec.getClass().getConstructor(ClassLoader.class).newInstance(cl);
}
CODECS.put(hash, classLoaderCodec);
}
T task;
if (params.getLambdaBody() != null) {
ByteArrayInputStream is = new ByteArrayInputStream(params.getLambdaBody());
//set thread context class loader to be the classLoaderCodec.getClassLoader() variable as there could be reflection
//done while reading from input stream which reflection will use thread class loader to load classes on demand
ClassLoader currentThreadClassLoader = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(classLoaderCodec.getClassLoader());
ObjectInput oo = new CustomObjectInputStream(classLoaderCodec.getClassLoader(), is);
task = (T) oo.readObject();
oo.close();
} finally {
Thread.currentThread().setContextClassLoader(currentThreadClassLoader);
}
} else {
task = (T) classLoaderCodec.getValueDecoder().decode(stateBuf, null);
}
Injector.inject(task, RedissonClient.class, redisson);
Injector.inject(task, String.class, params.getRequestId());
if (tasksInjector != null) {
tasksInjector.inject(task);
}
return task;
} catch (Exception e) {
throw new IllegalStateException("Unable to initialize codec with ClassLoader parameter", e);
} finally {
classBodyBuf.release();
stateBuf.release();
}
}
public void executeRunnable(TaskParameters params, boolean removeTask) {
try {
if (params.getRequestId() != null && !(params instanceof ScheduledParameters)) {
RFuture future = renewRetryTime(params.getRequestId());
try {
future.toCompletableFuture().get();
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
Runnable runnable = decode(params);
runnable.run();
} catch (RedissonShutdownException e) {
throw e;
} catch (RedisException e) {
finish(params.getRequestId(), removeTask);
throw e;
} catch (ExecutionException e) {
if (e.getCause() instanceof RuntimeException) {
throw (RuntimeException) e.getCause();
} else {
throw new IllegalArgumentException(e.getCause());
}
}
finish(params.getRequestId(), removeTask);
}
@Override
public void executeRunnable(TaskParameters params) {
executeRunnable(params, true);
}
/**
* Check shutdown state. If tasksCounter equals 0
* and executor in shutdown
state, then set terminated
state
* and notify terminationTopicName
*
* If scheduledRequestId
is not null then
* delete scheduled task
*
* @param requestId
*/
void finish(String requestId, boolean removeTask) {
if (Thread.currentThread().isInterrupted()) {
return;
}
String script = "";
if (removeTask) {
script += "local scheduled = redis.call('zscore', KEYS[5], ARGV[3]);"
+ "if scheduled == false then "
+ "redis.call('hdel', KEYS[4], ARGV[3]); "
+ "end;";
}
script += "redis.call('zrem', KEYS[5], 'ff:' .. ARGV[3]);" +
"if redis.call('decr', KEYS[1]) == 0 then "
+ "redis.call('del', KEYS[1]);"
+ "if redis.call('get', KEYS[2]) == ARGV[1] then "
+ "redis.call('del', KEYS[6]);"
+ "redis.call('set', KEYS[2], ARGV[2]);"
+ "redis.call('publish', KEYS[3], ARGV[2]);"
+ "end;"
+ "end;";
RFuture