/**
* Copyright 2016 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 java.io.IOException;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Date;
import java.util.concurrent.Callable;
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.api.annotation.RInject;
import org.redisson.client.RedisException;
import org.redisson.client.codec.Codec;
import org.redisson.client.protocol.RedisCommands;
import org.redisson.command.CommandExecutor;
import org.redisson.misc.Injector;
import org.redisson.remote.RemoteParams;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
/**
* Executor service runs Callable and Runnable tasks.
*
* @author Nikita Koksharov
*
*/
public class RemoteExecutorServiceImpl implements RemoteExecutorService, RemoteParams {
private final ClassLoaderDelegator classLoader = new ClassLoaderDelegator();
private final ThreadLocal<String> requestId = new ThreadLocal<String>();
private final Codec codec;
private final String name;
private final CommandExecutor commandExecutor;
private final RedissonClient redisson;
private String tasksCounterName;
private String statusName;
private String terminationTopicName;
private String schedulerTasksName;
private String schedulerQueueName;
private String schedulerChannelName;
public RemoteExecutorServiceImpl(CommandExecutor commandExecutor, RedissonClient redisson, Codec codec, String name) {
this.commandExecutor = commandExecutor;
this.name = name;
this.redisson = redisson;
try {
this.codec = codec.getClass().getConstructor(ClassLoader.class).newInstance(classLoader);
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
public void setSchedulerQueueName(String schedulerQueueName) {
this.schedulerQueueName = schedulerQueueName;
}
public void setSchedulerChannelName(String schedulerChannelName) {
this.schedulerChannelName = schedulerChannelName;
}
public void setSchedulerTasksName(String schedulerTasksName) {
this.schedulerTasksName = schedulerTasksName;
}
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(String className, byte[] classBody, byte[] state, long startTime, long period) {
long newStartTime = System.currentTimeMillis() + period;
RFuture<Void> future = asyncScheduledServiceAtFixed().scheduleAtFixedRate(className, classBody, state, newStartTime, period);
try {
executeRunnable(className, classBody, state);
} catch (RuntimeException e) {
// cancel task if it throws an exception
future.cancel(true);
throw e;
}
}
@Override
public void schedule(String className, byte[] classBody, byte[] state, long startTime, String cronExpression) {
Date nextStartDate = new CronExpression(cronExpression).getNextValidTimeAfter(new Date());
RFuture<Void> future = asyncScheduledServiceAtFixed().schedule(className, classBody, state, nextStartDate.getTime(), cronExpression);
try {
executeRunnable(className, classBody, state);
} catch (RuntimeException e) {
// cancel task if it throws an exception
future.cancel(true);
throw e;
}
}
/**
* 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() {
ScheduledExecutorRemoteService scheduledRemoteService = new ScheduledExecutorRemoteService(codec, redisson, name, commandExecutor);
scheduledRemoteService.setTerminationTopicName(terminationTopicName);
scheduledRemoteService.setTasksCounterName(tasksCounterName);
scheduledRemoteService.setStatusName(statusName);
scheduledRemoteService.setSchedulerQueueName(schedulerQueueName);
scheduledRemoteService.setSchedulerChannelName(schedulerChannelName);
scheduledRemoteService.setSchedulerTasksName(schedulerTasksName);
scheduledRemoteService.setRequestId(requestId.get());
RemoteExecutorServiceAsync asyncScheduledServiceAtFixed = scheduledRemoteService.get(RemoteExecutorServiceAsync.class, RemoteInvocationOptions.defaults().noAck().noResult());
return asyncScheduledServiceAtFixed;
}
@Override
public void scheduleWithFixedDelay(String className, byte[] classBody, byte[] state, long startTime, long delay) {
executeRunnable(className, classBody, state);
long newStartTime = System.currentTimeMillis() + delay;
asyncScheduledServiceAtFixed().scheduleWithFixedDelay(className, classBody, state, newStartTime, delay);
}
@Override
public Object scheduleCallable(String className, byte[] classBody, byte[] state, long startTime) {
return executeCallable(className, classBody, state, requestId.get());
}
@Override
public void scheduleRunnable(String className, byte[] classBody, byte[] state, long startTime) {
executeRunnable(className, classBody, state, requestId.get());
}
@Override
public Object executeCallable(String className, byte[] classBody, byte[] state) {
return executeCallable(className, classBody, state, null);
}
private Object executeCallable(String className, byte[] classBody, byte[] state, String scheduledRequestId) {
ByteBuf buf = null;
try {
buf = Unpooled.wrappedBuffer(state);
RedissonClassLoader cl = new RedissonClassLoader(getClass().getClassLoader());
cl.loadClass(className, classBody);
classLoader.setCurrentClassLoader(cl);
Callable<?> callable = decode(buf);
return callable.call();
} catch (RedissonShutdownException e) {
return null;
// skip
} catch (RedisException e) {
throw e;
} catch (Exception e) {
throw new IllegalArgumentException(e);
} finally {
buf.release();
finish(scheduledRequestId);
}
}
private <T> T decode(ByteBuf buf) throws IOException {
T task = (T) codec.getValueDecoder().decode(buf, null);
Injector.inject(task, redisson);
return task;
}
@Override
public void executeRunnable(String className, byte[] classBody, byte[] state) {
executeRunnable(className, classBody, state, null);
}
private void executeRunnable(String className, byte[] classBody, byte[] state, String scheduledRequestId) {
ByteBuf buf = null;
try {
buf = Unpooled.wrappedBuffer(state);
RedissonClassLoader cl = new RedissonClassLoader(getClass().getClassLoader());
cl.loadClass(className, classBody);
classLoader.setCurrentClassLoader(cl);
Runnable runnable = decode(buf);
runnable.run();
} catch (RedissonShutdownException e) {
// skip
} catch (RedisException e) {
throw e;
} catch (Exception e) {
throw new IllegalArgumentException(e);
} finally {
buf.release();
finish(scheduledRequestId);
}
}
/**
* Check shutdown state. If tasksCounter equals <code>0</code>
* and executor in <code>shutdown</code> state, then set <code>terminated</code> state
* and notify terminationTopicName
* <p>
* If <code>scheduledRequestId</code> is not null then
* delete scheduled task
*
* @param scheduledRequestId
*/
private void finish(String scheduledRequestId) {
classLoader.clearCurrentClassLoader();
if (scheduledRequestId != null) {
commandExecutor.evalWriteAsync(name, codec, RedisCommands.EVAL_VOID,
"redis.call('hdel', KEYS[4], 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('set', KEYS[2], ARGV[2]);"
+ "redis.call('publish', KEYS[3], ARGV[2]);"
+ "end;"
+ "end;",
Arrays.<Object>asList(tasksCounterName, statusName, terminationTopicName, schedulerTasksName),
RedissonExecutorService.SHUTDOWN_STATE, RedissonExecutorService.TERMINATED_STATE, scheduledRequestId);
return;
}
commandExecutor.evalWriteAsync(name, codec, RedisCommands.EVAL_VOID,
"if redis.call('decr', KEYS[1]) == 0 then "
+ "redis.call('del', KEYS[1]);"
+ "if redis.call('get', KEYS[2]) == ARGV[1] then "
+ "redis.call('set', KEYS[2], ARGV[2]);"
+ "redis.call('publish', KEYS[3], ARGV[2]);"
+ "end;"
+ "end;",
Arrays.<Object>asList(tasksCounterName, statusName, terminationTopicName),
RedissonExecutorService.SHUTDOWN_STATE, RedissonExecutorService.TERMINATED_STATE);
}
@Override
public void setRequestId(String id) {
requestId.set(id);
}
}