/* * Copyright 2013-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.bus.redis; import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.instanceOf; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Properties; import java.util.concurrent.TimeUnit; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.expression.Expression; import org.springframework.integration.channel.DirectChannel; import org.springframework.integration.endpoint.AbstractEndpoint; import org.springframework.integration.redis.inbound.RedisQueueMessageDrivenEndpoint; import org.springframework.integration.test.util.TestUtils; import org.springframework.messaging.support.GenericMessage; import org.springframework.retry.support.RetryTemplate; 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.PartitionCapableBusTests; import org.springframework.xd.dirt.integration.redis.RedisMessageBus; import org.springframework.xd.test.redis.RedisTestSupport; /** * @author Gary Russell */ public class RedisMessageBusTests extends PartitionCapableBusTests { @Rule public RedisTestSupport redisAvailableRule = new RedisTestSupport(); private RedisTemplate<String, Object> redisTemplate; private static final EmbeddedHeadersMessageConverter embeddedHeadersMessageConverter = new EmbeddedHeadersMessageConverter(); @Override protected MessageBus getMessageBus() { if (testMessageBus == null) { testMessageBus = new RedisTestMessageBus(redisAvailableRule.getResource(), getCodec()); } return testMessageBus; } @Override protected boolean usesExplicitRouting() { return true; } @Override public void testSendAndReceivePubSub() throws Exception { TimeUnit.SECONDS.sleep(2); //TODO remove timing issue super.testSendAndReceivePubSub(); } @Before public void setup() { createTemplate().boundListOps("queue.direct.0").trim(1, 0); } @Test public void testConsumerProperties() throws Exception { MessageBus bus = getMessageBus(); Properties properties = new Properties(); properties.put("maxAttempts", "1"); // disable retry bus.bindConsumer("props.0", new DirectChannel(), properties); @SuppressWarnings("unchecked") List<Binding> bindings = TestUtils.getPropertyValue(bus, "messageBus.bindings", List.class); assertEquals(1, bindings.size()); AbstractEndpoint endpoint = bindings.get(0).getEndpoint(); assertThat(endpoint, instanceOf(RedisQueueMessageDrivenEndpoint.class)); assertSame(DirectChannel.class, TestUtils.getPropertyValue(endpoint, "outputChannel").getClass()); bus.unbindConsumers("props.0"); assertEquals(0, bindings.size()); properties.put("backOffInitialInterval", "2000"); properties.put("backOffMaxInterval", "20000"); properties.put("backOffMultiplier", "5.0"); properties.put("concurrency", "2"); properties.put("maxAttempts", "23"); properties.put("partitionIndex", 0); bus.bindConsumer("props.0", new DirectChannel(), properties); assertEquals(1, bindings.size()); endpoint = bindings.get(0).getEndpoint(); verifyConsumer(endpoint); try { bus.bindPubSubConsumer("dummy", null, properties); fail("Expected exception"); } catch (IllegalArgumentException e) { assertThat(e.getMessage(), allOf( containsString("RedisMessageBus does not support consumer properties: "), containsString("partitionIndex"), containsString("concurrency"), containsString(" for dummy."))); } try { bus.bindConsumer("queue:dummy", null, properties); fail("Expected exception"); } catch (IllegalArgumentException e) { assertEquals("RedisMessageBus does not support consumer property: partitionIndex for queue:dummy.", e.getMessage()); } bus.unbindConsumers("props.0"); assertEquals(0, bindings.size()); } @Test public void testProducerProperties() throws Exception { MessageBus bus = getMessageBus(); bus.bindProducer("props.0", new DirectChannel(), null); @SuppressWarnings("unchecked") List<Binding> bindings = TestUtils.getPropertyValue(bus, "messageBus.bindings", List.class); assertEquals(1, bindings.size()); AbstractEndpoint endpoint = bindings.get(0).getEndpoint(); assertEquals( "queue.props.0", TestUtils.getPropertyValue(endpoint, "handler.delegate.queueNameExpression", Expression.class).getExpressionString()); bus.unbindProducers("props.0"); assertEquals(0, bindings.size()); Properties properties = new Properties(); properties.put("partitionKeyExpression", "'foo'"); properties.put("partitionKeyExtractorClass", "foo"); properties.put("partitionSelectorExpression", "0"); properties.put("partitionSelectorClass", "foo"); properties.put(BusProperties.NEXT_MODULE_COUNT, "2"); bus.bindProducer("props.0", new DirectChannel(), properties); assertEquals(1, bindings.size()); endpoint = bindings.get(0).getEndpoint(); assertEquals( "'queue.props.0-' + headers['partition']", TestUtils.getPropertyValue(endpoint, "handler.delegate.queueNameExpression", Expression.class).getExpressionString()); try { bus.bindPubSubProducer("dummy", null, properties); fail("Expected exception"); } catch (IllegalArgumentException e) { assertThat(e.getMessage(), allOf( containsString("RedisMessageBus does not support producer properties: "), containsString("partitionSelectorExpression"), containsString("partitionKeyExtractorClass"), containsString("partitionKeyExpression"), containsString("partitionSelectorClass"))); assertThat(e.getMessage(), containsString("for dummy.")); } try { bus.bindProducer("queue:dummy", new DirectChannel(), properties); fail("Expected exception"); } catch (IllegalArgumentException e) { assertThat(e.getMessage(), allOf( containsString("RedisMessageBus does not support producer properties: "), containsString("partitionSelectorExpression"), containsString("partitionKeyExtractorClass"), containsString("partitionKeyExpression"), containsString("partitionSelectorClass"))); assertThat(e.getMessage(), containsString("for queue:dummy.")); } bus.unbindProducers("props.0"); assertEquals(0, bindings.size()); } @Test public void testRequestReplyRequestorProperties() throws Exception { MessageBus bus = getMessageBus(); Properties properties = new Properties(); properties.put("backOffInitialInterval", "2000"); properties.put("backOffMaxInterval", "20000"); properties.put("backOffMultiplier", "5.0"); properties.put("concurrency", "2"); properties.put("maxAttempts", "23"); bus.bindRequestor("props.0", new DirectChannel(), new DirectChannel(), properties); @SuppressWarnings("unchecked") List<Binding> bindings = TestUtils.getPropertyValue(bus, "messageBus.bindings", List.class); assertEquals(2, bindings.size()); AbstractEndpoint endpoint = bindings.get(0).getEndpoint(); // producer assertEquals( "queue.props.0.requests", TestUtils.getPropertyValue(endpoint, "handler.delegate.queueNameExpression", Expression.class).getExpressionString()); endpoint = bindings.get(1).getEndpoint(); // consumer verifyConsumer(endpoint); properties.put("partitionKeyExpression", "'foo'"); properties.put("partitionKeyExtractorClass", "foo"); properties.put("partitionSelectorExpression", "0"); properties.put("partitionSelectorClass", "foo"); properties.put("partitionIndex", "0"); try { bus.bindRequestor("dummy", new DirectChannel(), new DirectChannel(), properties); fail("Expected exception"); } catch (IllegalArgumentException e) { assertThat(e.getMessage(), allOf( containsString("RedisMessageBus does not support producer properties: "), containsString("partitionSelectorExpression"), containsString("partitionKeyExtractorClass"), containsString("partitionKeyExpression"), containsString("partitionSelectorClass"))); assertThat(e.getMessage(), allOf(containsString("partitionIndex"), containsString("for dummy."))); } bus.unbindConsumers("props.0"); bus.unbindProducers("props.0"); assertEquals(0, bindings.size()); } @Test public void testRequestReplyReplierProperties() throws Exception { MessageBus bus = getMessageBus(); Properties properties = new Properties(); properties.put("backOffInitialInterval", "2000"); properties.put("backOffMaxInterval", "20000"); properties.put("backOffMultiplier", "5.0"); properties.put("concurrency", "2"); properties.put("maxAttempts", "23"); bus.bindReplier("props.0", new DirectChannel(), new DirectChannel(), properties); @SuppressWarnings("unchecked") List<Binding> bindings = TestUtils.getPropertyValue(bus, "messageBus.bindings", List.class); assertEquals(2, bindings.size()); AbstractEndpoint endpoint = bindings.get(1).getEndpoint(); // producer assertEquals( "headers['replyTo']", TestUtils.getPropertyValue(endpoint, "handler.delegate.queueNameExpression", Expression.class).getExpressionString()); endpoint = bindings.get(0).getEndpoint(); // consumer verifyConsumer(endpoint); properties.put("partitionKeyExpression", "'foo'"); properties.put("partitionKeyExtractorClass", "foo"); properties.put("partitionSelectorExpression", "0"); properties.put("partitionSelectorClass", "foo"); properties.put(BusProperties.NEXT_MODULE_COUNT, "1"); properties.put("partitionIndex", "0"); try { bus.bindReplier("dummy", new DirectChannel(), new DirectChannel(), properties); fail("Expected exception"); } catch (IllegalArgumentException e) { assertThat(e.getMessage(), allOf( containsString("RedisMessageBus does not support consumer properties: "), containsString("partitionSelectorExpression"), containsString("partitionKeyExtractorClass"), containsString("partitionKeyExpression"), containsString("partitionSelectorClass"))); assertThat(e.getMessage(), allOf(containsString("partitionIndex"), containsString("for dummy."))); } bus.unbindConsumers("props.0"); bus.unbindProducers("props.0"); assertEquals(0, bindings.size()); } private void verifyConsumer(AbstractEndpoint endpoint) { assertThat(endpoint.getClass().getName(), containsString("CompositeRedisQueueMessageDrivenEndpoint")); assertEquals(2, TestUtils.getPropertyValue(endpoint, "consumers", Collection.class).size()); DirectChannel channel = TestUtils.getPropertyValue( TestUtils.getPropertyValue(endpoint, "consumers", List.class).get(0), "outputChannel", DirectChannel.class); assertThat( channel.getClass().getName(), containsString("RedisMessageBus$")); // retry wrapper assertThat( TestUtils.getPropertyValue(TestUtils.getPropertyValue(endpoint, "consumers", List.class).get(1), "outputChannel").getClass().getName(), containsString("RedisMessageBus$")); // retry wrapper RetryTemplate retry = TestUtils.getPropertyValue(channel, "val$retryTemplate", RetryTemplate.class); assertEquals(23, TestUtils.getPropertyValue(retry, "retryPolicy.maxAttempts")); assertEquals(2000L, TestUtils.getPropertyValue(retry, "backOffPolicy.initialInterval")); assertEquals(20000L, TestUtils.getPropertyValue(retry, "backOffPolicy.maxInterval")); assertEquals(5.0, TestUtils.getPropertyValue(retry, "backOffPolicy.multiplier")); } @Test public void testRetryFail() { MessageBus bus = getMessageBus(); DirectChannel channel = new DirectChannel(); bus.bindProducer("retry.0", channel, null); Properties props = new Properties(); props.put("maxAttempts", 2); props.put("backOffInitialInterval", 100); props.put("backOffMultiplier", "1.0"); bus.bindConsumer("retry.0", new DirectChannel(), props); // no subscriber channel.send(new GenericMessage<String>("foo")); RedisTemplate<String, Object> template = createTemplate(); Object rightPop = template.boundListOps("ERRORS:retry.0").rightPop(5, TimeUnit.SECONDS); assertNotNull(rightPop); assertThat(new String((byte[]) rightPop), containsString("foo")); } @Test public void testMoreHeaders() { RedisMessageBus bus = new RedisMessageBus(mock(RedisConnectionFactory.class), getCodec(), "foo", "bar"); Collection<String> headers = Arrays.asList(TestUtils.getPropertyValue(bus, "headersToMap", String[].class)); assertEquals(10, headers.size()); assertTrue(headers.contains("foo")); assertTrue(headers.contains("bar")); } private RedisTemplate<String, Object> createTemplate() { if (this.redisTemplate != null) { return this.redisTemplate; } RedisTemplate<String, Object> template = new RedisTemplate<String, Object>(); template.setConnectionFactory(this.redisAvailableRule.getResource()); template.setKeySerializer(new StringRedisSerializer()); template.setEnableDefaultSerializer(false); template.afterPropertiesSet(); this.redisTemplate = template; return template; } @Override protected String getEndpointRouting(AbstractEndpoint endpoint) { return TestUtils.getPropertyValue(endpoint, "handler.delegate.queueNameExpression", Expression.class).getExpressionString(); } @Override protected String getPubSubEndpointRouting(AbstractEndpoint endpoint) { return TestUtils.getPropertyValue(endpoint, "handler.delegate.topicExpression", Expression.class).getExpressionString(); } @Override public Spy spyOn(final String queue) { final RedisTemplate<String, Object> template = createTemplate(); return new Spy() { @Override public Object receive(boolean expectNull) throws Exception { byte[] bytes = (byte[]) template.boundListOps("queue." + queue).rightPop(50, TimeUnit.MILLISECONDS); if (bytes == null) { return null; } bytes = (byte[]) embeddedHeadersMessageConverter.extractHeaders(new GenericMessage<byte[]>(bytes), false).getPayload(); return new String(bytes, "UTF-8"); } }; } @Override protected void busBindUnbindLatency() throws InterruptedException { Thread.sleep(3000); // needed for Redis see INT-3442 } }