com.viadeo.kasper.core.component.event.eventbus.EventMessageHandlerITest.java Source code

Java tutorial

Introduction

Here is the source code for com.viadeo.kasper.core.component.event.eventbus.EventMessageHandlerITest.java

Source

// ----------------------------------------------------------------------------
//  This file is part of the Kasper framework.
//
//  The Kasper framework is free software: you can redistribute it and/or 
//  modify it under the terms of the GNU Lesser General Public License as 
//  published by the Free Software Foundation, either version 3 of the 
//  License, or (at your option) any later version.
//
//  Kasper framework is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU Lesser General Public License for more details.
//
//  You should have received a copy of the GNU Lesser General Public License
//  along with the framework Kasper.  
//  If not, see <http://www.gnu.org/licenses/>.
// --
//  Ce fichier fait partie du framework logiciel Kasper
//
//  Ce programme est un logiciel libre ; vous pouvez le redistribuer ou le 
//  modifier suivant les termes de la GNU Lesser General Public License telle 
//  que publie par la Free Software Foundation ; soit la version 3 de la 
//  licence, soit ( votre gr) toute version ultrieure.
//
//  Ce programme est distribu dans l'espoir qu'il sera utile, mais SANS 
//  AUCUNE GARANTIE ; sans mme la garantie tacite de QUALIT MARCHANDE ou 
//  d'ADQUATION  UN BUT PARTICULIER. Consultez la GNU Lesser General Public 
//  License pour plus de dtails.
//
//  Vous devez avoir reu une copie de la GNU Lesser General Public License en 
//  mme temps que ce programme ; si ce n'est pas le cas, consultez 
//  <http://www.gnu.org/licenses>
// ----------------------------------------------------------------------------
// ============================================================================
//                 KASPER - Kasper is the treasure keeper
//    www.viadeo.com - mobile.viadeo.com - api.viadeo.com - dev.viadeo.com
//
//           Viadeo Framework for effective CQRS/DDD architecture
// ============================================================================
package com.viadeo.kasper.core.component.event.eventbus;

import com.codahale.metrics.MetricRegistry;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Function;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.collect.Maps;
import com.jayway.awaitility.Awaitility;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.typesafe.config.Config;
import com.viadeo.kasper.api.component.event.Event;
import com.viadeo.kasper.api.component.event.EventResponse;
import com.viadeo.kasper.api.context.Context;
import com.viadeo.kasper.api.context.ContextHelper;
import com.viadeo.kasper.api.response.CoreReasonCode;
import com.viadeo.kasper.api.response.KasperReason;
import com.viadeo.kasper.common.serde.ObjectMapperProvider;
import com.viadeo.kasper.core.component.event.eventbus.spring.RabbitMQConfiguration;
import com.viadeo.kasper.core.component.event.eventbus.spring.RabbitMQEventBusConfiguration;
import com.viadeo.kasper.core.component.event.listener.AutowiredEventListener;
import com.viadeo.kasper.core.component.event.listener.EventListener;
import com.viadeo.kasper.core.metrics.KasperMetrics;
import com.viadeo.kasper.spring.core.KasperConfiguration;
import com.viadeo.kasper.spring.core.KasperContextConfiguration;
import com.viadeo.kasper.spring.core.KasperIDConfiguration;
import io.github.fallwizard.rabbitmq.mgmt.RabbitMgmtService;
import io.github.fallwizard.rabbitmq.mgmt.model.Queue;
import org.axonframework.domain.EventMessage;
import org.axonframework.domain.GenericEventMessage;
import org.axonframework.serializer.Serializer;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.junit.*;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.springframework.amqp.AmqpIOException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.core.ChannelCallback;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.retry.MessageRecoverer;
import org.springframework.amqp.support.converter.MessageConversionException;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import javax.inject.Inject;
import java.util.Collection;
import java.util.concurrent.Callable;

import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { KasperConfiguration.class, KasperContextConfiguration.class,
        KasperIDConfiguration.class, RabbitMQConfiguration.class, EventMessageHandlerITest.TestConfiguration.class,
        EventMessageHandlerITest.OverrideAmqpClusterConfiguration.class })
@ActiveProfiles(profiles = "rabbitmq")
public class EventMessageHandlerITest {

    public static final int TIMEOUT = 500;

    private static TestEventListener eventListenerWrapper;

    @SuppressWarnings("unchecked")
    private static final AutowiredEventListener<TestEvent> mockedEventListener = mock(AutowiredEventListener.class);

    @Inject
    private AMQPCluster cluster;

    @Inject
    private TestMessageConverter messageConverter;

    @Inject
    private RabbitTemplate rabbitTemplate;

    @Inject
    private AMQPComponentNameFormatter nameFormatter;

    @Inject
    private Config config;

    @Inject
    private RabbitAdmin rabbitAdmin;

    @Inject
    private RabbitMgmtService rabbitMgmtService;

    private String clusterName;
    private String exchangeName;
    private String queueName;
    private String deadLetterQueueName;

    @SuppressWarnings("unchecked")
    @BeforeClass
    public static void init() throws Exception {
        KasperMetrics.setMetricRegistry(new MetricRegistry());
        eventListenerWrapper = new TestEventListener(mockedEventListener);
    }

    @Before
    public void setUp() throws Exception {
        clusterName = config.getString("runtime.eventbus.amqp.clusterName");
        exchangeName = nameFormatter.getFullExchangeName(cluster.getExchangeDescriptor().name,
                cluster.getExchangeDescriptor().version);
        deadLetterQueueName = nameFormatter.getDeadLetterQueueName(exchangeName, clusterName, eventListenerWrapper);
        queueName = nameFormatter.getQueueName(exchangeName, clusterName, eventListenerWrapper);

        try {
            rabbitAdmin.purgeQueue(deadLetterQueueName, false);
            rabbitAdmin.purgeQueue(queueName, false);
        } catch (AmqpIOException e) {
            // happen when the related queues are not existing
        }

        eventListenerWrapper.reset();

        Mockito.reset((AutowiredEventListener) mockedEventListener);
        cluster.subscribe(eventListenerWrapper);
        cluster.start();
    }

    @After
    public void tearDown() throws Exception {
        try {
            cluster.unsubscribe(eventListenerWrapper);
        } catch (Exception e) {
            // happen when we unsubscribe explicitly an event listener in our test
        }
    }

    @Test
    public void handle_withSuccessResponse_isOk() throws InterruptedException {
        // Given
        when(mockedEventListener.handle(any(Context.class), any(TestEvent.class)))
                .thenReturn(EventResponse.success());
        ArgumentCaptor<TestEvent> eventCaptor = ArgumentCaptor.forClass(TestEvent.class);
        TestEvent expectedEvent = new TestEvent();

        // When
        cluster.publish(new GenericEventMessage<>(expectedEvent));

        synchronized (mockedEventListener) {
            mockedEventListener.wait(TIMEOUT);
        }

        // Then
        verify(mockedEventListener).handle(any(Context.class), eventCaptor.capture());
        assertEquals(1, eventCaptor.getAllValues().size());
        assertEquals(expectedEvent, eventCaptor.getValue());
        assertEquals(0, getQueueSize(queueName));
        assertEquals(0, getQueueSize(deadLetterQueueName));
    }

    @SuppressWarnings("deprecation")
    @Test
    public void handle_withIgnoredResponse_isOk() throws InterruptedException {
        // Given
        when(mockedEventListener.handle(any(Context.class), any(TestEvent.class)))
                .thenReturn(EventResponse.ignored());
        ArgumentCaptor<TestEvent> eventCaptor = ArgumentCaptor.forClass(TestEvent.class);
        TestEvent expectedEvent = new TestEvent();

        // When
        cluster.publish(new GenericEventMessage<>(expectedEvent));

        synchronized (mockedEventListener) {
            mockedEventListener.wait(TIMEOUT);
        }

        // Then
        verify(mockedEventListener).handle(any(Context.class), eventCaptor.capture());
        assertEquals(1, eventCaptor.getAllValues().size());
        assertEquals(expectedEvent, eventCaptor.getValue());
        assertEquals(0, getQueueSize(queueName));
        assertEquals(0, getQueueSize(deadLetterQueueName));
    }

    @Ignore("a weak test")
    @Test
    public void handle_withUnexpectedException_shouldRetry5Times_thenPublishInDeadLetter()
            throws InterruptedException {
        // Given
        eventListenerWrapper.setExpectedNumberTimes(5);
        doThrow(new RuntimeException("Bazinga!!")).when(mockedEventListener).handle(any(Context.class),
                any(TestEvent.class));
        ArgumentCaptor<TestEvent> eventCaptor = ArgumentCaptor.forClass(TestEvent.class);
        TestEvent testEvent = new TestEvent();

        // When
        cluster.publish(new GenericEventMessage<>(testEvent));

        synchronized (mockedEventListener) {
            mockedEventListener.wait(TIMEOUT);
        }

        // Then
        verify(mockedEventListener, times(5)).handle(any(Context.class), eventCaptor.capture());
        assertEquals(5, eventCaptor.getAllValues().size());
        for (TestEvent event : eventCaptor.getAllValues()) {
            assertEquals(testEvent, event);
        }

        assertEquals(0, getQueueSize(queueName));
        assertEquals(1, getQueueSize(deadLetterQueueName));

        Message message = rabbitTemplate
                .receive(nameFormatter.getDeadLetterQueueName(exchangeName, clusterName, eventListenerWrapper));
        assertNotNull(message);
        assertEquals(testEvent, ((GenericEventMessage) messageConverter.fromMessage(message)).getPayload());
    }

    @Test
    public void handle_withFailureResponse_shouldRetry5Times_thenPublishInDeadLetter() throws InterruptedException {
        // Given
        eventListenerWrapper.setExpectedNumberTimes(5);
        when(mockedEventListener.handle(any(Context.class), any(TestEvent.class))).thenReturn(EventResponse
                .failure(new KasperReason(CoreReasonCode.INTERNAL_COMPONENT_ERROR, new RuntimeException("npe"))));
        ArgumentCaptor<TestEvent> eventCaptor = ArgumentCaptor.forClass(TestEvent.class);
        TestEvent testEvent = new TestEvent();

        // When
        cluster.publish(new GenericEventMessage<>(testEvent));

        synchronized (mockedEventListener) {
            mockedEventListener.wait(TIMEOUT);
        }

        // Then
        verify(mockedEventListener, times(5)).handle(any(Context.class), eventCaptor.capture());
        assertEquals(5, eventCaptor.getAllValues().size());
        for (TestEvent event : eventCaptor.getAllValues()) {
            assertEquals(testEvent, event);
        }

        Awaitility.await().atMost(TIMEOUT, MILLISECONDS).pollDelay(100, MILLISECONDS)
                .until(new Callable<Boolean>() {
                    @Override
                    public Boolean call() throws Exception {
                        return 0 == getQueueSize(queueName);
                    }
                });

        assertEquals(0, getQueueSize(queueName));
        assertEquals(1, getQueueSize(deadLetterQueueName));

        Message message = rabbitTemplate
                .receive(nameFormatter.getDeadLetterQueueName(exchangeName, clusterName, eventListenerWrapper));
        assertNotNull(message);
        assertEquals(testEvent, ((GenericEventMessage) messageConverter.fromMessage(message)).getPayload());
    }

    @Test
    public void handle_withErrorResponse_isOk() throws InterruptedException {
        // Given
        when(mockedEventListener.handle(any(Context.class), any(TestEvent.class)))
                .thenReturn(EventResponse.error(new KasperReason(CoreReasonCode.INVALID_ID)));
        ArgumentCaptor<TestEvent> eventCaptor = ArgumentCaptor.forClass(TestEvent.class);
        TestEvent expectedEvent = new TestEvent();

        // When
        cluster.publish(new GenericEventMessage<>(expectedEvent));

        synchronized (mockedEventListener) {
            mockedEventListener.wait(TIMEOUT);
        }

        // Then
        verify(mockedEventListener).handle(any(Context.class), eventCaptor.capture());
        assertEquals(1, eventCaptor.getAllValues().size());
        assertEquals(expectedEvent, eventCaptor.getValue());
        assertEquals(0, getQueueSize(queueName));
        assertEquals(0, getQueueSize(deadLetterQueueName));
    }

    @Ignore("a weak test")
    @Test
    public void handle_withTemporarilyUnavailableResponse_shouldRetry5Times_thenRequeue()
            throws InterruptedException {
        // Given
        eventListenerWrapper.setExpectedNumberTimes(5);
        when(mockedEventListener.handle(any(Context.class), any(TestEvent.class))).thenReturn(
                EventResponse.temporarilyUnavailable(new KasperReason(CoreReasonCode.INTERNAL_COMPONENT_TIMEOUT,
                        new RuntimeException("timeout"))));
        ArgumentCaptor<TestEvent> eventCaptor = ArgumentCaptor.forClass(TestEvent.class);
        TestEvent expectedEvent = new TestEvent();

        // When
        cluster.publish(new GenericEventMessage<>(expectedEvent));

        // ...we un-subscribe the event listener in order to not consume the requeued event
        cluster.unsubscribe(eventListenerWrapper);

        synchronized (mockedEventListener) {
            mockedEventListener.wait(TIMEOUT);
        }

        // Then
        verify(mockedEventListener, times(5)).handle(any(Context.class), eventCaptor.capture());
        assertEquals(5, eventCaptor.getAllValues().size());
        for (TestEvent event : eventCaptor.getAllValues()) {
            assertEquals(expectedEvent, event);
        }

        Awaitility.await().atMost(2, SECONDS).pollDelay(200, MILLISECONDS).until(new Callable<Boolean>() {
            @Override
            public Boolean call() throws Exception {
                return 1 == getMessageSize(queueName);
            }
        });

        assertEquals(1, getMessageSize(queueName));
        assertEquals(0, getQueueSize(deadLetterQueueName));

        // Clean
        try {
            rabbitAdmin.declareQueue(new org.springframework.amqp.core.Queue(queueName));
        } catch (AmqpIOException e) {
            // happen when the related queues are not existing
        }
    }

    @Test
    public void handle_withTemporarilyUnavailableResponse_withExpiredMessage_shouldRetry5Times_thenPublishInDeadLetter()
            throws InterruptedException {
        // Given
        eventListenerWrapper.setExpectedNumberTimes(5);
        messageConverter.setExpired();
        when(mockedEventListener.handle(any(Context.class), any(TestEvent.class))).thenReturn(
                EventResponse.temporarilyUnavailable(new KasperReason(CoreReasonCode.INTERNAL_COMPONENT_TIMEOUT,
                        new RuntimeException("timeout"))));
        ArgumentCaptor<TestEvent> eventCaptor = ArgumentCaptor.forClass(TestEvent.class);
        TestEvent expectedEvent = new TestEvent();

        // When
        cluster.publish(new GenericEventMessage<>(expectedEvent));

        synchronized (mockedEventListener) {
            mockedEventListener.wait(TIMEOUT);
        }

        // Then
        verify(mockedEventListener, times(5)).handle(any(Context.class), eventCaptor.capture());
        assertEquals(5, eventCaptor.getAllValues().size());
        for (TestEvent event : eventCaptor.getAllValues()) {
            assertEquals(expectedEvent, event);
        }
        assertEquals(expectedEvent, eventCaptor.getValue());

        Awaitility.await().atMost(TIMEOUT, MILLISECONDS).pollDelay(100, MILLISECONDS)
                .until(new Callable<Boolean>() {
                    @Override
                    public Boolean call() throws Exception {
                        return 0 == getQueueSize(queueName);
                    }
                });

        assertEquals(0, getQueueSize(queueName));
        assertEquals(1, getQueueSize(deadLetterQueueName));
    }

    protected int getQueueSize(final String name) {
        return rabbitAdmin.getRabbitTemplate().execute(new ChannelCallback<AMQP.Queue.DeclareOk>() {
            public AMQP.Queue.DeclareOk doInRabbit(Channel channel) throws Exception {
                return channel.queueDeclarePassive(name);
            }
        }).getMessageCount();
    }

    protected int getMessageSize(final String name) {
        int messageSize = 0;
        Optional<Collection<Queue>> collectionOptional = rabbitMgmtService.queues().allOnVHost("/");
        if (collectionOptional.isPresent()) {
            Queue queue = Maps.uniqueIndex(collectionOptional.get(), new Function<Queue, String>() {
                @Override
                public String apply(Queue input) {
                    return input.getName();
                }
            }).get(name);

            messageSize = (int) queue.getMessagesReady() + (int) queue.getMessagesUnacknowledged();
        }
        return messageSize;
    }

    @Configuration
    public static class OverrideAmqpClusterConfiguration
            extends RabbitMQEventBusConfiguration.AmqpClusterConfiguration {
        @Bean
        public MessageListenerContainerManager messageListenerContainerManager(final Config config,
                final MetricRegistry metricRegistry,
                final MessageListenerContainerFactory messageListenerContainerFactory,
                final MessageConverter messageConverter, final MessageRecoverer messageRecoverer,
                final MessageListenerContainerController messageListenerContainerController) {
            DefaultMessageListenerContainerManager containerManager = new DefaultMessageListenerContainerManager(
                    messageListenerContainerFactory, metricRegistry, messageConverter,
                    messageListenerContainerController) {
                @Override
                protected Object createDelegateMessageListener(MessageConverter messageConverter,
                        EventListener eventListener, MetricRegistry metricRegistry,
                        boolean enabledMessageHandling) {
                    return new EventMessageHandler(messageConverter, eventListener, metricRegistry,
                            messageRecoverer);
                }
            };
            containerManager.setEnabledMessageHandling(config.getBoolean("runtime.eventbus.amqp.enableListeners"));
            return containerManager;
        }

        @Bean
        public TestMessageConverter messageConverter(ContextHelper contextHelper, ObjectMapper objectMapper) {
            return new TestMessageConverter(contextHelper, createEventMessageSerializer(objectMapper));
        }

    }

    @Configuration
    public static class TestConfiguration {
        @Bean
        public MetricRegistry metricRegistry() {
            return new MetricRegistry();
        }

        @Bean
        public ObjectMapper objectMapper() {
            return ObjectMapperProvider.INSTANCE.mapper();
        }
    }

    public static class TestMessageConverter extends EventBusMessageConverter {

        private boolean expired;

        public TestMessageConverter(final ContextHelper contextHelper, final Serializer serializer) {
            super(contextHelper, serializer);
        }

        @Override
        public Message toMessage(Object object, MessageProperties messageProperties)
                throws MessageConversionException {
            final EventMessage eventMessage = (EventMessage) object;
            final DateTime expiredDate = expired ? eventMessage.getTimestamp().minusHours(5)
                    : eventMessage.getTimestamp();
            final GenericEventMessage<Object> expiredEventMessage = new GenericEventMessage<>(
                    eventMessage.getIdentifier(), expiredDate, eventMessage.getPayload(),
                    eventMessage.getMetaData());

            return super.toMessage(expiredEventMessage, messageProperties);
        }

        public void setExpired() {
            expired = true;
        }
    }

    public static class TestEvent implements Event {

        private final DateTime dateTime;

        public TestEvent() {
            this(DateTime.now());
        }

        @JsonCreator
        public TestEvent(@JsonProperty("dateTime") DateTime dateTime) {
            this.dateTime = dateTime.toDateTime(DateTimeZone.UTC);
        }

        public DateTime getDateTime() {
            return dateTime;
        }

        @Override
        public int hashCode() {
            return 31 * super.hashCode() + Objects.hashCode(dateTime);
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null || getClass() != obj.getClass()) {
                return false;
            }
            final TestEvent other = (TestEvent) obj;
            return Objects.equal(this.dateTime, other.dateTime);
        }

        @Override
        public String toString() {
            return Objects.toStringHelper(this).add("dateTime", dateTime).toString();
        }
    }

    public static class TestEventListener extends AutowiredEventListener<TestEvent> {

        private final AutowiredEventListener<TestEvent> eventListener;
        private int expectedNumberTimes;
        private int actualNumberTimes;

        public TestEventListener(AutowiredEventListener<TestEvent> eventListener) {
            this.eventListener = eventListener;
            this.expectedNumberTimes = 1;
            this.actualNumberTimes = 0;
        }

        @Override
        public EventResponse handle(Context context, TestEvent event) {
            EventResponse response;
            actualNumberTimes++;

            if (actualNumberTimes == expectedNumberTimes) {
                response = eventListener.handle(context, event);
                synchronized (eventListener) {
                    eventListener.notify();
                }
            } else {
                response = eventListener.handle(context, event);
            }

            return response;
        }

        public void setExpectedNumberTimes(int expectedNumberTimes) {
            this.expectedNumberTimes = expectedNumberTimes;
        }

        public void reset() {
            this.actualNumberTimes = 0;
        }
    }

}