/* * Copyright 2014-2015 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.xd.dirt.integration.redis; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Properties; import java.util.Set; import org.springframework.beans.factory.DisposableBean; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.expression.Expression; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.integration.channel.DirectChannel; import org.springframework.integration.codec.Codec; import org.springframework.integration.endpoint.EventDrivenConsumer; import org.springframework.integration.endpoint.MessageProducerSupport; import org.springframework.integration.handler.AbstractMessageHandler; import org.springframework.integration.handler.AbstractReplyProducingMessageHandler; import org.springframework.integration.redis.inbound.RedisInboundChannelAdapter; import org.springframework.integration.redis.inbound.RedisQueueMessageDrivenEndpoint; import org.springframework.integration.redis.outbound.RedisPublishingMessageHandler; import org.springframework.integration.redis.outbound.RedisQueueOutboundChannelAdapter; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageHandler; import org.springframework.messaging.SubscribableChannel; import org.springframework.messaging.support.MessageBuilder; import org.springframework.retry.RecoveryCallback; import org.springframework.retry.RetryCallback; import org.springframework.retry.RetryContext; import org.springframework.retry.support.RetryTemplate; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.xd.dirt.integration.bus.AbstractBusPropertiesAccessor; import org.springframework.xd.dirt.integration.bus.Binding; import org.springframework.xd.dirt.integration.bus.BusProperties; import org.springframework.xd.dirt.integration.bus.EmbeddedHeadersMessageConverter; import org.springframework.xd.dirt.integration.bus.MessageBus; import org.springframework.xd.dirt.integration.bus.MessageBusSupport; import org.springframework.xd.dirt.integration.bus.MessageValues; import org.springframework.xd.dirt.integration.bus.XdHeaders; /** * A {@link MessageBus} implementation backed by Redis. * @author Mark Fisher * @author Gary Russell * @author David Turanski * @author Jennifer Hickey */ public class RedisMessageBus extends MessageBusSupport implements DisposableBean { private static final String ERROR_HEADER = "errorKey"; private static final SpelExpressionParser parser = new SpelExpressionParser(); private final String[] headersToMap; /** * Retry only. */ private static final Set<Object> SUPPORTED_PUBSUB_CONSUMER_PROPERTIES = new SetBuilder() .addAll(CONSUMER_STANDARD_PROPERTIES) .addAll(CONSUMER_RETRY_PROPERTIES) .build(); /** * Retry + concurrency. */ private static final Set<Object> SUPPORTED_NAMED_CONSUMER_PROPERTIES = new SetBuilder() .addAll(CONSUMER_STANDARD_PROPERTIES) .addAll(CONSUMER_RETRY_PROPERTIES) .add(BusProperties.CONCURRENCY) .build(); /** * Named + partitioning. */ private static final Set<Object> SUPPORTED_CONSUMER_PROPERTIES = new SetBuilder() .addAll(SUPPORTED_NAMED_CONSUMER_PROPERTIES) .add(BusProperties.PARTITION_INDEX) .build(); /** * Retry + concurrency (request). */ private static final Set<Object> SUPPORTED_REPLYING_CONSUMER_PROPERTIES = new SetBuilder() // request .addAll(CONSUMER_STANDARD_PROPERTIES) .addAll(CONSUMER_RETRY_PROPERTIES) .add(BusProperties.CONCURRENCY) .build(); /** * None. */ private static final Set<Object> SUPPORTED_PUBSUB_PRODUCER_PROPERTIES = PRODUCER_STANDARD_PROPERTIES; /** * None. */ private static final Set<Object> SUPPORTED_NAMED_PRODUCER_PROPERTIES = PRODUCER_STANDARD_PROPERTIES; /** * Partitioning. */ private static final Set<Object> SUPPORTED_PRODUCER_PROPERTIES = new SetBuilder() .addAll(PRODUCER_PARTITIONING_PROPERTIES) .addAll(PRODUCER_STANDARD_PROPERTIES) .add(BusProperties.DIRECT_BINDING_ALLOWED) .build(); /** * Retry, concurrency (reply). */ private static final Set<Object> SUPPORTED_REQUESTING_PRODUCER_PROPERTIES = new SetBuilder() // reply .addAll(CONSUMER_RETRY_PROPERTIES) .add(BusProperties.CONCURRENCY) .build(); private final RedisConnectionFactory connectionFactory; private final EmbeddedHeadersMessageConverter embeddedHeadersMessageConverter = new EmbeddedHeadersMessageConverter(); private final RedisQueueOutboundChannelAdapter errorAdapter; public RedisMessageBus(RedisConnectionFactory connectionFactory, Codec codec) { this(connectionFactory, codec, new String[0]); } public RedisMessageBus(RedisConnectionFactory connectionFactory, Codec codec, String... headersToMap) { Assert.notNull(connectionFactory, "connectionFactory must not be null"); Assert.notNull(codec, "codec must not be null"); this.connectionFactory = connectionFactory; setCodec(codec); this.errorAdapter = new RedisQueueOutboundChannelAdapter( parser.parseExpression("headers['" + ERROR_HEADER + "']"), connectionFactory); if (headersToMap != null && headersToMap.length > 0) { String[] combinedHeadersToMap = Arrays.copyOfRange(XdHeaders.STANDARD_HEADERS, 0, XdHeaders.STANDARD_HEADERS.length + headersToMap.length); System.arraycopy(headersToMap, 0, combinedHeadersToMap, XdHeaders.STANDARD_HEADERS.length, headersToMap.length); this.headersToMap = combinedHeadersToMap; } else { this.headersToMap = XdHeaders.STANDARD_HEADERS; } } @Override protected void onInit() { this.errorAdapter.setIntegrationEvaluationContext(this.evaluationContext); } @Override public void bindConsumer(final String name, MessageChannel moduleInputChannel, Properties properties) { if (name.startsWith(P2P_NAMED_CHANNEL_TYPE_PREFIX)) { validateConsumerProperties(name, properties, SUPPORTED_NAMED_CONSUMER_PROPERTIES); } else { validateConsumerProperties(name, properties, SUPPORTED_CONSUMER_PROPERTIES); } RedisPropertiesAccessor accessor = new RedisPropertiesAccessor(properties); String queueName = "queue." + name; int partitionIndex = accessor.getPartitionIndex(); if (partitionIndex >= 0) { queueName += "-" + partitionIndex; } MessageProducerSupport adapter = createInboundAdapter(accessor, queueName); doRegisterConsumer(name, name + (partitionIndex >= 0 ? "-" + partitionIndex : ""), moduleInputChannel, adapter, accessor); bindExistingProducerDirectlyIfPossible(name, moduleInputChannel); } private MessageProducerSupport createInboundAdapter(RedisPropertiesAccessor accessor, String queueName) { MessageProducerSupport adapter; int concurrency = accessor.getConcurrency(this.defaultConcurrency); concurrency = concurrency > 0 ? concurrency : 1; if (concurrency == 1) { RedisQueueMessageDrivenEndpoint single = new RedisQueueMessageDrivenEndpoint(queueName, this.connectionFactory); single.setBeanFactory(getBeanFactory()); single.setSerializer(null); adapter = single; } else { adapter = new CompositeRedisQueueMessageDrivenEndpoint(queueName, concurrency); } return adapter; } @Override public void bindPubSubConsumer(final String name, MessageChannel moduleInputChannel, Properties properties) { if (logger.isInfoEnabled()) { logger.info("declaring pubsub for inbound: " + name); } validateConsumerProperties(name, properties, SUPPORTED_PUBSUB_CONSUMER_PROPERTIES); RedisInboundChannelAdapter adapter = new RedisInboundChannelAdapter(this.connectionFactory); adapter.setBeanFactory(this.getBeanFactory()); adapter.setSerializer(null); adapter.setTopics(applyPubSub(name)); doRegisterConsumer(name, name, moduleInputChannel, adapter, new RedisPropertiesAccessor(properties)); } private void doRegisterConsumer(String bindingName, String channelName, MessageChannel moduleInputChannel, MessageProducerSupport adapter, RedisPropertiesAccessor properties) { DirectChannel bridgeToModuleChannel = new DirectChannel(); bridgeToModuleChannel.setBeanFactory(this.getBeanFactory()); bridgeToModuleChannel.setBeanName(channelName + ".bridge"); MessageChannel bridgeInputChannel = addRetryIfNeeded(channelName, bridgeToModuleChannel, properties); adapter.setOutputChannel(bridgeInputChannel); adapter.setBeanName("inbound." + bindingName); adapter.afterPropertiesSet(); Binding consumerBinding = Binding.forConsumer(bindingName, adapter, moduleInputChannel, properties); addBinding(consumerBinding); ReceivingHandler convertingBridge = new ReceivingHandler(); convertingBridge.setOutputChannel(moduleInputChannel); convertingBridge.setBeanName(channelName + ".bridge.handler"); convertingBridge.afterPropertiesSet(); bridgeToModuleChannel.subscribe(convertingBridge); consumerBinding.start(); } /** * If retry is enabled, wrap the bridge channel in another that will invoke send() within the scope of a retry * template. * @param name The name. * @param bridgeToModuleChannel The channel. * @param properties The properties. * @return The channel, or a wrapper. */ private MessageChannel addRetryIfNeeded(final String name, final DirectChannel bridgeToModuleChannel, RedisPropertiesAccessor properties) { final RetryTemplate retryTemplate = buildRetryTemplateIfRetryEnabled(properties); if (retryTemplate == null) { return bridgeToModuleChannel; } else { DirectChannel channel = new DirectChannel() { @Override protected boolean doSend(final Message<?> message, final long timeout) { try { return retryTemplate.execute(new RetryCallback<Boolean, Exception>() { @Override public Boolean doWithRetry(RetryContext context) throws Exception { return bridgeToModuleChannel.send(message, timeout); } }, new RecoveryCallback<Boolean>() { /** * Send the failed message to 'ERRORS:[name]'. */ @Override public Boolean recover(RetryContext context) throws Exception { logger.error( "Failed to deliver message; retries exhausted; message sent to queue 'ERRORS:" + name + "' " + context.getLastThrowable()); errorAdapter.handleMessage(getMessageBuilderFactory().fromMessage(message) .setHeader(ERROR_HEADER, "ERRORS:" + name) .build()); return true; } }); } catch (Exception e) { logger.error("Failed to deliver message", e); return false; } } }; channel.setBeanName(name + ".bridge"); return channel; } } @Override public void bindProducer(final String name, MessageChannel moduleOutputChannel, Properties properties) { Assert.isInstanceOf(SubscribableChannel.class, moduleOutputChannel); if (name.startsWith(P2P_NAMED_CHANNEL_TYPE_PREFIX)) { validateProducerProperties(name, properties, SUPPORTED_NAMED_PRODUCER_PROPERTIES); } else { validateProducerProperties(name, properties, SUPPORTED_PRODUCER_PROPERTIES); } RedisPropertiesAccessor accessor = new RedisPropertiesAccessor(properties); if (!bindNewProducerDirectlyIfPossible(name, (SubscribableChannel) moduleOutputChannel, accessor)) { String partitionKeyExtractorClass = accessor.getPartitionKeyExtractorClass(); Expression partitionKeyExpression = accessor.getPartitionKeyExpression(); RedisQueueOutboundChannelAdapter queue; String queueName = "queue." + name; if (partitionKeyExpression == null && !StringUtils.hasText(partitionKeyExtractorClass)) { queue = new RedisQueueOutboundChannelAdapter(queueName, this.connectionFactory); } else { queue = new RedisQueueOutboundChannelAdapter( parser.parseExpression(buildPartitionRoutingExpression(queueName)), this.connectionFactory); } queue.setIntegrationEvaluationContext(this.evaluationContext); queue.setBeanFactory(this.getBeanFactory()); queue.afterPropertiesSet(); doRegisterProducer(name, moduleOutputChannel, queue, accessor); } } @Override public void bindPubSubProducer(final String name, MessageChannel moduleOutputChannel, Properties properties) { validateProducerProperties(name, properties, SUPPORTED_PUBSUB_PRODUCER_PROPERTIES); RedisPublishingMessageHandler topic = new RedisPublishingMessageHandler(connectionFactory); topic.setBeanFactory(this.getBeanFactory()); topic.setTopic(applyPubSub(name)); topic.afterPropertiesSet(); doRegisterProducer(name, moduleOutputChannel, topic, new RedisPropertiesAccessor(properties)); } private void doRegisterProducer(final String name, MessageChannel moduleOutputChannel, MessageHandler delegate, RedisPropertiesAccessor properties) { this.doRegisterProducer(name, moduleOutputChannel, delegate, null, properties); } private void doRegisterProducer(final String name, MessageChannel moduleOutputChannel, MessageHandler delegate, String replyTo, RedisPropertiesAccessor properties) { Assert.isInstanceOf(SubscribableChannel.class, moduleOutputChannel); MessageHandler handler = new SendingHandler(delegate, replyTo, properties); EventDrivenConsumer consumer = new EventDrivenConsumer((SubscribableChannel) moduleOutputChannel, handler); consumer.setBeanFactory(this.getBeanFactory()); consumer.setBeanName("outbound." + name); consumer.afterPropertiesSet(); Binding producerBinding = Binding.forProducer(name, moduleOutputChannel, consumer, properties); addBinding(producerBinding); producerBinding.start(); } @Override public void bindRequestor(String name, MessageChannel requests, MessageChannel replies, Properties properties) { if (logger.isInfoEnabled()) { logger.info("binding requestor: " + name); } Assert.isInstanceOf(SubscribableChannel.class, requests); validateProducerProperties(name, properties, SUPPORTED_REQUESTING_PRODUCER_PROPERTIES); RedisQueueOutboundChannelAdapter queue = new RedisQueueOutboundChannelAdapter("queue." + applyRequests(name), this.connectionFactory); queue.setBeanFactory(this.getBeanFactory()); queue.afterPropertiesSet(); String replyQueueName = name + ".replies." + this.getIdGenerator().generateId(); RedisPropertiesAccessor accessor = new RedisPropertiesAccessor(properties); this.doRegisterProducer(name, requests, queue, replyQueueName, accessor); MessageProducerSupport adapter = createInboundAdapter(accessor, replyQueueName); this.doRegisterConsumer(name, name, replies, adapter, accessor); } @Override public void bindReplier(String name, MessageChannel requests, MessageChannel replies, Properties properties) { if (logger.isInfoEnabled()) { logger.info("binding replier: " + name); } validateConsumerProperties(name, properties, SUPPORTED_REPLYING_CONSUMER_PROPERTIES); RedisPropertiesAccessor accessor = new RedisPropertiesAccessor(properties); MessageProducerSupport adapter = createInboundAdapter(accessor, "queue." + applyRequests(name)); this.doRegisterConsumer(name, name, requests, adapter, accessor); RedisQueueOutboundChannelAdapter replyQueue = new RedisQueueOutboundChannelAdapter( RedisMessageBus.parser.parseExpression("headers['" + XdHeaders.REPLY_TO + "']"), this.connectionFactory); replyQueue.setBeanFactory(this.getBeanFactory()); replyQueue.setIntegrationEvaluationContext(this.evaluationContext); replyQueue.afterPropertiesSet(); this.doRegisterProducer(name, replies, replyQueue, accessor); } @Override public void destroy() { stopBindings(); } private class SendingHandler extends AbstractMessageHandler { private final MessageHandler delegate; private final String replyTo; private final PartitioningMetadata partitioningMetadata; private SendingHandler(MessageHandler delegate, String replyTo, RedisPropertiesAccessor properties) { this.delegate = delegate; this.replyTo = replyTo; this.partitioningMetadata = new PartitioningMetadata(properties, properties.getNextModuleCount()); this.setBeanFactory(RedisMessageBus.this.getBeanFactory()); } @Override protected void handleMessageInternal(Message<?> message) throws Exception { MessageValues transformed = serializePayloadIfNecessary(message); if (replyTo != null) { transformed.put(XdHeaders.REPLY_TO, this.replyTo); } if (this.partitioningMetadata.isPartitionedModule()) { transformed.put(PARTITION_HEADER, determinePartition(message, this.partitioningMetadata)); } byte[] messageToSend = embeddedHeadersMessageConverter.embedHeaders(transformed, RedisMessageBus.this.headersToMap); delegate.handleMessage(MessageBuilder.withPayload(messageToSend).copyHeaders(transformed).build()); } } private class ReceivingHandler extends AbstractReplyProducingMessageHandler { public ReceivingHandler() { super(); this.setBeanFactory(RedisMessageBus.this.getBeanFactory()); } @SuppressWarnings("unchecked") @Override protected Object handleRequestMessage(Message<?> requestMessage) { MessageValues theRequestMessage; try { theRequestMessage = embeddedHeadersMessageConverter.extractHeaders((Message<byte[]>) requestMessage, true); } catch (Exception e) { logger.error(EmbeddedHeadersMessageConverter.decodeExceptionMessage(requestMessage), e); theRequestMessage = new MessageValues(requestMessage); } return deserializePayloadIfNecessary(theRequestMessage).toMessage(getMessageBuilderFactory()); } @Override protected boolean shouldCopyRequestHeaders() { // prevent returned message from being copied in superclass return false; } } private static class RedisPropertiesAccessor extends AbstractBusPropertiesAccessor { public RedisPropertiesAccessor(Properties properties) { super(properties); } } /** * Provides concurrency by creating a list of message-driven endpoints. */ private class CompositeRedisQueueMessageDrivenEndpoint extends MessageProducerSupport { private final List<RedisQueueMessageDrivenEndpoint> consumers = new ArrayList<RedisQueueMessageDrivenEndpoint>(); public CompositeRedisQueueMessageDrivenEndpoint(String queueName, int concurrency) { for (int i = 0; i < concurrency; i++) { RedisQueueMessageDrivenEndpoint adapter = new RedisQueueMessageDrivenEndpoint(queueName, connectionFactory); adapter.setBeanFactory(RedisMessageBus.this.getBeanFactory()); adapter.setSerializer(null); adapter.setBeanName("inbound." + queueName + "." + i); this.consumers.add(adapter); } this.setBeanFactory(RedisMessageBus.this.getBeanFactory()); } @Override protected void onInit() { for (RedisQueueMessageDrivenEndpoint consumer : consumers) { consumer.afterPropertiesSet(); } } @Override protected void doStart() { for (RedisQueueMessageDrivenEndpoint consumer : consumers) { consumer.start(); } } @Override protected void doStop() { for (RedisQueueMessageDrivenEndpoint consumer : consumers) { consumer.stop(); } } @Override public void setOutputChannel(MessageChannel outputChannel) { for (RedisQueueMessageDrivenEndpoint consumer : consumers) { consumer.setOutputChannel(outputChannel); } } @Override public void setErrorChannel(MessageChannel errorChannel) { for (RedisQueueMessageDrivenEndpoint consumer : consumers) { consumer.setErrorChannel(errorChannel); } } } }