/*
* Copyright 2014-2016 the original author or authors.
*
* 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.springframework.integration.redis.inbound;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.BoundListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.integration.channel.MessagePublishingErrorHandler;
import org.springframework.integration.gateway.MessagingGatewaySupport;
import org.springframework.integration.redis.event.RedisExceptionEvent;
import org.springframework.integration.support.channel.BeanFactoryChannelResolver;
import org.springframework.integration.support.management.IntegrationManagedResource;
import org.springframework.integration.util.ErrorHandlingTaskExecutor;
import org.springframework.jmx.export.annotation.ManagedMetric;
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessagingException;
import org.springframework.scheduling.SchedulingAwareRunnable;
import org.springframework.util.Assert;
/**
* @author David Liu
* @author Artem Bilan
* @author Gary Russell
* @since 4.1
*/
@ManagedResource
@IntegrationManagedResource
public class RedisQueueInboundGateway extends MessagingGatewaySupport implements ApplicationEventPublisherAware {
private static final String QUEUE_NAME_SUFFIX = ".reply";
private static final RedisSerializer<String> stringSerializer = new StringRedisSerializer();
public static final long DEFAULT_RECEIVE_TIMEOUT = 1000;
public static final long DEFAULT_RECOVERY_INTERVAL = 5000;
private final RedisTemplate<String, byte[]> template;
private final BoundListOperations<String, byte[]> boundListOperations;
private volatile ApplicationEventPublisher applicationEventPublisher;
private volatile boolean serializerExplicitlySet;
private volatile Executor taskExecutor;
private volatile RedisSerializer<?> serializer = new JdkSerializationRedisSerializer();
private volatile long receiveTimeout = DEFAULT_RECEIVE_TIMEOUT;
private volatile long recoveryInterval = DEFAULT_RECOVERY_INTERVAL;
private volatile boolean active;
private volatile boolean listening;
private volatile boolean extractPayload = true;
private volatile Runnable stopCallback;
/**
* @param queueName Must not be an empty String
* @param connectionFactory Must not be null
*/
public RedisQueueInboundGateway(String queueName, RedisConnectionFactory connectionFactory) {
Assert.hasText(queueName, "'queueName' is required");
Assert.notNull(connectionFactory, "'connectionFactory' must not be null");
this.template = new RedisTemplate<String, byte[]>();
this.template.setConnectionFactory(connectionFactory);
this.template.setEnableDefaultSerializer(false);
this.template.setKeySerializer(new StringRedisSerializer());
this.template.afterPropertiesSet();
this.boundListOperations = this.template.boundListOps(queueName);
}
public void setExtractPayload(boolean extractPayload) {
this.extractPayload = extractPayload;
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
public void setSerializer(RedisSerializer<?> serializer) {
this.serializer = serializer;
this.serializerExplicitlySet = true;
}
/**
* This timeout (milliseconds) is used when retrieving elements from the queue
* specified by {@link #boundListOperations}.
* <p> If the queue does contain elements, the data is retrieved immediately. However,
* if the queue is empty, the Redis connection is blocked until either an element
* can be retrieved from the queue or until the specified timeout passes.
* <p> A timeout of zero can be used to block indefinitely. If not set explicitly
* the timeout value will default to {@code 1000}
* <p> See also: http://redis.io/commands/brpop
* @param receiveTimeout Must be non-negative. Specified in milliseconds.
*/
public void setReceiveTimeout(long receiveTimeout) {
Assert.isTrue(receiveTimeout > 0, "'receiveTimeout' must be > 0.");
this.receiveTimeout = receiveTimeout;
}
public void setTaskExecutor(Executor taskExecutor) {
this.taskExecutor = taskExecutor;
}
public void setRecoveryInterval(long recoveryInterval) {
this.recoveryInterval = recoveryInterval;
}
@Override
protected void onInit() throws Exception {
super.onInit();
if (!this.extractPayload) {
Assert.notNull(this.serializer, "'serializer' has to be provided where 'extractPayload == false'.");
}
if (this.taskExecutor == null) {
String beanName = this.getComponentName();
this.taskExecutor = new SimpleAsyncTaskExecutor((beanName == null ? "" : beanName + "-")
+ this.getComponentType());
}
if (!(this.taskExecutor instanceof ErrorHandlingTaskExecutor) && this.getBeanFactory() != null) {
MessagePublishingErrorHandler errorHandler =
new MessagePublishingErrorHandler(new BeanFactoryChannelResolver(getBeanFactory()));
errorHandler.setDefaultErrorChannel(getErrorChannel());
this.taskExecutor = new ErrorHandlingTaskExecutor(this.taskExecutor, errorHandler);
}
}
@Override
public String getComponentType() {
return "redis:queue-inbound-gateway";
}
private void handlePopException(Exception e) {
this.listening = false;
if (this.active) {
logger.error("Failed to execute listening task. Will attempt to resubmit in " + this.recoveryInterval
+ " milliseconds.", e);
this.publishException(e);
this.sleepBeforeRecoveryAttempt();
}
else {
logger.debug("Failed to execute listening task. " + e.getClass() + ": " + e.getMessage());
}
}
@SuppressWarnings("unchecked")
private void receiveAndReply() {
byte[] value = null;
try {
value = this.boundListOperations.rightPop(this.receiveTimeout, TimeUnit.MILLISECONDS);
}
catch (Exception e) {
handlePopException(e);
return;
}
String uuid = null;
if (value != null) {
if (!this.active) {
this.boundListOperations.rightPush(value);
return;
}
uuid = stringSerializer.deserialize(value);
try {
value = this.template.boundListOps(uuid).rightPop(this.receiveTimeout, TimeUnit.MILLISECONDS);
}
catch (Exception e) {
handlePopException(e);
return;
}
Message<Object> requestMessage = null;
if (value != null) {
if (!this.active) {
this.template.boundListOps(uuid).rightPush(value);
this.boundListOperations.rightPush(stringSerializer.serialize(uuid));
return;
}
if (this.extractPayload) {
Object payload = value;
if (this.serializer != null) {
payload = this.serializer.deserialize(value);
}
requestMessage = this.getMessageBuilderFactory().withPayload(payload).build();
}
else {
try {
requestMessage = (Message<Object>) this.serializer.deserialize(value);
}
catch (Exception e) {
throw new MessagingException("Deserialization of Message failed.", e);
}
}
Message<?> replyMessage = this.sendAndReceiveMessage(requestMessage);
if (replyMessage != null) {
if (this.extractPayload) {
value = extractReplyPayload(replyMessage);
}
else {
if (this.serializer != null) {
value = ((RedisSerializer<Object>) this.serializer).serialize(replyMessage);
}
}
this.template.boundListOps(uuid + QUEUE_NAME_SUFFIX).leftPush(value);
}
}
}
}
@SuppressWarnings("unchecked")
private byte[] extractReplyPayload(Message<?> replyMessage) {
byte[] value;
if (!(replyMessage.getPayload() instanceof byte[])) {
if (replyMessage.getPayload() instanceof String && !this.serializerExplicitlySet) {
value = stringSerializer.serialize((String) replyMessage.getPayload());
}
else {
value = ((RedisSerializer<Object>) this.serializer).serialize(replyMessage.getPayload());
}
}
else {
value = (byte[]) replyMessage.getPayload();
}
return value;
}
@Override
protected void doStart() {
super.doStart();
if (!this.active) {
this.active = true;
this.restart();
}
}
/**
* Sleep according to the specified recovery interval.
* Called between recovery attempts.
*/
private void sleepBeforeRecoveryAttempt() {
if (this.recoveryInterval > 0) {
try {
Thread.sleep(this.recoveryInterval);
}
catch (InterruptedException e) {
logger.debug("Thread interrupted while sleeping the recovery interval");
Thread.currentThread().interrupt();
}
}
}
private void publishException(Exception e) {
if (this.applicationEventPublisher != null) {
this.applicationEventPublisher.publishEvent(new RedisExceptionEvent(this, e));
}
else {
if (logger.isDebugEnabled()) {
logger.debug("No application event publisher for exception: " + e.getMessage());
}
}
}
private void restart() {
this.taskExecutor.execute(new ListenerTask());
}
@Override
protected void doStop(Runnable callback) {
this.stopCallback = callback;
doStop();
}
@Override
protected void doStop() {
super.doStop();
this.active = this.listening = false;
}
public boolean isListening() {
return this.listening;
}
/**
* Returns the size of the Queue specified by {@link #boundListOperations}. The queue is
* represented by a Redis list. If the queue does not exist <code>0</code>
* is returned. See also http://redis.io/commands/llen
* @return Size of the queue. Never negative.
*/
@ManagedMetric
public long getQueueSize() {
return this.boundListOperations.size();
}
/**
* Clear the Redis Queue specified by {@link #boundListOperations}.
*/
@ManagedOperation
public void clearQueue() {
this.boundListOperations.getOperations().delete(this.boundListOperations.getKey());
}
private class ListenerTask implements SchedulingAwareRunnable {
ListenerTask() {
super();
}
@Override
public boolean isLongLived() {
return true;
}
@Override
public void run() {
try {
while (RedisQueueInboundGateway.this.active) {
RedisQueueInboundGateway.this.listening = true;
receiveAndReply();
}
}
finally {
if (RedisQueueInboundGateway.this.active) {
restart();
}
else if (RedisQueueInboundGateway.this.stopCallback != null) {
RedisQueueInboundGateway.this.stopCallback.run();
RedisQueueInboundGateway.this.stopCallback = null;
}
}
}
}
}