diff --git a/.travis.yml b/.travis.yml index 41380ead..0d51f23c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,15 +3,18 @@ jdk: - oraclejdk8 services: - rabbitmq +- docker +before_install: + - docker pull webcenter/activemq:latest + - docker run -d --name='activemq' -p 8161:8161 -p 61616:61616 -p 61613:61613 webcenter/activemq:latest + - docker ps -a + - docker inspect activemq install: '' script: - mvn clean deploy --settings settings.xml -B -V -- mvn -Dtest=io.github.tcdl.msb.acceptance.* -DfailIfNoTests=false test -- mvn -Dtest=io.github.tcdl.msb.acceptance.bdd.* -DfailIfNoTests=false test after_success: - mvn clean test jacoco:report coveralls:report env: global: - - secure: QRvQihs4BrO9UloFkafR0sUKkGI1pD2CY50SqN7mC+1iIkkrYRNpJpaUhxsZXTJFvo6sf9tOiZpe7mokAfFT72VLDpTCXCnN8GzIS0ozPfh7CjE/CdhfjZahxdMsz5x8Imsuo0kbB31r5j3uwJdc+zLr9lW5NRY5mpQbsZJIL+s= - - secure: j6r6rhtdRGVDVQf3CBCsB9Hrx/lpW6SRBznpryarkQS7e5gNy/Ftp+2bwxjYSvXTVovm1lSAOfLE3/EiQdo6BVgzSMftExR5R8pD20zNzUqje+wC9ANW0K9GfyeimFPR/I106tmUlMI/LiaC7xVt0dQlvlruMEejqwz1tLTMAw8= - - secure: kIjP9JhBP19mOgfvPJA0WbLhzr69/WV1gpxpFYCrfmyBQI6vFWnnsYrUrkLka9APpBUh3sgYARO71xUpFkmDoXadRHrpOi0FESDVNsRG5oKcBaCO3yWPALeptA+mqu6ofIHFMmjGU8BvHO7TsyI9bpjrYcSGC1xLe5ADbDZNESY= + - secure: LIfUuBznv98TeC2mzZG6beSXgoyAtAnVdEkj+HfLR2Dm5wmqAC/LujAtFcqEL79c8+48/g4K3sM8OoUWyELdS8G9uYGgh6BLxpI+jAjpUoYGSlzfpM5Q6V5FG6oixkzBMMGV2tNDvH+0IcrhR7uPN1pMz8vvS01AqOlWXAD83Ms= + - secure: CBVCm/P1QMFd4B6jqdfJV5cwYeubIMcMyppS8PS//QL3OzaC9x+h5wWf2Rv7x4PItCOHPEy7kavIBJRAEel8H5egVVydkoNSvGglqBO5uQQvojl89VJGAfCp/7Evv9PP8Z9YhCIWPzQbr0kuL9+6amkUcIRxOLgcvQxrDvubqw8= diff --git a/README.md b/README.md index 9ecf899b..f4eef515 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,6 @@ For this you'll need to add a `server` to the `servers` section of your settings [YOUR_API_TOKEN] ``` - ## Bintray / jcenter SNAPSHOT publishing configuration: If you're part of the tcdl bintray organization (https://bintray.com/tcdl) and have sufficient rights you can publish snapshots to jfrog / jcenter (http://oss.jfrog.org/artifactory/simple/oss-snapshot-local/io/github/tcdl/). diff --git a/acceptance/pom.xml b/acceptance/pom.xml index fdcc302e..c5296d62 100644 --- a/acceptance/pom.xml +++ b/acceptance/pom.xml @@ -3,7 +3,7 @@ io.github.tcdl.msb msb-java - 1.3.0-SNAPSHOT + 1.6.7-SNAPSHOT ../pom.xml 4.0.0 @@ -21,7 +21,6 @@ tcdl https://github.com/tcdl - io.github.tcdl.msb @@ -34,14 +33,9 @@ io.github.tcdl.msb msb-java-examples - - - org.jbehave - jbehave-core test - @@ -57,5 +51,25 @@ - + + + IntegrationTesting + + true + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + **/*Runner.class + + + + + + + \ No newline at end of file diff --git a/acceptance/src/main/java/io/github/tcdl/msb/acceptance/MsbTestHelper.java b/acceptance/src/main/java/io/github/tcdl/msb/acceptance/MsbTestHelper.java index 54e83972..104ac039 100644 --- a/acceptance/src/main/java/io/github/tcdl/msb/acceptance/MsbTestHelper.java +++ b/acceptance/src/main/java/io/github/tcdl/msb/acceptance/MsbTestHelper.java @@ -2,13 +2,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.typesafe.config.Config; -import io.github.tcdl.msb.api.Callback; -import io.github.tcdl.msb.api.MessageTemplate; -import io.github.tcdl.msb.api.MsbContext; -import io.github.tcdl.msb.api.MsbContextBuilder; -import io.github.tcdl.msb.api.RequestOptions; -import io.github.tcdl.msb.api.Requester; -import io.github.tcdl.msb.api.ResponderServer; +import com.typesafe.config.ConfigValueFactory; +import io.github.tcdl.msb.api.*; import io.github.tcdl.msb.api.message.Acknowledge; import io.github.tcdl.msb.api.message.payload.RestPayload; import io.github.tcdl.msb.impl.MsbContextImpl; @@ -16,6 +11,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.UUID; /** * Utility to simplify using requester and responder server @@ -38,19 +34,35 @@ public static MsbTestHelper getInstance() { return instance; } - public void initDefault() { - contextMap.put(DEFAULT_CONTEXT_NAME, new MsbContextBuilder() - .enableShutdownHook(true).build()); + public MsbContext initDefault() { + return contextMap.put(DEFAULT_CONTEXT_NAME, new MsbContextBuilder() + .build()); } - public void initWithConfig(Config config) { - initWithConfig(DEFAULT_CONTEXT_NAME, config); + public MsbContext initWithConfig(Config config) { + return initWithConfig(DEFAULT_CONTEXT_NAME, config); } - public void initWithConfig(String contextName, Config config) { - contextMap.put(contextName, new MsbContextBuilder() - .withConfig(config) - .enableShutdownHook(true).build()); + public MsbContext initWithConfig(String contextName, Config config) { + MsbContext context = new MsbContextBuilder().withConfig(config).build(); + contextMap.put(contextName, context); + return context; + } + + /** + * Sets exchanges to be non-durable and queues to be temporary. This reduces the chances for tests to affect each other. + */ + public static Config temporaryInfrastructure(Config config){ + return config.withValue("msbConfig.brokerConfig.durable", ConfigValueFactory.fromAnyRef(false)); + } + + /** + * Creates MsbContext that will create unique (from context to context) queue names. + */ + public MsbContext initDistinctContext(Config baseConfig) { + String uuid = UUID.randomUUID().toString(); + Config config = baseConfig.withValue("msbConfig.serviceDetails.name", ConfigValueFactory.fromAnyRef(uuid)); + return initWithConfig(uuid, config); } public MsbContext getContext(String contextName) { @@ -65,25 +77,22 @@ public ObjectMapper getPayloadMapper(String contextName) { return ((MsbContextImpl) getContext(contextName)).getPayloadMapper(); } - public ObjectMapper getPayloadMapper() { - return getPayloadMapper(DEFAULT_CONTEXT_NAME); - } - public Requester createRequester(String namespace, Integer numberOfResponses, Class responsePayloadClass) { - return createRequester(DEFAULT_CONTEXT_NAME, namespace, numberOfResponses, null, null, responsePayloadClass); + return createRequester(DEFAULT_CONTEXT_NAME, namespace, null, numberOfResponses, null, null, responsePayloadClass); } - public Requester createRequester(String contextName, String namespace, Integer numberOfResponses, Class responsePayloadClass) { - return createRequester(contextName, namespace, numberOfResponses, null, null, responsePayloadClass); + public Requester createRequester(String contextName, String namespace, String forwardNamespace, Integer numberOfResponses, Class responsePayloadClass) { + return createRequester(contextName, namespace, forwardNamespace, numberOfResponses, null, null, responsePayloadClass); } public Requester createRequester(String namespace, Integer numberOfResponses, Integer ackTimeout, Integer responseTimeout, Class responsePayloadClass) { - return createRequester(DEFAULT_CONTEXT_NAME, namespace, numberOfResponses, ackTimeout, responseTimeout, responsePayloadClass); + return createRequester(DEFAULT_CONTEXT_NAME, namespace, null, numberOfResponses, ackTimeout, responseTimeout, responsePayloadClass); } - public Requester createRequester(String contextName, String namespace, Integer numberOfResponses, Integer ackTimeout, Integer responseTimeout, Class responsePayloadClass) { + public Requester createRequester(String contextName, String namespace, String forwardNamespace, Integer numberOfResponses, Integer ackTimeout, Integer responseTimeout, Class responsePayloadClass) { RequestOptions options = new RequestOptions.Builder() .withWaitForResponses(numberOfResponses) + .withForwardNamespace(forwardNamespace) .withAckTimeout(Utils.ifNull(ackTimeout, 5000)) .withResponseTimeout(Utils.ifNull(responseTimeout, 15000)) .build(); @@ -91,42 +100,54 @@ public Requester createRequester(String contextName, String namespace, In } public void sendRequest(Requester requester, Object payload, Integer waitForResponses, Callback responseCallback) throws Exception { - sendRequest(requester, payload, true, waitForResponses, null, responseCallback); + sendRequest(requester, payload, true, waitForResponses, null, responseCallback, null); } public void sendRequest(Requester requester, Object payload, boolean waitForAck, Integer waitForResponses, Callback ackCallback, Callback responseCallback) throws Exception { + sendRequest(requester, payload, waitForAck, waitForResponses, + ackCallback, responseCallback, null); + } + + public void sendRequest(Requester requester, Object payload, boolean waitForAck, Integer waitForResponses, + Callback ackCallback, Callback responseCallback, Callback endCallback) throws Exception { + sendRequest(requester, payload, waitForAck, waitForResponses, ackCallback, responseCallback, endCallback, null); + } - requester.onAcknowledge(acknowledge -> { + public void sendRequest(Requester requester, Object payload, boolean waitForAck, Integer waitForResponses, + Callback ackCallback, Callback responseCallback, Callback endCallback, String tag) throws Exception { + + requester.onAcknowledge((acknowledge, ackHandler) -> { System.out.println(">>> ACKNOWLEDGE: " + acknowledge); if (waitForAck && ackCallback != null) ackCallback.call(acknowledge); }); - requester.onResponse(response -> { + requester.onResponse((response, ackHandler) -> { System.out.println(">>> RESPONSE: " + response); if (waitForResponses != null && waitForResponses > 0 && responseCallback != null) { responseCallback.call(response); } }); - requester.publish(payload); - } + requester.onEnd((end) -> { + System.out.println(">>> END: "); + if(endCallback != null) { + endCallback.call(null); + } + }); - public ResponderServer createResponderServer(String namespace, ResponderServer.RequestHandler requestHandler) { - return createResponderServer(DEFAULT_CONTEXT_NAME, namespace, requestHandler); + requester.publish(payload, tag); } public ResponderServer createResponderServer(String contextName, String namespace, ResponderServer.RequestHandler requestHandler) { - MessageTemplate options = new MessageTemplate(); System.out.println(">>> RESPONDER SERVER on: " + namespace); - return getContext(contextName).getObjectFactory().createResponderServer(namespace, options, requestHandler, RestPayload.class); + return getContext(contextName).getObjectFactory().createResponderServer(namespace, ResponderOptions.DEFAULTS, requestHandler, RestPayload.class); } public ResponderServer createResponderServer(String namespace, ResponderServer.RequestHandler requestHandler, Class payloadClass) { - MessageTemplate options = new MessageTemplate(); System.out.println(">>> RESPONDER SERVER on: " + namespace); - return getDefaultContext().getObjectFactory().createResponderServer(namespace, options, requestHandler, payloadClass); + return getDefaultContext().getObjectFactory().createResponderServer(namespace, ResponderOptions.DEFAULTS, requestHandler, payloadClass); } public void shutdown(String contextName) { @@ -138,18 +159,14 @@ public void shutdown(String contextName) { } public void shutdown() { - getDefaultContext().shutdown(); + MsbContext context = contextMap.remove(DEFAULT_CONTEXT_NAME); + if(context != null){ + context.shutdown(); + } } - public RestPayload createFacetParserPayload(String query, String body) { - Map queryMap = new HashMap<>(); - queryMap.put("q", query); - - Map bodyMap = new HashMap<>(); - bodyMap.put("body", body); - return new RestPayload.Builder, Object, Object, Map>() - .withQuery(queryMap) - .withBody(bodyMap) - .build(); + public void shutdownAll(){ + contextMap.values().forEach(MsbContext::shutdown); + contextMap.clear(); } } \ No newline at end of file diff --git a/acceptance/src/main/java/io/github/tcdl/msb/acceptance/MultipleRequester.java b/acceptance/src/main/java/io/github/tcdl/msb/acceptance/MultipleRequester.java index 91446e7a..43377fc7 100644 --- a/acceptance/src/main/java/io/github/tcdl/msb/acceptance/MultipleRequester.java +++ b/acceptance/src/main/java/io/github/tcdl/msb/acceptance/MultipleRequester.java @@ -1,6 +1,5 @@ package io.github.tcdl.msb.acceptance; -import com.fasterxml.jackson.core.type.TypeReference; import io.github.tcdl.msb.api.MsbContext; import io.github.tcdl.msb.api.MsbContextBuilder; import io.github.tcdl.msb.api.RequestOptions; @@ -13,6 +12,8 @@ import java.util.concurrent.TimeUnit; import java.util.function.Consumer; +import com.fasterxml.jackson.core.type.TypeReference; + public class MultipleRequester { public static void main(String... args) throws Exception { @@ -54,10 +55,10 @@ public static void runRequester(String namespace, String requestId, String query CompletableFuture.supplyAsync(() -> { msbContext.getObjectFactory().createRequester(namespace, options, new TypeReference>() {}) - .onAcknowledge(acknowledge -> + .onAcknowledge((acknowledge, ackHandler) -> System.out.println(">>> ACK timeout: " + acknowledge.getTimeoutMs()) ) - .onResponse(payload -> { + .onResponse((payload, ackHandler) -> { System.out.println(">>> RESPONSE body: " + payload.getBody()); callback.accept(payload.getBody()); }) diff --git a/acceptance/src/main/java/io/github/tcdl/msb/acceptance/MultipleRequesterResponder.java b/acceptance/src/main/java/io/github/tcdl/msb/acceptance/MultipleRequesterResponder.java index 57228a48..ba43b4df 100644 --- a/acceptance/src/main/java/io/github/tcdl/msb/acceptance/MultipleRequesterResponder.java +++ b/acceptance/src/main/java/io/github/tcdl/msb/acceptance/MultipleRequesterResponder.java @@ -1,12 +1,13 @@ package io.github.tcdl.msb.acceptance; +import com.google.common.base.Joiner; import io.github.tcdl.msb.api.Requester; -import org.apache.commons.lang3.concurrent.BasicThreadFactory; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.commons.lang3.StringUtils; public class MultipleRequesterResponder { @@ -18,6 +19,9 @@ public class MultipleRequesterResponder { private String requesterNamespace1; private String requesterNamespace2; + private final AtomicInteger responseCounter = new AtomicInteger(); + private final List responseBodies = new CopyOnWriteArrayList<>(); + MultipleRequesterResponder(String responderNamespace, String requesterNamespace1, String requesterNamespace2) { this.responderNamespace = responderNamespace; this.requesterNamespace1 = requesterNamespace1; @@ -25,26 +29,14 @@ public class MultipleRequesterResponder { } public void runMultipleRequesterResponder() { - BasicThreadFactory threadFactory = new BasicThreadFactory.Builder() - .namingPattern("MultipleRequesterResponder-%d") - .build(); - - ExecutorService executor = Executors.newFixedThreadPool(2, threadFactory); - - util.createResponderServer(responderNamespace, (request, responder) -> { + util.createResponderServer(responderNamespace, (request, responderContext) -> { System.out.print(">>> REQUEST: " + request); - - Future futureRequester1 = createAndRunRequester(executor, requesterNamespace1); - Future futureRequester2 = createAndRunRequester(executor, requesterNamespace2); - - Thread.sleep(500); - - String result1 = futureRequester1.get(); - String result2 = futureRequester2.get(); - - executor.shutdownNow(); - - responder.send("response from MultipleRequesterResponder:" + (result1 + result2)); + Runnable onFinalResponse = () -> { + String responses = Joiner.on(StringUtils.EMPTY).join(responseBodies); + responderContext.getResponder().send("response from MultipleRequesterResponder:" + responses); + }; + createAndRunRequester(requesterNamespace1, onFinalResponse); + createAndRunRequester(requesterNamespace2, onFinalResponse); }, String.class) .listen(); } @@ -53,29 +45,18 @@ public void shutdown() { util.shutdown(); } - private Future createAndRunRequester(ExecutorService executor, String namespace) { + private void createAndRunRequester(String namespace, Runnable onFinalResponse) { Requester requester = util.createRequester(namespace, NUMBER_OF_RESPONSES, null, 5000, String.class); - Future future = executor.submit(new Callable() { - String result = null; - - @Override - public String call() throws Exception { - util.sendRequest(requester, "PING", NUMBER_OF_RESPONSES, response -> { - System.out.println(">>> RESPONSE body: " + response); - result = response; - synchronized (this) { - notify(); - } - - }); - - synchronized (this) { - wait(); + try { + util.sendRequest(requester, "PING", NUMBER_OF_RESPONSES, response -> { + System.out.println(">>> RESPONSE body: " + response); + responseBodies.add(response); + if(responseCounter.incrementAndGet() == 2) { + onFinalResponse.run(); } - - return result; - } - }); - return future; + }); + } catch (Exception ex) { + throw new RuntimeException(ex); + } } } diff --git a/acceptance/src/main/java/io/github/tcdl/msb/acceptance/MultipleResponder.java b/acceptance/src/main/java/io/github/tcdl/msb/acceptance/MultipleResponder.java index e60345e3..6d405798 100644 --- a/acceptance/src/main/java/io/github/tcdl/msb/acceptance/MultipleResponder.java +++ b/acceptance/src/main/java/io/github/tcdl/msb/acceptance/MultipleResponder.java @@ -1,9 +1,9 @@ package io.github.tcdl.msb.acceptance; import com.fasterxml.jackson.core.type.TypeReference; -import io.github.tcdl.msb.api.MessageTemplate; import io.github.tcdl.msb.api.MsbContext; import io.github.tcdl.msb.api.MsbContextBuilder; +import io.github.tcdl.msb.api.ResponderOptions; import io.github.tcdl.msb.api.message.payload.RestPayload; import java.util.Map; @@ -18,15 +18,14 @@ public static void main(String... args) { } public static void runResponder(String namespace, MsbContext msbContext) { - MessageTemplate options = new MessageTemplate(); - msbContext.getObjectFactory().createResponderServer(namespace, options, (request, responder) -> { + msbContext.getObjectFactory().createResponderServer(namespace, ResponderOptions.DEFAULTS, (request, responderContext) -> { Map requestBody = request.getBody(); System.out.println(">>> GOT request: " + requestBody); String requestId = (String) requestBody.get("requestId"); SearchResponse response = new SearchResponse(requestId, "response"); System.out.println(">>> SENDING response in request to " + requestId); - responder.send(new RestPayload.Builder() + responderContext.getResponder().send(new RestPayload.Builder() .withBody(response) .build()); }, new TypeReference>() {}) diff --git a/acceptance/src/main/java/io/github/tcdl/msb/acceptance/RequesterResponderTest.java b/acceptance/src/main/java/io/github/tcdl/msb/acceptance/RequesterResponderTest.java deleted file mode 100644 index 549192e3..00000000 --- a/acceptance/src/main/java/io/github/tcdl/msb/acceptance/RequesterResponderTest.java +++ /dev/null @@ -1,43 +0,0 @@ -package io.github.tcdl.msb.acceptance; - -import io.github.tcdl.msb.api.Requester; - -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -public class RequesterResponderTest { - - private static final Integer NUMBER_OF_RESPONSES = 1; - - final String NAMESPACE = "test:requester-responder-example"; - - private MsbTestHelper helper = MsbTestHelper.getInstance(); - - private CountDownLatch passedLatch; - - public boolean isPassed() { - try { - passedLatch.await(15, TimeUnit.SECONDS); - } catch (InterruptedException e) { - return false; - } - - return passedLatch.getCount() == 0; - } - - public void runRequesterResponder() throws Exception { - helper.initDefault(); - // running responder server - helper.createResponderServer(NAMESPACE, (request, responder) -> { - System.out.println(">>> REQUEST: " + request); - responder.sendAck(1000, NUMBER_OF_RESPONSES); - responder.send("Pong"); - }, String.class) - .listen(); - - // sending a request - Requester requester = helper.createRequester(NAMESPACE, NUMBER_OF_RESPONSES, String.class); - passedLatch = new CountDownLatch(1); - helper.sendRequest(requester, "Ping", true, NUMBER_OF_RESPONSES, arg -> {}, payload -> passedLatch.countDown()); - } -} diff --git a/acceptance/src/main/java/io/github/tcdl/msb/acceptance/SimpleResponder.java b/acceptance/src/main/java/io/github/tcdl/msb/acceptance/SimpleResponder.java index 5f72859d..ad04351a 100644 --- a/acceptance/src/main/java/io/github/tcdl/msb/acceptance/SimpleResponder.java +++ b/acceptance/src/main/java/io/github/tcdl/msb/acceptance/SimpleResponder.java @@ -14,10 +14,9 @@ public class SimpleResponder { public void runSimpleResponderExample() { helper.initDefault(); - helper.createResponderServer(namespace, (request, responder) -> { - System.out.print(">>> REQUEST: " + request); - Thread.sleep(500); - responder.send(namespace + ":" + "SimpleResponder"); + helper.createResponderServer(namespace, (request, responderContext) -> { + System.out.println(">>> REQUEST: " + request); + responderContext.getResponder().send(namespace + ":" + "SimpleResponder"); }, String.class) .listen(); } diff --git a/acceptance/src/main/resources/application.conf b/acceptance/src/main/resources/application.conf index 52825bd1..182abbe5 100644 --- a/acceptance/src/main/resources/application.conf +++ b/acceptance/src/main/resources/application.conf @@ -6,26 +6,4 @@ msbConfig { version = "1.0.1" instanceId = "msbd06a-ed59-4a39-9f95-811c5fb6ab87" } - - brokerAdapterFactory = "io.github.tcdl.msb.adapters.amqp.AmqpAdapterFactory" - - # Thread pool used for scheduling ack and response timeout tasks - timerThreadPoolSize: 2 - - # Enable/disable message validation against json schema - validateMessage = true - - # Broker Adapter Defaults - brokerConfig = { - host = "127.0.0.1" - port = "5672" - groupId = "msb-java" - durable = false - consumerThreadPoolSize = 5 - # -1 means unlimited - consumerThreadPoolQueueCapacity = 20 - requeueRejectedMessages = true - } - -} - +} \ No newline at end of file diff --git a/acceptance/src/main/resources/log4j.xml b/acceptance/src/main/resources/log4j.xml deleted file mode 100644 index ee87ca65..00000000 --- a/acceptance/src/main/resources/log4j.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/acceptance/src/test/java/io/github/tcdl/msb/acceptance/BlockingRequesterTest.java b/acceptance/src/test/java/io/github/tcdl/msb/acceptance/BlockingRequesterTest.java new file mode 100644 index 00000000..0104edad --- /dev/null +++ b/acceptance/src/test/java/io/github/tcdl/msb/acceptance/BlockingRequesterTest.java @@ -0,0 +1,115 @@ +package io.github.tcdl.msb.acceptance; + +import io.github.tcdl.msb.api.MsbContext; +import io.github.tcdl.msb.api.MsbContextBuilder; +import io.github.tcdl.msb.api.RequestOptions; +import io.github.tcdl.msb.api.ResponderOptions; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.concurrent.*; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + + +public class BlockingRequesterTest { + + private static final String REQUEST_NAMESPACE = "test:future"; + private static final String REQUEST_BODY = "Hi there"; + private static final String RESPONSE_BODY = "Hello, Future!"; + + private MsbContext msbContext; + + @Before + public void setUp() throws Exception { + msbContext = new MsbContextBuilder().build(); + } + + @Test + public void singleFutureResponse() throws Exception { + + //set up responder + msbContext.getObjectFactory() + .createResponderServer(REQUEST_NAMESPACE, ResponderOptions.DEFAULTS, (request, responderContext) -> { + if (REQUEST_BODY.equals(request)) { + responderContext.getResponder().send(RESPONSE_BODY); + } else { + responderContext.getResponder().sendAck(0, 0); + } + }, String.class) + .listen(); + + //prepare and send request + int timeoutMs = 2000; + RequestOptions requestOptions = new RequestOptions.Builder().withResponseTimeout(timeoutMs).build(); + + CompletableFuture futureResponse = msbContext.getObjectFactory() + .createRequesterForSingleResponse(REQUEST_NAMESPACE, String.class, requestOptions) + .request(REQUEST_BODY); + + //wait for response + try { + String actualResponseBody = futureResponse.get(timeoutMs, TimeUnit.MILLISECONDS); + assertEquals("Unexpected response body", RESPONSE_BODY, actualResponseBody); + } catch (Exception e) { + fail("Response was not received in time: " + e); + } + } + + @Test + public void singleFutureResponseAfterAcknowledge() throws Exception { + + int newTimeout = 500; + int initialTimeout = 200; + + //set up responder + msbContext.getObjectFactory().createResponderServer(REQUEST_NAMESPACE, ResponderOptions.DEFAULTS, (request, responderContext) -> { + //send ack requesting additional time for response generation, wait, send response + responderContext.getResponder().sendAck(newTimeout, 1); + TimeUnit.MILLISECONDS.sleep(newTimeout / 2); + responderContext.getResponder().send(RESPONSE_BODY); + }, String.class) + .listen(); + + //prepare and send request + RequestOptions requestOptions = new RequestOptions.Builder().withResponseTimeout(initialTimeout).build(); + CompletableFuture futureResponse = msbContext.getObjectFactory() + .createRequesterForSingleResponse(REQUEST_NAMESPACE, String.class, requestOptions) + .request(REQUEST_BODY); + + //wait for response + try { + String actualResponseBody = futureResponse.get(newTimeout, TimeUnit.MILLISECONDS); + assertEquals("Unexpected response body", RESPONSE_BODY, actualResponseBody); + } catch (Exception e) { + fail("Response was not received in time: " + e); + } + } + + @Test(timeout = 5000, expected = CancellationException.class) + public void tooManyRemainingResponses() throws Exception { + + //set up responder + msbContext.getObjectFactory().createResponderServer(REQUEST_NAMESPACE, ResponderOptions.DEFAULTS, (request, responderContext) -> { + //send ack requesting additional time for response generation, wait, send response + responderContext.getResponder().sendAck(null, 2); //oops + responderContext.getResponder().send(RESPONSE_BODY); + }, String.class) + .listen(); + + //prepare and send request + CompletableFuture futureResponse = msbContext.getObjectFactory() + .createRequesterForSingleResponse(REQUEST_NAMESPACE, String.class, RequestOptions.DEFAULTS) + .request(REQUEST_BODY); + + //wait for response + futureResponse.get(); + } + + @After + public void tearDown() throws Exception { + msbContext.shutdown(); + } +} diff --git a/acceptance/src/test/java/io/github/tcdl/msb/acceptance/MultipleRequesterResponderRunner.java b/acceptance/src/test/java/io/github/tcdl/msb/acceptance/MultipleRequesterResponderRunner.java index 712b5273..107ac49d 100644 --- a/acceptance/src/test/java/io/github/tcdl/msb/acceptance/MultipleRequesterResponderRunner.java +++ b/acceptance/src/test/java/io/github/tcdl/msb/acceptance/MultipleRequesterResponderRunner.java @@ -29,6 +29,7 @@ public void runTest() throws Exception { responderExample1.runSimpleResponderExample(); responderExample2.runSimpleResponderExample(); multipleRequesterResponder.runMultipleRequesterResponder(); + Thread.sleep(500); requesterExample.runSimpleRequesterExample("test:simple-queue2", "test:simple-queue3"); assertTrue(requesterExample.isPassed()); diff --git a/acceptance/src/test/java/io/github/tcdl/msb/acceptance/RequesterResponderRunner.java b/acceptance/src/test/java/io/github/tcdl/msb/acceptance/RequesterResponderRunner.java index 119d157c..d3607f32 100644 --- a/acceptance/src/test/java/io/github/tcdl/msb/acceptance/RequesterResponderRunner.java +++ b/acceptance/src/test/java/io/github/tcdl/msb/acceptance/RequesterResponderRunner.java @@ -1,16 +1,51 @@ package io.github.tcdl.msb.acceptance; +import io.github.tcdl.msb.api.Requester; +import io.github.tcdl.msb.api.Responder; import org.junit.Test; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + import static org.junit.Assert.assertTrue; -public class RequesterResponderRunner { +public class RequesterResponderRunner { //todo clean refactor or throw away if same functionality is covered by the other tests + + private static final Integer NUMBER_OF_RESPONSES = 1; + + final String NAMESPACE = "test:requester-responder-example"; + + private MsbTestHelper helper = MsbTestHelper.getInstance(); + + private CountDownLatch passedLatch; @Test public void runTest() throws Exception { - RequesterResponderTest test = new RequesterResponderTest(); - test.runRequesterResponder(); + helper.initDefault(); + // running responder server + helper.createResponderServer(NAMESPACE, (request, responderContext) -> { + System.out.println(">>> REQUEST: " + request); + Responder responder = responderContext.getResponder(); + responder.sendAck(1000, NUMBER_OF_RESPONSES); + responder.send("Pong"); + }, String.class) + .listen(); + + // sending a request + Requester requester = helper.createRequester(NAMESPACE, NUMBER_OF_RESPONSES, String.class); + passedLatch = new CountDownLatch(1); + helper.sendRequest(requester, "Ping", true, NUMBER_OF_RESPONSES, arg -> {}, payload -> passedLatch.countDown()); + + assertTrue(isPassed()); + } + + public boolean isPassed() { + try { + passedLatch.await(15, TimeUnit.SECONDS); + } catch (InterruptedException e) { + return false; + } - assertTrue(test.isPassed()); + return passedLatch.getCount() == 0; } } diff --git a/acceptance/src/test/java/io/github/tcdl/msb/acceptance/RequesterResponderTest.java b/acceptance/src/test/java/io/github/tcdl/msb/acceptance/RequesterResponderTest.java new file mode 100644 index 00000000..2cf96dca --- /dev/null +++ b/acceptance/src/test/java/io/github/tcdl/msb/acceptance/RequesterResponderTest.java @@ -0,0 +1,285 @@ +package io.github.tcdl.msb.acceptance; + + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import io.github.tcdl.msb.api.MsbContext; +import io.github.tcdl.msb.api.RequestOptions; +import io.github.tcdl.msb.api.ResponderOptions; +import org.junit.After; +import org.junit.Test; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class RequesterResponderTest { + + private MsbTestHelper helper = MsbTestHelper.getInstance(); + Config config = ConfigFactory.load(); + + private static final String RESPONSE_BODY = "hello requester"; + private static final String REQUEST_BODY = "hello responder"; + private static final String NAMESPACE = "test:requester-responder"; + private static final int TIMEOUT_MS = 5000; + + @After + public void tearDown() throws Exception { + helper.shutdownAll(); + } + + @Test + public void simpleRequestResponse() throws Exception { + + MsbContext responderContext = helper.initDistinctContext(MsbTestHelper.temporaryInfrastructure(config)); + responderContext.getObjectFactory().createResponderServer(NAMESPACE, ResponderOptions.DEFAULTS, (req, ctx) -> { + ctx.getResponder().send(RESPONSE_BODY); + }, String.class) + .listen(); + + MsbContext requesterContext = helper.initDistinctContext(MsbTestHelper.temporaryInfrastructure(config)); + + int timeoutMs = 5000; + CountDownLatch latch = new CountDownLatch(1); + RequestOptions requestOptions = new RequestOptions.Builder() + .withWaitForResponses(1) + .withResponseTimeout(timeoutMs) + .build(); + + requesterContext.getObjectFactory() + .createRequester(NAMESPACE, requestOptions, String.class) + .onResponse((resp, ctx) -> { + if (RESPONSE_BODY.equals(resp)) { + latch.countDown(); + } + }) + .publish(REQUEST_BODY); + + assertTrue("Expected response not received in time", latch.await(timeoutMs, TimeUnit.MILLISECONDS)); + } + + @Test + public void tagsAreSentWithTheMessage() throws Exception { + + CountDownLatch latch = new CountDownLatch(1); + String tag = "CUSTOM_TAG"; + + MsbContext responderContext = helper.initDistinctContext(MsbTestHelper.temporaryInfrastructure(config)); + responderContext.getObjectFactory().createResponderServer(NAMESPACE, ResponderOptions.DEFAULTS, + (req, ctx) -> { + if (ctx.getOriginalMessage().getTags().contains(tag)) { + latch.countDown(); + } + }, String.class) + .listen(); + + MsbContext requesterContext = helper.initDistinctContext(MsbTestHelper.temporaryInfrastructure(config)); + + RequestOptions requestOptions = new RequestOptions.Builder() + .withWaitForResponses(1) + .withResponseTimeout(TIMEOUT_MS) + .build(); + + requesterContext.getObjectFactory() + .createRequesterForFireAndForget(NAMESPACE, requestOptions) + .publish(REQUEST_BODY, tag); + + assertTrue("Message with the expected tag not received in time", latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + } + + @Test + public void manualRetry() throws Exception { + + MsbContext requesterContext = helper.initDistinctContext(MsbTestHelper.temporaryInfrastructure(config)); + MsbContext responderContext = helper.initDistinctContext(MsbTestHelper.temporaryInfrastructure(config)); + + int expectedDeliveryCount = 5; + + AtomicInteger deliveryCount = new AtomicInteger(0); + responderContext.getObjectFactory().createResponderServer(NAMESPACE, ResponderOptions.DEFAULTS, + (request, ctx) -> { + deliveryCount.incrementAndGet(); + if(deliveryCount.get() < expectedDeliveryCount) { + ctx.getAcknowledgementHandler().retryMessage(); + } else { + ctx.getAcknowledgementHandler().rejectMessage(); + } + }, + String.class) + .listen(); + + requesterContext.getObjectFactory().createRequesterForFireAndForget(NAMESPACE).publish(REQUEST_BODY); + + //wait a bit + TimeUnit.SECONDS.sleep(1); + assertEquals("Incorrect delivery count", expectedDeliveryCount, deliveryCount.get()); + } + + @Test + public void singleAutomaticRetryOnErrorHandlerFailure() throws Exception { + + MsbContext requesterContext = helper.initDistinctContext(MsbTestHelper.temporaryInfrastructure(config)); + MsbContext responderContext = helper.initDistinctContext(MsbTestHelper.temporaryInfrastructure(config)); + + int expectedDeliveryCount = 2; + + AtomicInteger deliveryCount = new AtomicInteger(0); + responderContext.getObjectFactory().createResponderServer(NAMESPACE, ResponderOptions.DEFAULTS, + (request, ctx) -> { + deliveryCount.incrementAndGet(); + throw new RuntimeException("Trigger onError handler retry"); + },(exception, originalMessage) -> { + throw new RuntimeException("Trigger automatic retry"); + }, + String.class) + .listen(); + + requesterContext.getObjectFactory().createRequesterForFireAndForget(NAMESPACE).publish(REQUEST_BODY); + + //wait a bit + TimeUnit.SECONDS.sleep(1); + assertEquals("Incorrect delivery count", expectedDeliveryCount, deliveryCount.get()); + } + + @Test + public void responseAfterRedelivery() throws Exception { + + MsbContext requesterContext = helper.initDistinctContext(MsbTestHelper.temporaryInfrastructure(config)); + MsbContext responderContext = helper.initDistinctContext(MsbTestHelper.temporaryInfrastructure(config)); + + AtomicInteger deliveryCount = new AtomicInteger(0); + responderContext.getObjectFactory().createResponderServer(NAMESPACE, ResponderOptions.DEFAULTS, + (request, ctx) -> { + deliveryCount.incrementAndGet(); + if (deliveryCount.get() > 1) { + ctx.getResponder().send(RESPONSE_BODY); + } else { + ctx.getAcknowledgementHandler().retryMessage(); + } + + }, + String.class) + .listen(); + + CompletableFuture deferredResponse = requesterContext.getObjectFactory() + .createRequesterForSingleResponse(NAMESPACE, String.class) + .request(REQUEST_BODY); + + String response = deferredResponse.get(1, TimeUnit.SECONDS); + assertEquals("Unexpected response body", RESPONSE_BODY, response); + } + + @Test + public void responseFromDifferentThread() throws Exception { + + MsbContext requesterContext = helper.initDistinctContext(MsbTestHelper.temporaryInfrastructure(config)); + MsbContext responderContext = helper.initDistinctContext(MsbTestHelper.temporaryInfrastructure(config)); + + responderContext.getObjectFactory().createResponderServer(NAMESPACE, ResponderOptions.DEFAULTS, + (request, ctx) -> new Thread(() -> ctx.getResponder().send(RESPONSE_BODY)).start(), + String.class) + .listen(); + + CompletableFuture deferredResponse = requesterContext.getObjectFactory() + .createRequesterForSingleResponse(NAMESPACE, String.class) + .request(REQUEST_BODY); + + String response = deferredResponse.get(1, TimeUnit.SECONDS); + assertEquals("Unexpected response body", RESPONSE_BODY, response); + } + + @Test + public void rejectedMessageNotRedelivered() throws Exception { + + MsbContext requesterContext = helper.initDistinctContext(MsbTestHelper.temporaryInfrastructure(config)); + MsbContext responderContext = helper.initDistinctContext(MsbTestHelper.temporaryInfrastructure(config)); + + int expectedDeliveryCount = 1; + + AtomicInteger deliveryCount = new AtomicInteger(0); + responderContext.getObjectFactory().createResponderServer(NAMESPACE, ResponderOptions.DEFAULTS, + (request, ctx) -> { + deliveryCount.incrementAndGet(); + ctx.getAcknowledgementHandler().rejectMessage(); + }, + String.class) + .listen(); + + requesterContext.getObjectFactory().createRequesterForFireAndForget(NAMESPACE).publish(REQUEST_BODY); + + //wait a bit + TimeUnit.SECONDS.sleep(1); + assertEquals("Incorrect delivery count", expectedDeliveryCount, deliveryCount.get()); + } + + @Test + public void longRunningOnResponseHandler() throws Exception { + + int responsesCount = 10; + MsbContext responderContext = helper.initDistinctContext(MsbTestHelper.temporaryInfrastructure(config)); + responderContext.getObjectFactory().createResponderServer(NAMESPACE, ResponderOptions.DEFAULTS, (req, ctx) -> { + for (int i = 0; i < responsesCount; i++) { + ctx.getResponder().send(RESPONSE_BODY); + } + }, String.class) + .listen(); + + MsbContext requesterContext = helper.initDistinctContext(MsbTestHelper.temporaryInfrastructure(config)); + + int timeoutMs = 5000; + CountDownLatch latch = new CountDownLatch(responsesCount); + + RequestOptions requestOptions = new RequestOptions.Builder() + .withWaitForResponses(10) + .withResponseTimeout(timeoutMs) + .build(); + + int delay = (timeoutMs / responsesCount) * 2; + requesterContext.getObjectFactory() + .createRequester(NAMESPACE, requestOptions, String.class) + .onResponse((resp, ctx) -> { + //slow response handler + try { + TimeUnit.MILLISECONDS.sleep(delay); + } catch (InterruptedException irrelevant) { + //do nothing + } + latch.countDown(); + }) + .publish(REQUEST_BODY); + + double latencyFactor = 1.5; + int delayInMicroseconds = delay * 1000; + assertTrue("Not all responses processed in time", latch.await((int) (delayInMicroseconds * responsesCount * latencyFactor), TimeUnit.MICROSECONDS)); + } + + @Test + public void messageWithForwardNamespace() throws Exception { + + String forwardNamespace = "test:forward"; + MsbContext responderContext = helper.initDistinctContext(MsbTestHelper.temporaryInfrastructure(config)); + + CountDownLatch latch = new CountDownLatch(1); + responderContext.getObjectFactory().createResponderServer(NAMESPACE, ResponderOptions.DEFAULTS, + (req, ctx) -> { + String receivedForwardNamespace = ctx.getOriginalMessage().getTopics().getForward(); + if (forwardNamespace.equals(receivedForwardNamespace)) { + latch.countDown(); + } + }, String.class) + .listen(); + + MsbContext requesterContext = helper.initDistinctContext(MsbTestHelper.temporaryInfrastructure(config)); + + RequestOptions requestOptions = new RequestOptions.Builder().withForwardNamespace(forwardNamespace).build(); + requesterContext.getObjectFactory() + .createRequesterForFireAndForget(NAMESPACE, requestOptions) + .publish(REQUEST_BODY); + + assertTrue("Message with expected forward topic not received in time", latch.await(1, TimeUnit.SECONDS)); + } +} \ No newline at end of file diff --git a/acceptance/src/test/java/io/github/tcdl/msb/acceptance/RoutingKeysTest.java b/acceptance/src/test/java/io/github/tcdl/msb/acceptance/RoutingKeysTest.java new file mode 100644 index 00000000..d6ac8d8a --- /dev/null +++ b/acceptance/src/test/java/io/github/tcdl/msb/acceptance/RoutingKeysTest.java @@ -0,0 +1,163 @@ +package io.github.tcdl.msb.acceptance; + +import com.google.common.collect.Maps; +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import com.typesafe.config.ConfigValueFactory; +import io.github.tcdl.msb.api.*; +import org.junit.After; +import org.junit.Test; + +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +import static com.google.common.collect.Sets.newHashSet; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + + +public class RoutingKeysTest { + + private static final String NAMESPACE = "test:routing"; + + @After + public void tearDown() throws Exception { + MsbTestHelper.getInstance().shutdownAll(); + } + + @Test + public void consumerReceivesMessagesFilteredByRoutingKeys_ifRoutingKeysAreSupported() throws Exception { + + Config baseConfig = ConfigFactory.load(); + + MsbContext responder1Context = distinctContext(baseConfig); + MsbContext responder2Context = distinctContext(baseConfig); + + MsbContext requesterContext = distinctContext(baseConfig); + + String routingKey1 = "routing-key-1"; + String routingKey2 = "routing-key-2"; + String routingKey3 = "routing-key-3"; + + String message1 = "message1"; + String message2 = "message2"; + String message3 = "message3"; + + CompletableFuture deferredResult1 = new CompletableFuture<>(); + CompletableFuture deferredResult2 = new CompletableFuture<>(); + CompletableFuture deferredResult3 = new CompletableFuture<>(); + + ConcurrentMap> deferredResults1 = Maps.newConcurrentMap(); + deferredResults1.put(message1, deferredResult1); + deferredResults1.put(message2, deferredResult2); + setUpResponderForRoutingKeys(responder1Context, newHashSet(routingKey1, routingKey2), arrivingMessagesChecker(deferredResults1)); + + ConcurrentMap> deferredResults2 = Maps.newConcurrentMap(); + deferredResults2.put(message3, deferredResult3); + setUpResponderForRoutingKeys(responder2Context, newHashSet(routingKey3), arrivingMessagesChecker(deferredResults2)); + + //publish messages with different routing keys + publishMessage(requesterContext, ExchangeType.TOPIC, routingKey1, message1); + publishMessage(requesterContext, ExchangeType.TOPIC, routingKey2, message2); + publishMessage(requesterContext, ExchangeType.TOPIC, routingKey3, message3); + + //wait for at most 1 second and check + CompletableFuture combinedDeferredResult = CompletableFuture.allOf(deferredResult1, deferredResult2, deferredResult3); + combinedDeferredResult.get(1, TimeUnit.SECONDS); + assertFalse(combinedDeferredResult.isCancelled()); + } + + @Test + public void consumerReceivesAllMessages_ifRoutingKeysAreNotSupported() throws Exception { + Config baseConfig = ConfigFactory.load(); + + MsbContext responder1Context = distinctContext(baseConfig); + MsbContext responder2Context = distinctContext(baseConfig); + + MsbContext requesterContext = distinctContext(baseConfig); + + String routingKey1 = "routing-key-1"; + String routingKey2 = "routing-key-2"; + + String message1 = "message1"; + String message2 = "message2"; + + CountDownLatch expectedMessagesCountDown = new CountDownLatch(4); //2 for each consumer + + //set up fanout exchange with two queues bound using routing keys + responder1Context.getObjectFactory().createResponderServer(NAMESPACE, new ResponderOptions.Builder().withBindingKeys(newHashSet(routingKey1)).build(), + (request, responderContext) -> expectedMessagesCountDown.countDown(), + String.class) + .listen(); + + responder2Context.getObjectFactory().createResponderServer(NAMESPACE, new ResponderOptions.Builder().withBindingKeys(newHashSet(routingKey1)).build(), + (request, responderContext) -> expectedMessagesCountDown.countDown(), + String.class) + .listen(); + + //publish messages with different routing keys + publishMessage(requesterContext, ExchangeType.FANOUT, routingKey1, message1); + publishMessage(requesterContext, ExchangeType.FANOUT, routingKey2, message2); + + //check that all messages where received by all consumers + assertTrue("", expectedMessagesCountDown.await(1, TimeUnit.SECONDS)); + } + + @Test + public void consumerByDefaultReceivesMessages() throws Exception { + Config baseConfig = ConfigFactory.load(); + MsbContext requesterContext = distinctContext(baseConfig); + MsbContext responderContext = distinctContext(baseConfig); + + CountDownLatch expectedMessagesCountDown = new CountDownLatch(1); + + //routing key not specified + ResponderOptions responderOptions = new AmqpResponderOptions.Builder().withExchangeType(ExchangeType.TOPIC).build(); + + responderContext.getObjectFactory().createResponderServer(NAMESPACE, responderOptions, + (request, responderContext1) -> expectedMessagesCountDown.countDown(), String.class) + .listen(); + + RequestOptions requestOptions = new AmqpRequestOptions.Builder().withExchangeType(ExchangeType.TOPIC).withRoutingKey("some.routing.key").build(); + requesterContext.getObjectFactory().createRequesterForFireAndForget(NAMESPACE, requestOptions).publish("message"); + + assertTrue(expectedMessagesCountDown.await(1, TimeUnit.SECONDS)); + } + + private Consumer arrivingMessagesChecker(ConcurrentMap> deferredResults) { + return (message) -> { + if (deferredResults.containsKey(message)) { + deferredResults.get(message).complete(null); //value is irrelevant + } else { + deferredResults.values().forEach(result -> result.cancel(false)); + } + }; + } + + private MsbContext distinctContext(Config baseConfig) { + return MsbTestHelper.getInstance().initDistinctContext(MsbTestHelper.temporaryInfrastructure(baseConfig)); + } + + private void publishMessage(MsbContext requesterContext, ExchangeType exchangeType, String routingKey, String message) { + RequestOptions requestOptions = new AmqpRequestOptions.Builder() + .withExchangeType(exchangeType) + .withRoutingKey(routingKey) + .build(); + requesterContext.getObjectFactory().createRequester(NAMESPACE, requestOptions, String.class).publish(message); + } + + private void setUpResponderForRoutingKeys(MsbContext context, Set bindingKeys, Consumer messageHandler) { + ResponderOptions responderOptions = new AmqpResponderOptions.Builder() + .withExchangeType(ExchangeType.TOPIC) + .withBindingKeys(bindingKeys).build(); + + context.getObjectFactory().createResponderServer(NAMESPACE, responderOptions, + (request, responderContext) -> messageHandler.accept(request), + String.class) + .listen(); + } +} diff --git a/acceptance/src/test/java/io/github/tcdl/msb/acceptance/ShutdownTest.java b/acceptance/src/test/java/io/github/tcdl/msb/acceptance/ShutdownTest.java new file mode 100644 index 00000000..f64112fe --- /dev/null +++ b/acceptance/src/test/java/io/github/tcdl/msb/acceptance/ShutdownTest.java @@ -0,0 +1,136 @@ +package io.github.tcdl.msb.acceptance; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import io.github.tcdl.msb.api.MsbContext; +import io.github.tcdl.msb.api.ResponderOptions; +import io.github.tcdl.msb.api.ResponderServer; +import org.junit.After; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * This test checks the case when {@link MsbContext} in shut down while message is being processed. + * It ensures that if message processing is successful than acknowledgement is sent to the broker. + *

+ * Steps being performed: + *

    + *
  1. set up MsbContext with durable AMQP queues and exchanges
  2. + *
  3. ensure incoming queue bound to test:shutdown exchange is empty by consuming everything from it
  4. + *
  5. start ResponderServer with blocking message handler (to suspend processing)
  6. + *
  7. publish test message to invoke blocking handler
  8. + *
  9. call {@link MsbContext#shutdown()}
  10. + *
  11. unblock message handler
  12. + *
  13. check that handler was successfully executed
  14. + *
  15. run ResponderServer from new 'probe' MsbContext that generates the same queue names as the previous one and check that there are no messages in incoming queue
  16. + *
+ * There are several arbitrary delays between some steps because of reactive non-blocking API of msb-java. + * + * @author Alexandr Zolotov + */ +public class ShutdownTest { + + private static final Logger LOG = LoggerFactory.getLogger(ShutdownTest.class); + + private ScheduledExecutorService executor = Executors.newScheduledThreadPool(2); + private Config config = ConfigFactory.load(); + + @Test(timeout = 10000) + public void testShutdown() throws Exception { + + String namespace = "test:shutdown"; + AtomicBoolean handlerFinished = new AtomicBoolean(false); + CountDownLatch handlerBlockingLatch = new CountDownLatch(1); + + MsbContext msbContext = createMsbContext(config); + emptyIncomingQueue(namespace, msbContext).get(); //block until queue is empty + + TimeUnit.MILLISECONDS.sleep(50); + msbContext.getObjectFactory().createResponderServer(namespace, ResponderOptions.DEFAULTS, + (request, responderContext) -> { + handlerBlockingLatch.await(); + handlerFinished.set(true); + }, + String.class) + .listen(); + + TimeUnit.MILLISECONDS.sleep(50); + + msbContext.getObjectFactory() + .createRequesterForFireAndForget(namespace) + .publish("very important message"); + + executor.schedule(handlerBlockingLatch::countDown, 200, TimeUnit.MILLISECONDS); + //give msb context some time to be sure it received request before shutdown + TimeUnit.MILLISECONDS.sleep(50); + msbContext.shutdown(); + + //verify handler was executed + assertTrue(handlerFinished.get()); + MsbContext probeContext = createMsbContext(config); + + CountDownLatch messageReceivedFromQueueLatch = new CountDownLatch(1); + + probeContext.getObjectFactory().createResponderServer(namespace, ResponderOptions.DEFAULTS, + (request, responderContext) -> messageReceivedFromQueueLatch.countDown(), String.class) + .listen(); + + assertFalse("Message is not expected to be present in the queue but it is", messageReceivedFromQueueLatch.await(200, TimeUnit.MILLISECONDS)); + } + + private CompletableFuture emptyIncomingQueue(String namespace, MsbContext msbContext) { + + int timeoutTillEmptyMs = 1000; + int oneTickMs = 100; + int tickMaxCount = timeoutTillEmptyMs / oneTickMs; + + CompletableFuture handle = new CompletableFuture<>(); + + AtomicInteger ticksLeft = new AtomicInteger(tickMaxCount); + ResponderServer cleanUpServer = msbContext.getObjectFactory().createResponderServer(namespace, + ResponderOptions.DEFAULTS, + (request, responderContext) -> { + LOG.info("Message received. Resetting the timer."); + ticksLeft.set(tickMaxCount);//reset + }, + String.class + ); + + executor.scheduleWithFixedDelay(() -> { + if (ticksLeft.decrementAndGet() <= 0) { + LOG.info("Queue is empty"); + cleanUpServer.stop(); + handle.complete(null); + throw new RuntimeException("poor man's way to cancel scheduled task"); + } + }, 100, oneTickMs, TimeUnit.MILLISECONDS); + + cleanUpServer.listen(); + return handle; + } + + private MsbContext createMsbContext(Config baseConfig) { + return MsbTestHelper.getInstance().initWithConfig(baseConfig); + } + + @After + public void tearDown() throws Exception { + if (executor != null) { + executor.shutdownNow(); + } + + MsbTestHelper.getInstance().shutdownAll(); + } +} diff --git a/acceptance/src/test/java/io/github/tcdl/msb/acceptance/bdd/ScenarioRunner.java b/acceptance/src/test/java/io/github/tcdl/msb/acceptance/bdd/ScenarioRunner.java deleted file mode 100644 index 51d33594..00000000 --- a/acceptance/src/test/java/io/github/tcdl/msb/acceptance/bdd/ScenarioRunner.java +++ /dev/null @@ -1,55 +0,0 @@ -package io.github.tcdl.msb.acceptance.bdd; - -import io.github.tcdl.msb.acceptance.bdd.steps.AsyncRequesterSteps; -import io.github.tcdl.msb.acceptance.bdd.steps.ConfigurationSteps; -import io.github.tcdl.msb.acceptance.bdd.steps.LoggerSteps; -import io.github.tcdl.msb.acceptance.bdd.steps.RequesterResponderSteps; -import org.jbehave.core.configuration.Configuration; -import org.jbehave.core.configuration.MostUsefulConfiguration; -import org.jbehave.core.io.LoadFromURL; -import org.jbehave.core.io.StoryFinder; -import org.jbehave.core.junit.JUnitStories; -import org.jbehave.core.reporters.Format; -import org.jbehave.core.reporters.StoryReporterBuilder; -import org.jbehave.core.steps.InjectableStepsFactory; -import org.jbehave.core.steps.InstanceStepsFactory; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -public class ScenarioRunner extends JUnitStories { - - private final String STORY_PATH = "target/test-classes"; - private final String STORY_PATTERN = "**/*.story"; - - public List getStepInstances() { - List steps = new ArrayList<>(); - steps.addAll(Arrays.asList( - new ConfigurationSteps(), - new RequesterResponderSteps(), - new AsyncRequesterSteps(), - new LoggerSteps() - )); - return steps; - } - - @Override - public Configuration configuration() { - return new MostUsefulConfiguration() - .useStoryLoader(new LoadFromURL()) - .useStoryReporterBuilder(new StoryReporterBuilder() - .withDefaultFormats().withFormats(Format.CONSOLE, Format.HTML) - .withFailureTrace(true)); - } - - @Override - protected List storyPaths() { - return new StoryFinder().findPaths(STORY_PATH, Arrays.asList(STORY_PATTERN), Arrays.asList(""), getClass().getResource("/").toString()); - } - - @Override - public InjectableStepsFactory stepsFactory() { - return new InstanceStepsFactory(configuration(), getStepInstances()); - } -} diff --git a/acceptance/src/test/java/io/github/tcdl/msb/acceptance/bdd/steps/AsyncRequesterSteps.java b/acceptance/src/test/java/io/github/tcdl/msb/acceptance/bdd/steps/AsyncRequesterSteps.java deleted file mode 100644 index 1623136a..00000000 --- a/acceptance/src/test/java/io/github/tcdl/msb/acceptance/bdd/steps/AsyncRequesterSteps.java +++ /dev/null @@ -1,48 +0,0 @@ -package io.github.tcdl.msb.acceptance.bdd.steps; - -import io.github.tcdl.msb.api.Requester; -import io.github.tcdl.msb.api.message.payload.RestPayload; -import org.jbehave.core.annotations.Given; -import org.jbehave.core.annotations.Then; - -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -import static org.junit.Assert.assertEquals; - -/** - * Steps to send multiple requests - */ -public class AsyncRequesterSteps extends MsbSteps { - - private CountDownLatch await; - - @Given("$numberOfRequesters requesters send a request to namespace $namespace with query '$query'") - public void sendRequests(int numberOfRequesters, String namespace, String query) throws Exception { - await = new CountDownLatch(numberOfRequesters); - - for (int i = 0; i < numberOfRequesters; i++) { - CompletableFuture.supplyAsync(() -> { - Requester requester = helper.createRequester(namespace, 1, RestPayload.class); - try { - RestPayload requestPayload = helper.createFacetParserPayload(query, null); - helper.sendRequest(requester, requestPayload, true, 1, null, this::onResponse); - } catch (Exception e) { - System.err.println(e.getMessage()); - } - return null; - }); - } - } - - @Then("wait responses in $timeout ms") - public void waitForResponse(long timeout) throws Exception { - await.await(timeout, TimeUnit.MILLISECONDS); - assertEquals("Some requests were not responded", 0, await.getCount()); - } - - private void onResponse(RestPayload payload) { - await.countDown(); - } -} diff --git a/acceptance/src/test/java/io/github/tcdl/msb/acceptance/bdd/steps/ConfigurationSteps.java b/acceptance/src/test/java/io/github/tcdl/msb/acceptance/bdd/steps/ConfigurationSteps.java deleted file mode 100644 index 75d0a386..00000000 --- a/acceptance/src/test/java/io/github/tcdl/msb/acceptance/bdd/steps/ConfigurationSteps.java +++ /dev/null @@ -1,68 +0,0 @@ -package io.github.tcdl.msb.acceptance.bdd.steps; - -import com.typesafe.config.Config; -import com.typesafe.config.ConfigFactory; -import com.typesafe.config.ConfigValueFactory; -import org.jbehave.core.annotations.Given; -import org.jbehave.core.annotations.Then; -import org.jbehave.core.annotations.When; - -/** - * Steps to manipulate with MSB configuration - */ -public class ConfigurationSteps extends MsbSteps { - - private String MSB_CONFIG_ROOT = "msbConfig"; - private String VALIDATE_MESSAGE = MSB_CONFIG_ROOT + ".validateMessage"; - private String TIME_THREAD_POOL_SIZE = MSB_CONFIG_ROOT + ".timerThreadPoolSize"; - - private String MSB_BROKER_CONFIG_ROOT = "msbConfig.brokerConfig"; - private String MSB_BROKER_CONSUMER_THREAD_POOL_SIZE = MSB_BROKER_CONFIG_ROOT + ".consumerThreadPoolSize"; - private String MSB_BROKER_CONSUMER_THREAD_POOL_QUEUE_CAPACITY = MSB_BROKER_CONFIG_ROOT + ".consumerThreadPoolQueueCapacity"; - - private Config config = ConfigFactory.load(); - - @Given("MSB configuration with validate message $validate") - public void initWithValidateMessage(boolean validate) { - config = config.withValue(VALIDATE_MESSAGE, ConfigValueFactory.fromAnyRef(validate)); - } - - @Given("MSB configuration with timer thread pool size $size") - public void initWithTimerThreadPoolSize(int size) { - config = config.withValue(TIME_THREAD_POOL_SIZE, ConfigValueFactory.fromAnyRef(size)); - } - - @Given("MSB configuration with consumer thread pool size $size") - public void initWithConsumerThreadPoolSize(int size) { - config = config.withValue(MSB_BROKER_CONSUMER_THREAD_POOL_SIZE, ConfigValueFactory.fromAnyRef(size)); - } - - @Given("MSB configuration with consumer thread pool queue capacity $capacity") - public void initWithConsumerThreadPoolQueueCapacity(int capacity) { - config = config.withValue(MSB_BROKER_CONSUMER_THREAD_POOL_QUEUE_CAPACITY, ConfigValueFactory.fromAnyRef(capacity)); - } - - @Given("start MSB") - public void initMSB() { - helper.initWithConfig(config); - } - - @Given("init MSB context $contextName") - @When("init MSB context $contextName") - public void initMsbContext(String contextName) { - helper.initWithConfig(contextName, config); - } - - @Then("shutdown context $contextName") - @When("shutdown context $contextName") - public void shutdownMsbContext(String contextName) { - helper.shutdown(contextName); - } - - @Then("shutdown MSB") - public void shutdownMSB() { - helper.shutdown(); - } - -} - diff --git a/acceptance/src/test/java/io/github/tcdl/msb/acceptance/bdd/steps/LoggerSteps.java b/acceptance/src/test/java/io/github/tcdl/msb/acceptance/bdd/steps/LoggerSteps.java deleted file mode 100644 index f5372033..00000000 --- a/acceptance/src/test/java/io/github/tcdl/msb/acceptance/bdd/steps/LoggerSteps.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.github.tcdl.msb.acceptance.bdd.steps; - -import io.github.tcdl.msb.acceptance.bdd.util.ListAppender; -import org.jbehave.core.annotations.Given; -import org.jbehave.core.annotations.Then; -import org.junit.Assert; - -public class LoggerSteps { - @Given("logger scanner reset") - public void start() { - ListAppender listAppender = ListAppender.getInstance(); - listAppender.reset(); - } - - @Then("log contains '$substring'") - public void logContains(String substring) throws Exception { - Assert.assertNotNull("String not found '" + substring + "'", ListAppender.getInstance().findLine(substring, 5, 500)); - } -} diff --git a/acceptance/src/test/java/io/github/tcdl/msb/acceptance/bdd/steps/MsbSteps.java b/acceptance/src/test/java/io/github/tcdl/msb/acceptance/bdd/steps/MsbSteps.java deleted file mode 100644 index 5cec0442..00000000 --- a/acceptance/src/test/java/io/github/tcdl/msb/acceptance/bdd/steps/MsbSteps.java +++ /dev/null @@ -1,52 +0,0 @@ -package io.github.tcdl.msb.acceptance.bdd.steps; - -import io.github.tcdl.msb.acceptance.MsbTestHelper; -import io.github.tcdl.msb.api.MsbContext; -import org.jbehave.core.annotations.Given; -import org.jbehave.core.annotations.Then; - -import java.lang.reflect.Method; -import java.util.HashMap; -import java.util.Map; - -public class MsbSteps { - - private final static String DEFAULT_PACKAGE = "io.github.tcdl.msb.examples."; - - MsbTestHelper helper = MsbTestHelper.getInstance(); - private Map microserviceMap = new HashMap<>(); - - @Given("microservice $microservice") - public void startMicroservice(String microservice) throws Throwable { - Class microserviceClass = getClass().getClassLoader().loadClass(resolveClass(microservice)); - Method startMethod = microserviceClass.getMethod("start", MsbContext.class); - startMethod.invoke(microserviceClass.newInstance(), helper.getDefaultContext()); - } - - @Given("start microservice $microservice with context $contextName") - public void startMicroserviceWithContext(String microservice, String contextName) throws Throwable { - Class microserviceClass = getClass().getClassLoader().loadClass(resolveClass(microservice)); - Method startMethod = microserviceClass.getMethod("start", MsbContext.class); - Object microserviceInstance = microserviceClass.newInstance(); - startMethod.invoke(microserviceInstance, helper.getContext(contextName)); - microserviceMap.putIfAbsent(microservice, microserviceInstance); - } - - @Then("stop microservice $microservice") - public void stopMicroservice(String microservice) throws Throwable { - if (microserviceMap.containsKey(microservice)) { - Object microserviceInstance = microserviceMap.get(microservice); - Class microserviceClass = getClass().getClassLoader().loadClass(resolveClass(microservice)); - Method stopMethod = microserviceClass.getMethod("stop"); - stopMethod.invoke(microserviceInstance); - } - } - - protected String resolveClass(String microservice) { - if (microservice.startsWith("com.") || microservice.startsWith("io.")) { - return microservice; - } else { - return DEFAULT_PACKAGE + microservice; - } - } -} diff --git a/acceptance/src/test/java/io/github/tcdl/msb/acceptance/bdd/steps/RequesterResponderSteps.java b/acceptance/src/test/java/io/github/tcdl/msb/acceptance/bdd/steps/RequesterResponderSteps.java deleted file mode 100644 index c0518f5a..00000000 --- a/acceptance/src/test/java/io/github/tcdl/msb/acceptance/bdd/steps/RequesterResponderSteps.java +++ /dev/null @@ -1,121 +0,0 @@ -package io.github.tcdl.msb.acceptance.bdd.steps; - -import com.fasterxml.jackson.databind.ObjectMapper; -import io.github.tcdl.msb.api.Requester; -import io.github.tcdl.msb.api.message.payload.RestPayload; -import io.github.tcdl.msb.support.Utils; -import org.hamcrest.Matchers; -import org.jbehave.core.annotations.Given; -import org.jbehave.core.annotations.Then; -import org.jbehave.core.annotations.When; -import org.jbehave.core.model.ExamplesTable; -import org.jbehave.core.model.OutcomesTable; -import org.junit.Assert; - -import java.util.Map; - -import static io.github.tcdl.msb.acceptance.MsbTestHelper.DEFAULT_CONTEXT_NAME; - -/** - * Steps to send requests and respond with predifined responses - */ -public class RequesterResponderSteps extends MsbSteps { - - private Requester requester; - private String responseBody; - private Map receivedResponse; - - // responder steps - @Given("responder server listens on namespace $namespace") - public void createResponderServer(String namespace) { - createResponderServer(DEFAULT_CONTEXT_NAME, namespace); - } - - @Given("responder server from $contextName listens on namespace $namespace") - @When("responder server from $contextName listens on namespace $namespace") - public void createResponderServer(String contextName, String namespace) { - ObjectMapper mapper = helper.getPayloadMapper(contextName); - helper.createResponderServer(contextName, namespace, (request, responder) -> { - if (responseBody != null) { - RestPayload payload = new RestPayload.Builder() - .withBody(Utils.fromJson(responseBody, Map.class, mapper)) - .build(); - responder.send(payload); - } - }).listen(); - } - - @Given("responder server responds with '$body'") - @When("responder server responds with '$body'") - public void respond(String body) { - responseBody = body; - } - - // requester steps - @Given("requester sends requests to namespace $namespace") - public void createRequester(String namespace) { - createRequester(DEFAULT_CONTEXT_NAME, namespace); - } - - @Given("requester from $contextName sends requests to namespace $namespace") - public void createRequester(String contextName, String namespace) { - requester = helper.createRequester(contextName, namespace, 1, RestPayload.class); - } - - @When("requester sends a request") - public void sendRequest() throws Exception { - sendRequest(DEFAULT_CONTEXT_NAME); - } - - @When("requester from $contextName sends a request") - public void sendRequest(String contextName) throws Exception { - RestPayload payload = helper.createFacetParserPayload("QUERY", null); - helper.sendRequest(requester, payload, 1, this::onResponse); - } - - @When("requester sends a request with query '$query'") - public void sendRequestWithQuery(String query) throws Exception { - RestPayload payload = helper.createFacetParserPayload(query, null); - helper.sendRequest(requester, payload, true, 1, null, this::onResponse); - } - - @When("requester sends a request with body '$body'") - public void sendRequestWithBody(String body) throws Exception { - RestPayload payload = helper.createFacetParserPayload(null, body); - helper.sendRequest(requester, payload, true, 1, null, this::onResponse); - } - - private void onResponse(RestPayload> payload) { - receivedResponse = payload.getBody(); - } - - @Then("requester gets response in $timeout ms") - public void waitForResponse(long timeout) throws Exception { - Thread.sleep(timeout); - Assert.assertNotNull("Response has not been received", receivedResponse); - } - - @Then("response equals $table") - public void responseEquals(ExamplesTable table) throws Exception { - Map expected = table.getRow(0); - OutcomesTable outcomes = new OutcomesTable(); - - for (String key : expected.keySet()) { - outcomes.addOutcome(key, receivedResponse.get(key), Matchers.equalTo(expected.get(key))); - } - - outcomes.verify(); - } - - @Then("response contains $table") - public void responseContains(ExamplesTable table) throws Exception { - Map expected = table.getRow(0); - OutcomesTable outcomes = new OutcomesTable(); - - for (String key : expected.keySet()) { - outcomes.addOutcome(key, receivedResponse.get(key).toString(), Matchers.containsString(expected.get(key))); - } - - outcomes.verify(); - } -} diff --git a/acceptance/src/test/java/io/github/tcdl/msb/acceptance/bdd/util/ListAppender.java b/acceptance/src/test/java/io/github/tcdl/msb/acceptance/bdd/util/ListAppender.java deleted file mode 100644 index c56c01c9..00000000 --- a/acceptance/src/test/java/io/github/tcdl/msb/acceptance/bdd/util/ListAppender.java +++ /dev/null @@ -1,52 +0,0 @@ -package io.github.tcdl.msb.acceptance.bdd.util; - -import org.apache.log4j.AppenderSkeleton; -import org.apache.log4j.spi.LoggingEvent; -import org.junit.Assert; - -import java.util.Optional; -import java.util.concurrent.ConcurrentLinkedQueue; - -public class ListAppender extends AppenderSkeleton { - - private ConcurrentLinkedQueue logEntries = new ConcurrentLinkedQueue<>(); - private static ListAppender instance; - - public ListAppender() { - instance = this; - } - - public static ListAppender getInstance() { - Assert.assertNotNull("ListAppender has not yet been initialized. Did you include it in you log4j config?"); - return instance; - } - - @Override - protected void append(LoggingEvent event) { - logEntries.add(event.getMessage().toString()); - } - - public void close() { - logEntries.clear(); - } - - public boolean requiresLayout() { - return false; - } - - public void reset() { - logEntries.clear(); - } - - public String findLine(String string, int retries, long pollIntervalMs) throws Exception { - Optional line; - int numberOfRetries = retries; - - do { - Thread.sleep(pollIntervalMs); - line = logEntries.stream().filter(logEntry -> logEntry.contains(string)).distinct().findFirst(); - } while (!line.isPresent() && numberOfRetries-- > 0); - - return line.orElse(null); - } -} \ No newline at end of file diff --git a/acceptance/src/test/java/io/github/tcdl/msb/acceptance/bdd/util/TestOutputStreamAppender.java b/acceptance/src/test/java/io/github/tcdl/msb/acceptance/bdd/util/TestOutputStreamAppender.java new file mode 100644 index 00000000..c9263ce3 --- /dev/null +++ b/acceptance/src/test/java/io/github/tcdl/msb/acceptance/bdd/util/TestOutputStreamAppender.java @@ -0,0 +1,68 @@ +package io.github.tcdl.msb.acceptance.bdd.util; + +import ch.qos.logback.core.OutputStreamAppender; + +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +/** + * This appender writes log messages into an internal buffer + * so it is possible to query log for particular messages + * during integration testing. + */ +public class TestOutputStreamAppender extends OutputStreamAppender { + + private final static ByteArrayOutputStream OUT = new ByteArrayOutputStream(); + private final static BufferedOutputStream OUT_BUFFERED = new BufferedOutputStream(OUT); + + /** + * Clean an internal text buffer. + * @throws Exception + */ + public synchronized static void reset() throws Exception { + OUT_BUFFERED.flush(); + OUT.flush(); + OUT.reset(); + } + + /** + * Substring lookup in an internal text buffer. + * @param substring + * @param retries + * @param pollIntervalMs + * @return + * @throws Exception + */ + public synchronized static boolean isPresent(String substring, int retries, long pollIntervalMs) throws Exception { + do { + if(OUT.toString().contains(substring)) { + return true; + } + Thread.sleep(pollIntervalMs); + } while (retries --> 0); + return false; + } + + @Override + public void start() { + setOutputStream(OUT_BUFFERED); + super.start(); + } + + @Override + public void stop() { + try { + OUT_BUFFERED.close(); + OUT.close(); + } catch (IOException ex) { + //ignore + } + super.stop(); + } + + @Override + protected void writeOut(E event) throws IOException { + super.writeOut(event); + } +} diff --git a/acceptance/src/test/resources/log4j.xml b/acceptance/src/test/resources/log4j.xml deleted file mode 100644 index d09d95c0..00000000 --- a/acceptance/src/test/resources/log4j.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/acceptance/src/test/resources/logback-test.xml b/acceptance/src/test/resources/logback-test.xml new file mode 100644 index 00000000..9ad5320d --- /dev/null +++ b/acceptance/src/test/resources/logback-test.xml @@ -0,0 +1,24 @@ + + + + + UTF-8 + %d{HH:mm:ss.SSS} %-5level %logger{1} [%t] tags[%X{msbTags}] corrId[%X{msbCorrelationId}] customTagKey[%X{customTagKey}] - %m%n + + + + + + UTF-8 + %-5level tags[%X{msbTags}] corrId[%X{msbCorrelationId}] customTagKey[%X{customTagKey}] %m%n + + + + + + + + + + + \ No newline at end of file diff --git a/acceptance/src/test/resources/scenarios/channel_recreation.story b/acceptance/src/test/resources/scenarios/channel_recreation.story deleted file mode 100644 index 7fb25663..00000000 --- a/acceptance/src/test/resources/scenarios/channel_recreation.story +++ /dev/null @@ -1,32 +0,0 @@ -Lifecycle: -Before: -Given init MSB context contextResponder -And init MSB context contextRequester -And logger scanner reset -After: -Outcome: ANY -Then shutdown context contextRequester -And shutdown context contextResponder - -Scenario: Sends a request to a responder server and gets response -Given responder server from contextResponder listens on namespace test:jbehave -And responder server responds with '{"result": "hello jbehave"}' -And requester from contextRequester sends requests to namespace test:jbehave -When requester from contextRequester sends a request -Then requester gets response in 5000 ms -And response equals -|result| -|hello jbehave| - -When shutdown context contextResponder -And requester from contextRequester sends a request -Then log contains 'Shutdown is NOT initiated by application. Resetting the channel.' - -When init MSB context contextResponder -And responder server from contextResponder listens on namespace test:jbehave -And responder server responds with '{"result": "hello jbehave"}' -And requester from contextRequester sends a request -Then requester gets response in 5000 ms -And response equals -|result| -|hello jbehave| diff --git a/acceptance/src/test/resources/scenarios/date_extractor.story b/acceptance/src/test/resources/scenarios/date_extractor.story deleted file mode 100644 index d7620c84..00000000 --- a/acceptance/src/test/resources/scenarios/date_extractor.story +++ /dev/null @@ -1,22 +0,0 @@ -Lifecycle: -Before: -Given start MSB -And microservice DateExtractor -After: -Outcome: ANY -Then shutdown MSB - -Scenario: Parsing date with date extractor microservice - -Given requester sends requests to namespace search:parsers:facets:v1 -When requester sends a request with query 'Holidays in 2015' -Then requester gets response in 5000 ms -And response contains -|results| -|year=15| - -When requester sends a request with query '2015-counter-2078' -Then requester gets response in 5000 ms -And response contains -|results| -|year=15| diff --git a/acceptance/src/test/resources/scenarios/multiple_requests_to_date_extractor.story b/acceptance/src/test/resources/scenarios/multiple_requests_to_date_extractor.story deleted file mode 100644 index 4bdc2926..00000000 --- a/acceptance/src/test/resources/scenarios/multiple_requests_to_date_extractor.story +++ /dev/null @@ -1,15 +0,0 @@ -Lifecycle: -Before: -Given MSB configuration with consumer thread pool size 5 -And MSB configuration with consumer thread pool queue capacity 20 -And MSB configuration with timer thread pool size 10 -And start MSB -And microservice DateExtractor -After: -Outcome: ANY -Then shutdown MSB - -Scenario: Sending multiple requests to date extractor microservice in parallel - -Given 2 requesters send a request to namespace search:parsers:facets:v1 with query 'Holidays in 2015' -Then wait responses in 5000 ms \ No newline at end of file diff --git a/acceptance/src/test/resources/scenarios/requester_responder.story b/acceptance/src/test/resources/scenarios/requester_responder.story deleted file mode 100644 index f539a059..00000000 --- a/acceptance/src/test/resources/scenarios/requester_responder.story +++ /dev/null @@ -1,19 +0,0 @@ -Lifecycle: -Before: -Given start MSB -And responder server listens on namespace test:jbehave -After: -Outcome: ANY -Then shutdown MSB - -Scenario: Sends a request to a responder server and waits for response - -Given responder server responds with '{"result": "hello jbehave"}' -And requester sends requests to namespace test:jbehave -When requester sends a request -Then requester gets response in 5000 ms -And response equals -|result| -|hello jbehave| - - diff --git a/activemq/pom.xml b/activemq/pom.xml new file mode 100644 index 00000000..1a64f9bd --- /dev/null +++ b/activemq/pom.xml @@ -0,0 +1,49 @@ + + + + io.github.tcdl.msb + msb-java + 1.6.7-SNAPSHOT + ../pom.xml + + 4.0.0 + msb-java-activemq + msb java activemq + msb java activemq + jar + + scm:git:https://github.com/tcdl/msb-java.git + scm:git:git@github.com:tcdl/msb-java.git + https://github.com/tcdl/msb-java + HEAD + + + tcdl + https://github.com/tcdl + + + + io.github.tcdl.msb + msb-java-core + + + io.github.tcdl.msb + msb-java-core + test-jar + test + + + org.apache.activemq + activemq-client + + + org.apache.activemq + activemq-pool + + + commons-collections + commons-collections + test + + + diff --git a/activemq/src/main/java/io/github/tcdl/msb/adapters/activemq/ActiveMQAcknowledgementAdapter.java b/activemq/src/main/java/io/github/tcdl/msb/adapters/activemq/ActiveMQAcknowledgementAdapter.java new file mode 100644 index 00000000..3a155975 --- /dev/null +++ b/activemq/src/main/java/io/github/tcdl/msb/adapters/activemq/ActiveMQAcknowledgementAdapter.java @@ -0,0 +1,46 @@ +package io.github.tcdl.msb.adapters.activemq; + +import io.github.tcdl.msb.acknowledge.AcknowledgementAdapter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jms.Message; + +/** + * ActiveMQ acknowledgement implementation. + */ +public class ActiveMQAcknowledgementAdapter implements AcknowledgementAdapter { + + private static final Logger LOG = LoggerFactory.getLogger(ActiveMQAcknowledgementAdapter.class); + + private Message message; + + public ActiveMQAcknowledgementAdapter(Message message) { + this.message = message; + } + + /** + * {@inheritDoc} + */ + @Override + public void confirm() throws Exception { + message.acknowledge(); + } + + /** + * {@inheritDoc} + */ + @Override + public void reject() throws Exception { + message.acknowledge(); + } + + /** + * {@inheritDoc} + */ + @Override + public void retry() throws Exception { + //Do nothing. Unconfirmed message will not be removed from the queue + } +} + diff --git a/activemq/src/main/java/io/github/tcdl/msb/adapters/activemq/ActiveMQAdapterFactory.java b/activemq/src/main/java/io/github/tcdl/msb/adapters/activemq/ActiveMQAdapterFactory.java new file mode 100644 index 00000000..6aecabf0 --- /dev/null +++ b/activemq/src/main/java/io/github/tcdl/msb/adapters/activemq/ActiveMQAdapterFactory.java @@ -0,0 +1,163 @@ +package io.github.tcdl.msb.adapters.activemq; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import io.github.tcdl.msb.adapters.AdapterFactory; +import io.github.tcdl.msb.adapters.ConsumerAdapter; +import io.github.tcdl.msb.api.*; +import io.github.tcdl.msb.api.exception.AdapterCreationException; +import io.github.tcdl.msb.api.exception.ChannelException; +import io.github.tcdl.msb.api.exception.ConfigurationException; +import io.github.tcdl.msb.config.MsbConfig; +import io.github.tcdl.msb.config.activemq.ActiveMQBrokerConfig; +import org.apache.activemq.ActiveMQConnectionFactory; +import org.apache.activemq.ActiveMQPrefetchPolicy; +import org.apache.activemq.RedeliveryPolicy; +import org.apache.activemq.jms.pool.PooledConnectionFactory; +import org.apache.commons.lang3.Validate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Optional; + +/** + * ActiveMQAdapterFactory is an implementation of {@link AdapterFactory} + * for {@link ActiveMQAdapterFactory} and {@link ActiveMQConsumerAdapter} + */ +public class ActiveMQAdapterFactory implements AdapterFactory { + + private static final Logger LOG = LoggerFactory.getLogger(ActiveMQAdapterFactory.class); + + private volatile ActiveMQBrokerConfig brokerConfig; + private volatile ActiveMQConnectionManager connectionManager; + + /** + * @throws ChannelException if an error is encountered during connecting to broker + * @throws ConfigurationException if provided configuration is broken + */ + @Override + public void init(MsbConfig msbConfig) { + brokerConfig = createActiveMQBrokerConfig(msbConfig); + LOG.debug("MSB ActiveMQ Broker configuration {}", brokerConfig); + PooledConnectionFactory connectionFactory = createConnectionFactory(brokerConfig); + connectionManager = createConnectionManager(connectionFactory); + } + + private ActiveMQBrokerConfig createActiveMQBrokerConfig(MsbConfig msbConfig) { + Config applicationConfig = msbConfig.getBrokerConfig(); + Config brokerConfig = ConfigFactory.load("activemq").getConfig("config.activemq"); + + Config commonConfig = ConfigFactory.defaultOverrides() + .withFallback(applicationConfig) + .withFallback(brokerConfig); + + return new ActiveMQBrokerConfig.ActiveMQBrokerConfigBuilder().withConfig(commonConfig).build(); + } + + /** + * {@inheritDoc} + */ + @Override + public ActiveMQProducerAdapter createProducerAdapter(String topic, boolean isResponseTopic, RequestOptions requestOptions) { + Validate.notEmpty(topic, "topic is mandatory"); + Validate.notNull(requestOptions, "subscription type is mandatory"); + + Class requestOptionsClass = requestOptions.getClass(); + SubscriptionType subscriptionType; + + if (ActiveMQRequestOptions.class.isAssignableFrom(requestOptionsClass)) { + subscriptionType = ((ActiveMQRequestOptions) requestOptions).getSubscriptionType(); + } else if (requestOptionsClass.equals(RequestOptions.class)) { + subscriptionType = isResponseTopic ? SubscriptionType.TOPIC : brokerConfig.getDefaultSubscriptionType(); + } else { + throw new AdapterCreationException("Illegal for this AdapterFactory RequestOptions subclass"); + } + + return new ActiveMQProducerAdapter(topic, subscriptionType, brokerConfig, connectionManager, isResponseTopic); + } + + /** + * {@inheritDoc} + */ + @Override + public ConsumerAdapter createConsumerAdapter(String topic, boolean isResponseTopic) { + return new ActiveMQConsumerAdapter(topic, brokerConfig.getDefaultSubscriptionType(), + ResponderOptions.DEFAULTS.getBindingKeys(), + brokerConfig, connectionManager, isResponseTopic); + } + + /** + * {@inheritDoc} + */ + @Override + public ActiveMQConsumerAdapter createConsumerAdapter(String topic, boolean isResponseTopic, ResponderOptions responderOptions) { + Validate.notEmpty(topic, "topic is mandatory"); + Validate.notNull(responderOptions, "responderOptions are mandatory"); + + Class responderOptionsClass = responderOptions.getClass(); + SubscriptionType subscriptionType; + + if (ActiveMQResponderOptions.class.isAssignableFrom(responderOptionsClass)) { + subscriptionType = ((ActiveMQResponderOptions) responderOptions).getSubscriptionType(); + } else if (responderOptionsClass.equals(ResponderOptions.class)) { + subscriptionType = isResponseTopic ? SubscriptionType.QUEUE : brokerConfig.getDefaultSubscriptionType(); + } else { + throw new AdapterCreationException("Illegal for this AdapterFactory ResponderOptions subclass"); + } + + return new ActiveMQConsumerAdapter(topic, subscriptionType, responderOptions.getBindingKeys(), + brokerConfig, connectionManager, isResponseTopic); + } + + private PooledConnectionFactory createConnectionFactory(ActiveMQBrokerConfig brokerConfig) { + String url = brokerConfig.getUrl(); + Optional username = brokerConfig.getUsername(); + Optional password = brokerConfig.getPassword(); + + ActiveMQPrefetchPolicy activeMQPrefetchPolicy = new ActiveMQPrefetchPolicy(); + activeMQPrefetchPolicy.setTopicPrefetch(brokerConfig.getPrefetchCount()); + activeMQPrefetchPolicy.setDurableTopicPrefetch(brokerConfig.getPrefetchCount()); + activeMQPrefetchPolicy.setQueuePrefetch(brokerConfig.getPrefetchCount()); + + RedeliveryPolicy redeliveryPolicy = new RedeliveryPolicy(); + redeliveryPolicy.setMaximumRedeliveries(1); + + ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(url); + username.ifPresent(connectionFactory::setUserName); + password.ifPresent(connectionFactory::setPassword); + connectionFactory.setPrefetchPolicy(activeMQPrefetchPolicy); + connectionFactory.setRedeliveryPolicy(redeliveryPolicy); + connectionFactory.setMaxThreadPoolSize(brokerConfig.getPrefetchCount()); + + PooledConnectionFactory pooledConnectionFactory = new PooledConnectionFactory(); + pooledConnectionFactory.setConnectionFactory(connectionFactory); + pooledConnectionFactory.setCreateConnectionOnStartup(true); + pooledConnectionFactory.setReconnectOnException(true); + pooledConnectionFactory.setIdleTimeout(brokerConfig.getConnectionIdleTimeout()); + pooledConnectionFactory.initConnectionsPool(); + pooledConnectionFactory.start(); + + return pooledConnectionFactory; + } + + private ActiveMQConnectionManager createConnectionManager(PooledConnectionFactory connectionFactory) { + return new ActiveMQConnectionManager(connectionFactory); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isUseMsbThreadingModel() { + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public void shutdown() { + connectionManager.close(); + } +} + diff --git a/activemq/src/main/java/io/github/tcdl/msb/adapters/activemq/ActiveMQConnectionManager.java b/activemq/src/main/java/io/github/tcdl/msb/adapters/activemq/ActiveMQConnectionManager.java new file mode 100644 index 00000000..d75a19c5 --- /dev/null +++ b/activemq/src/main/java/io/github/tcdl/msb/adapters/activemq/ActiveMQConnectionManager.java @@ -0,0 +1,81 @@ +package io.github.tcdl.msb.adapters.activemq; + +import io.github.tcdl.msb.api.exception.ChannelException; +import org.apache.activemq.jms.pool.PooledConnectionFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jms.Connection; +import javax.jms.JMSException; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; + +public class ActiveMQConnectionManager { + + private static Logger LOG = LoggerFactory.getLogger(ActiveMQConnectionManager.class); + + private PooledConnectionFactory connectionFactory; + private Map connectionsByClientId; + private Map> connectionCloseListeners; + + public ActiveMQConnectionManager(PooledConnectionFactory connectionFactory) { + this.connectionFactory = connectionFactory; + this.connectionsByClientId = new ConcurrentHashMap<>(); + this.connectionCloseListeners = new ConcurrentHashMap<>(); + } + + /** + * @throws ChannelException if some problems during connecting to Broker were occurred + */ + public Connection obtainConnection(String clientId) { + try { + if (!connectionsByClientId.containsKey(clientId)) { + Connection connection = openConnection(); + if (connection.getClientID() == null) { + connection.setClientID(clientId != null ? clientId : UUID.randomUUID().toString()); + } + connection.start(); + connectionsByClientId.put(clientId, connection); + } + + LOG.info("ActiveMQ connection obtained."); + return connectionsByClientId.get(clientId); + } catch (JMSException e) { + throw new ChannelException("Could not obtain ActiveMQ connection", e); + } + } + + public void addConnectionCloseListener(String clientId, Consumer connectionCloseListener) { + connectionCloseListeners.put(clientId, connectionCloseListener); + } + + + public void close() { + connectionsByClientId.forEach((clientId, connection) -> { + try { + if (connectionCloseListeners.containsKey(clientId)) { + connectionCloseListeners.get(clientId).accept(connection); + } + connection.stop(); + connection.close(); + } catch (JMSException e) { + LOG.error("Error closing connection with exception", e); + } + }); + + connectionFactory.clear(); + connectionFactory.stop(); + } + + private Connection openConnection() throws JMSException { + Connection connection = connectionFactory.createConnection(); + if (connection == null) { + connectionFactory.initConnectionsPool(); + connectionFactory.start(); + connection = connectionFactory.createConnection(); + } + return connection; + } +} diff --git a/activemq/src/main/java/io/github/tcdl/msb/adapters/activemq/ActiveMQConsumerAdapter.java b/activemq/src/main/java/io/github/tcdl/msb/adapters/activemq/ActiveMQConsumerAdapter.java new file mode 100644 index 00000000..efcc0b20 --- /dev/null +++ b/activemq/src/main/java/io/github/tcdl/msb/adapters/activemq/ActiveMQConsumerAdapter.java @@ -0,0 +1,97 @@ +package io.github.tcdl.msb.adapters.activemq; + +import io.github.tcdl.msb.adapters.ConsumerAdapter; +import io.github.tcdl.msb.api.SubscriptionType; +import io.github.tcdl.msb.api.exception.ChannelException; +import io.github.tcdl.msb.config.activemq.ActiveMQBrokerConfig; +import org.apache.commons.lang3.Validate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jms.JMSException; +import javax.jms.MessageConsumer; +import java.util.Optional; +import java.util.Set; + +public class ActiveMQConsumerAdapter implements ConsumerAdapter { + + private static final Logger LOG = LoggerFactory.getLogger(ActiveMQConsumerAdapter.class); + + private static final String VIRTUAL_DESTINATION_PREFIX = "VirtualTopic."; + private static final String CONSUMER_TOPIC_PATTERN = "Consumer.%s.%s"; + + private final String physicalTopic; + private final String groupId; + private final SubscriptionType subscriptionType; + private final Set bindingKeys; + private final ActiveMQBrokerConfig brokerConfig; + private final boolean isResponseTopic; + + private final ActiveMQSessionManager sessionManager; + private MessageConsumer consumer; + + + public ActiveMQConsumerAdapter(String topic, SubscriptionType subscriptionType, Set bindingKeys, ActiveMQBrokerConfig brokerConfig, + ActiveMQConnectionManager connectionManager, boolean isResponseTopic) { + Validate.notNull(topic, "Topic name is required"); + Validate.notNull(subscriptionType, "Subscription type is required"); + + this.groupId = brokerConfig.getGroupId().orElse("msb"); + this.isResponseTopic = isResponseTopic; + this.brokerConfig = brokerConfig; + this.subscriptionType = subscriptionType; + this.bindingKeys = bindingKeys; + this.physicalTopic = formatTopic(topic, groupId); + this.sessionManager = ActiveMQSessionManager.instance(connectionManager); + } + + /** + * {@inheritDoc} + */ + @Override + public void subscribe(RawMessageHandler msgHandler) { + try { + ActiveMQMessageConsumer messageConsumer = new ActiveMQMessageConsumer(msgHandler); + consumer = sessionManager.createConsumer(physicalTopic, subscriptionType, physicalTopic, bindingKeys, isDurable()); + consumer.setMessageListener(messageConsumer::handlerMessage); + } catch (JMSException e) { + throw new ChannelException(String.format("Failed to subscribe to topic %s with binding keys %s", physicalTopic, bindingKeys), e); + } + } + + private boolean isDurable() { + return !isResponseTopic && brokerConfig.isDurable(); + } + + /** + * {@inheritDoc} + */ + @Override + public void unsubscribe() { + try { + consumer.close(); + } catch (JMSException e) { + throw new ChannelException(String.format("Failed to unsubscribe from topic %s", physicalTopic), e); + } + } + + /** + * {@inheritDoc} + */ + @Override + public Optional messageCount() { + throw new UnsupportedOperationException("Message count metric is not supported"); + } + + /** + * {@inheritDoc} + */ + @Override + public Optional isConnected() { + throw new UnsupportedOperationException("IsConnected is not supported"); + } + + private String formatTopic(String topic, String groupId) { + return String.format(CONSUMER_TOPIC_PATTERN, groupId, VIRTUAL_DESTINATION_PREFIX + topic); + } +} diff --git a/activemq/src/main/java/io/github/tcdl/msb/adapters/activemq/ActiveMQMessageConsumer.java b/activemq/src/main/java/io/github/tcdl/msb/adapters/activemq/ActiveMQMessageConsumer.java new file mode 100644 index 00000000..eb0e4cce --- /dev/null +++ b/activemq/src/main/java/io/github/tcdl/msb/adapters/activemq/ActiveMQMessageConsumer.java @@ -0,0 +1,62 @@ +package io.github.tcdl.msb.adapters.activemq; + +import io.github.tcdl.msb.acknowledge.AcknowledgementHandlerImpl; +import io.github.tcdl.msb.acknowledge.AcknowledgementHandlerInternal; +import io.github.tcdl.msb.adapters.ConsumerAdapter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.TextMessage; + +public class ActiveMQMessageConsumer { + + private static final Logger LOG = LoggerFactory.getLogger(ActiveMQMessageConsumer.class); + + private ConsumerAdapter.RawMessageHandler msgHandler; + + public ActiveMQMessageConsumer(ConsumerAdapter.RawMessageHandler msgHandler) { + this.msgHandler = msgHandler; + } + + public void handlerMessage(Message message) { + String messageId = null; + try { + messageId = message.getJMSMessageID(); + } catch (JMSException e) { + LOG.error("Got exception while extracting message id", e); + } + + AcknowledgementHandlerInternal ackHandler = createAcknowledgementHandler(messageId, message); + + if (!(message instanceof TextMessage)) { + LOG.error("Unsupported message type {}", message.getClass()); + ackHandler.confirmMessage(); + } + + try { + String messageBody = ((TextMessage) message).getText(); + LOG.debug("[consumer tag: {}] Message consumed from broker.", messageId); + LOG.trace("Message: {}", messageBody); + + try { + msgHandler.onMessage(messageBody, ackHandler); + LOG.debug("[consumer tag: {}] Raw message has been handled.", messageId); + LOG.trace("Message: {}", messageBody); + } catch (Exception e) { + LOG.error("[consumer tag: {}] Can't handle a raw message.", messageId, e); + LOG.trace("Message: {}", messageBody); + throw e; + } + } catch (Exception e) { + LOG.error("[consumer tag: {}] Got exception while processing incoming message. About to send ActiveMQ reject...", messageId, e); + ackHandler.autoReject(); + } + } + + private AcknowledgementHandlerInternal createAcknowledgementHandler(String messageId, Message message) { + ActiveMQAcknowledgementAdapter adapter = new ActiveMQAcknowledgementAdapter(message); + return new AcknowledgementHandlerImpl(adapter, false, messageId); + } +} \ No newline at end of file diff --git a/activemq/src/main/java/io/github/tcdl/msb/adapters/activemq/ActiveMQProducerAdapter.java b/activemq/src/main/java/io/github/tcdl/msb/adapters/activemq/ActiveMQProducerAdapter.java new file mode 100644 index 00000000..f097948c --- /dev/null +++ b/activemq/src/main/java/io/github/tcdl/msb/adapters/activemq/ActiveMQProducerAdapter.java @@ -0,0 +1,99 @@ +package io.github.tcdl.msb.adapters.activemq; + +import io.github.tcdl.msb.adapters.ProducerAdapter; +import io.github.tcdl.msb.api.SubscriptionType; +import io.github.tcdl.msb.api.exception.ChannelException; +import io.github.tcdl.msb.config.activemq.ActiveMQBrokerConfig; +import joptsimple.internal.Strings; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jms.Destination; +import javax.jms.Message; +import javax.jms.MessageProducer; + +public class ActiveMQProducerAdapter implements ProducerAdapter { + + private static final Logger LOG = LoggerFactory.getLogger(ActiveMQProducerAdapter.class); + + private static final String VIRTUAL_DESTINATION_PREFIX = "VirtualTopic."; + private static final String PRODUCER_ID_PATTERN = "Producer.%s"; + private static final String ERROR_MESSAGE_TEMPLATE = "Failed to publish message to topic '%s' with routing key '%s'"; + + private final String physicalTopic; + private final SubscriptionType subscriptionType; + private final boolean isResponseTopic; + private final ActiveMQBrokerConfig brokerConfig; + private final ActiveMQSessionManager sessionManager; + private final MessageProducer producer; + + ActiveMQProducerAdapter(String topic, SubscriptionType subscriptionType, ActiveMQBrokerConfig brokerConfig, + ActiveMQConnectionManager connectionManager, boolean isResponseTopic) { + Validate.notNull(topic, "Topic is mandatory"); + Validate.notNull(subscriptionType, "Subscription type is mandatory"); + Validate.notNull(brokerConfig, "Broker config is mandatory"); + Validate.notNull(connectionManager, "Connection manager is mandatory"); + + this.physicalTopic = formatTopic(topic, subscriptionType, brokerConfig.isDurable()); + this.subscriptionType = subscriptionType; + this.isResponseTopic = isResponseTopic; + this.brokerConfig = brokerConfig; + + try { + String clientId = String.format(PRODUCER_ID_PATTERN, physicalTopic); + this.sessionManager = ActiveMQSessionManager.instance(connectionManager); + this.producer = sessionManager.createProducer(physicalTopic, subscriptionType, clientId); + } catch (Exception e) { + throw new ChannelException("Failed to setup channel from ActiveMQ connection", e); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void publish(String jsonMessage) { + publish(jsonMessage, Strings.EMPTY); + } + + /** + * {@inheritDoc} + */ + @Override + public void publish(String jsonMessage, String routingKey) { + try { + String clientId = String.format(PRODUCER_ID_PATTERN, physicalTopic); + Message message = sessionManager.createMessage(jsonMessage, clientId); + LOG.debug("Publishing message. Topic name = [{}], routing key = [{}]", physicalTopic, routingKey); + + //create virtual destination with routing key + String destinationTopic = this.physicalTopic; + if (StringUtils.isNotBlank(routingKey)) { + destinationTopic += "." + routingKey; + } + destinationTopic += !isDurable() ? ".t" : ""; + + Destination destination = sessionManager.createDestination(destinationTopic, subscriptionType, clientId); + + if (!isDurable()) { + sessionManager.autoRemove(clientId, destination); + } + + producer.send(destination, message); + } catch (Exception e) { + LOG.error(ERROR_MESSAGE_TEMPLATE, physicalTopic, routingKey); + LOG.trace("Message: {}", jsonMessage); + throw new ChannelException(String.format(ERROR_MESSAGE_TEMPLATE, physicalTopic, routingKey), e); + } + } + + private boolean isDurable() { + return !isResponseTopic && brokerConfig.isDurable(); + } + + private String formatTopic(String topic, SubscriptionType subscriptionType, boolean durable) { + return VIRTUAL_DESTINATION_PREFIX + topic; + } +} diff --git a/activemq/src/main/java/io/github/tcdl/msb/adapters/activemq/ActiveMQSessionManager.java b/activemq/src/main/java/io/github/tcdl/msb/adapters/activemq/ActiveMQSessionManager.java new file mode 100644 index 00000000..734febbc --- /dev/null +++ b/activemq/src/main/java/io/github/tcdl/msb/adapters/activemq/ActiveMQSessionManager.java @@ -0,0 +1,138 @@ +package io.github.tcdl.msb.adapters.activemq; + +import io.github.tcdl.msb.api.SubscriptionType; +import io.github.tcdl.msb.api.exception.ChannelException; +import org.apache.activemq.ActiveMQConnection; +import org.apache.activemq.command.ActiveMQDestination; +import org.apache.activemq.jms.pool.PooledConnection; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jms.*; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import static io.github.tcdl.msb.api.SubscriptionType.QUEUE; + +public class ActiveMQSessionManager { + + private static final Logger LOG = LoggerFactory.getLogger(ActiveMQSessionManager.class); + + private static ActiveMQSessionManager instance; + private ActiveMQConnectionManager connectionManager; + private Map sessionsByClientId; + + private ActiveMQSessionManager(ActiveMQConnectionManager connectionManager) { + this.connectionManager = connectionManager; + this.sessionsByClientId = new ConcurrentHashMap<>(); + } + + static ActiveMQSessionManager instance(ActiveMQConnectionManager connectionManager) { + if (instance == null) { + instance = new ActiveMQSessionManager(connectionManager); + } + return instance; + } + + public MessageProducer createProducer(String topic, SubscriptionType subscriptionType, String clientId) { + Validate.notEmpty(topic, "topic is mandatory"); + Validate.notNull(subscriptionType, "subscription type is mandatory"); + + try { + // omit destination to specify it later during sending a message + MessageProducer producer = getSession(clientId).createProducer(null); + producer.setDeliveryMode(DeliveryMode.PERSISTENT); + LOG.debug("Created producer on topic '{}'", topic); + + return producer; + } catch (JMSException e) { + throw new ChannelException("Producer creation failed with exception", e); + } + } + + public MessageConsumer createConsumer(String topic, SubscriptionType subscriptionType, String clientId, Set bindingKeys, boolean durable) { + Validate.notEmpty(topic, "topic is mandatory"); + Validate.notNull(subscriptionType, "subscription type is mandatory"); + + try { + //create virtual destination with routing keys + String destinationTopic = topic; + if (bindingKeys != null && !bindingKeys.isEmpty()) { + destinationTopic = bindingKeys.stream() + .filter(StringUtils::isNotBlank) + .map(key -> topic + "." + key) + .collect(Collectors.joining( ",")); + destinationTopic = StringUtils.isNotBlank(destinationTopic) ? destinationTopic : topic; + } + destinationTopic += !durable ? ".t" : ""; + + Session session = getSession(clientId); + Destination destination = createDestination(destinationTopic, subscriptionType, clientId); + + if (!durable) { + autoRemove(clientId, destination); + } + + MessageConsumer consumer; + if (subscriptionType == QUEUE || !durable) { + consumer = session.createConsumer(destination); + } else { + consumer = session.createDurableSubscriber((Topic) destination, clientId); + } + + LOG.debug("Created consumer on topic '{}'", topic); + + return consumer; + } catch (JMSException e) { + throw new ChannelException("Consumer creation failed with exception", e); + } + } + + public Destination createDestination(String destinationTopic, SubscriptionType subscriptionType, String clientId) { + Session session = getSession(clientId); + try { + return subscriptionType == QUEUE? + session.createQueue(destinationTopic): + session.createTopic(destinationTopic); + } catch (JMSException e) { + throw new ChannelException("Topic creation failed with exception", e); + } + } + + public Message createMessage(String body, String clientId) { + try { + Session session = getSession(clientId); + return session.createTextMessage(body); + } catch (JMSException e) { + throw new ChannelException("Message creation failed with exception", e); + } + } + + public void autoRemove(String clientId, Destination destination) { + connectionManager.addConnectionCloseListener(clientId, connection -> { + try { + ActiveMQDestination activeMQDestination = (ActiveMQDestination) destination; + LOG.debug("Invoke connection close hook for {}", activeMQDestination.getPhysicalName()); + ((ActiveMQConnection)((PooledConnection)connection).getConnection()).destroyDestination(activeMQDestination); + } catch (JMSException e) { + LOG.error("Error executing connection hook for {}", clientId, e); + } + }); + } + + private Session getSession(String clientId) { + try { + if (!sessionsByClientId.containsKey(clientId)) { + Session session = connectionManager.obtainConnection(clientId).createSession(false, Session.CLIENT_ACKNOWLEDGE); + sessionsByClientId.put(clientId, session); + } + return sessionsByClientId.get(clientId); + } catch (JMSException e) { + throw new ChannelException("Session creation failed with exception", e); + } + } +} diff --git a/activemq/src/main/java/io/github/tcdl/msb/api/ActiveMQRequestOptions.java b/activemq/src/main/java/io/github/tcdl/msb/api/ActiveMQRequestOptions.java new file mode 100644 index 00000000..e26fc42a --- /dev/null +++ b/activemq/src/main/java/io/github/tcdl/msb/api/ActiveMQRequestOptions.java @@ -0,0 +1,49 @@ +package io.github.tcdl.msb.api; + +public class ActiveMQRequestOptions extends RequestOptions { + + private final SubscriptionType subscriptionType; + + private ActiveMQRequestOptions(Integer ackTimeout, + Integer responseTimeout, + Integer waitForResponses, + MessageTemplate messageTemplate, + String forwardNamespace, + String routingKey, + SubscriptionType subscriptionType) { + + super(ackTimeout, responseTimeout, waitForResponses, messageTemplate, forwardNamespace, routingKey); + this.subscriptionType = subscriptionType; + } + + public SubscriptionType getSubscriptionType() { + return subscriptionType; + } + + /** + * {@inheritDoc} + */ + @Override + public RequestOptions.Builder asBuilder() { + return ((ActiveMQRequestOptions.Builder) (new Builder().from(this))).withSubscriptionType(this.subscriptionType); + } + + public static class Builder extends RequestOptions.Builder { + + private SubscriptionType subscriptionType = SubscriptionType.TOPIC; + + public Builder withSubscriptionType(SubscriptionType subscriptionType){ + this.subscriptionType = subscriptionType; + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public RequestOptions build() { + return new ActiveMQRequestOptions(ackTimeout, responseTimeout, waitForResponses, messageTemplate, + forwardNamespace, routingKey, subscriptionType); + } + } +} \ No newline at end of file diff --git a/activemq/src/main/java/io/github/tcdl/msb/api/ActiveMQResponderOptions.java b/activemq/src/main/java/io/github/tcdl/msb/api/ActiveMQResponderOptions.java new file mode 100644 index 00000000..ae5da39e --- /dev/null +++ b/activemq/src/main/java/io/github/tcdl/msb/api/ActiveMQResponderOptions.java @@ -0,0 +1,62 @@ +package io.github.tcdl.msb.api; + +import org.apache.commons.lang3.Validate; + +import javax.annotation.Nonnull; +import java.util.Collections; +import java.util.Set; + +public class ActiveMQResponderOptions extends ResponderOptions { + + private final SubscriptionType subscriptionType; + private final Set bindingKeys; + + private ActiveMQResponderOptions(Set bindingKeys, + MessageTemplate messageTemplate, + SubscriptionType subscriptionType) { + super(bindingKeys, messageTemplate); + this.bindingKeys = bindingKeys; + this.subscriptionType = subscriptionType; + } + + public SubscriptionType getSubscriptionType() { + return subscriptionType; + } + + public Set getBindingKeys() { + return bindingKeys; + } + + public static class Builder extends ResponderOptions.Builder { + + private SubscriptionType subscriptionType = SubscriptionType.QUEUE; + private Set bindingKeys; + + public Builder withMessageTemplate(MessageTemplate responseMessageTemplate) { + this.messageTemplate = responseMessageTemplate; + return this; + } + + public Builder withBindingKeys(Set bindingKeys) { + this.bindingKeys = bindingKeys; + return this; + } + + public Builder withSubscriptionType(@Nonnull SubscriptionType subscriptionType){ + Validate.notNull(subscriptionType); + this.subscriptionType = subscriptionType; + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public ResponderOptions build() { + Set bindingKeys = this.bindingKeys == null || this.bindingKeys.isEmpty() + ? Collections.emptySet() + : this.bindingKeys; + return new ActiveMQResponderOptions(bindingKeys, messageTemplate, subscriptionType); + } + } +} \ No newline at end of file diff --git a/activemq/src/main/java/io/github/tcdl/msb/api/SubscriptionType.java b/activemq/src/main/java/io/github/tcdl/msb/api/SubscriptionType.java new file mode 100644 index 00000000..8d3a7a8a --- /dev/null +++ b/activemq/src/main/java/io/github/tcdl/msb/api/SubscriptionType.java @@ -0,0 +1,6 @@ +package io.github.tcdl.msb.api; + +public enum SubscriptionType { + TOPIC, + QUEUE +} diff --git a/activemq/src/main/java/io/github/tcdl/msb/config/activemq/ActiveMQBrokerConfig.java b/activemq/src/main/java/io/github/tcdl/msb/config/activemq/ActiveMQBrokerConfig.java new file mode 100644 index 00000000..ac2b0c9c --- /dev/null +++ b/activemq/src/main/java/io/github/tcdl/msb/config/activemq/ActiveMQBrokerConfig.java @@ -0,0 +1,98 @@ +package io.github.tcdl.msb.config.activemq; + +import com.typesafe.config.Config; +import io.github.tcdl.msb.api.SubscriptionType; +import io.github.tcdl.msb.config.ConfigurationUtil; + +import java.util.Optional; + +public class ActiveMQBrokerConfig { + + private String url; + private final Optional username; + private final Optional password; + private final SubscriptionType defaultSubscriptionType; + private final Optional groupId; + private final boolean durable; + private final int prefetchCount; + private final int connectionIdleTimeout; + + private ActiveMQBrokerConfig(String url, Optional username, Optional password, + SubscriptionType defaultSubscriptionType, Optional groupId, + boolean durable, int prefetchCount, int connectionIdleTimeout) { + this.url = url; + this.username = username; + this.password = password; + this.defaultSubscriptionType = defaultSubscriptionType; + this.groupId = groupId; + this.durable = durable; + this.prefetchCount = prefetchCount; + this.connectionIdleTimeout = connectionIdleTimeout; + } + + public final static class ActiveMQBrokerConfigBuilder { + + private String url; + private Optional username; + private Optional password; + private SubscriptionType defaultSubscriptionType; + private Optional groupId; + private boolean durable; + private int prefetchCount; + private int connectionIdleTimeout; + + public ActiveMQBrokerConfigBuilder withConfig(Config config) { + this.url = ConfigurationUtil.getString(config, "url"); + this.username = ConfigurationUtil.getOptionalString(config, "username"); + this.password = ConfigurationUtil.getOptionalString(config, "password"); + this.defaultSubscriptionType = SubscriptionType.valueOf(ConfigurationUtil.getString(config, "defaultSubscriptionType").toUpperCase()); + this.groupId = ConfigurationUtil.getOptionalString(config, "groupId"); + this.durable = ConfigurationUtil.getBoolean(config, "durable"); + this.prefetchCount = ConfigurationUtil.getInt(config, "prefetchCount"); + this.connectionIdleTimeout = ConfigurationUtil.getInt(config, "connectionIdleTimeout"); + return this; + } + + public ActiveMQBrokerConfig build() { + return new ActiveMQBrokerConfig(url, username, password, defaultSubscriptionType, groupId, durable, prefetchCount, connectionIdleTimeout); + } + } + + public String getUrl() { + return url; + } + + public Optional getUsername() { + return username; + } + + public Optional getPassword() { + return password; + } + + public SubscriptionType getDefaultSubscriptionType() { + return defaultSubscriptionType; + } + + public Optional getGroupId() { + return groupId; + } + + public boolean isDurable() { + return durable; + } + + public int getPrefetchCount() { + return prefetchCount; + } + + public int getConnectionIdleTimeout() { + return connectionIdleTimeout; + } + + @Override + public String toString() { + return String.format("ActiveMQBrokerConfig [url=%s, username=%s, password=xxx, defaultSubscriptionType=%s, groupId=%s, durable=%s, prefetchCount=%s, connectionIdleTimeout=%s", + url, username, defaultSubscriptionType, groupId, durable, prefetchCount, connectionIdleTimeout); + } +} diff --git a/activemq/src/main/resources/activemq.conf b/activemq/src/main/resources/activemq.conf new file mode 100644 index 00000000..0a5830ec --- /dev/null +++ b/activemq/src/main/resources/activemq.conf @@ -0,0 +1,17 @@ +# ActiveMQ Broker Adapter Defaults +config.activemq = { + + url = "tcp://localhost:61616" + url = ${?MSB_BROKER_URL} + username = "admin" + username = ${?MSB_BROKER_USER_NAME} + password = "admin" + password = ${?MSB_BROKER_PASSWORD} + + durable = true + defaultSubscriptionType = "queue" + prefetchCount = 10 + groupId = "msb-java" + connectionIdleTimeout = 300000 +} + diff --git a/activemq/src/test/java/io/github/tcdl/msb/ActiveMQAcknowledgeTest.java b/activemq/src/test/java/io/github/tcdl/msb/ActiveMQAcknowledgeTest.java new file mode 100644 index 00000000..5fd0ad53 --- /dev/null +++ b/activemq/src/test/java/io/github/tcdl/msb/ActiveMQAcknowledgeTest.java @@ -0,0 +1,75 @@ +package io.github.tcdl.msb; + +import com.typesafe.config.ConfigFactory; +import io.github.tcdl.msb.api.*; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.*; + +public class ActiveMQAcknowledgeTest { + + private String namespace = "activemq:acknowledge:test"; + private MsbContext msbContext; + private ResponderServer responderServer; + + @Before + public void setUp() { + msbContext = new MsbContextBuilder() + .enableShutdownHook(true) + .withConfig(ConfigFactory.load()) + .build(); + } + + @After + public void tearDown() throws InterruptedException { + responderServer.stop(); + msbContext.shutdown(); + TimeUnit.SECONDS.sleep(5); + } + + @Test + @SuppressWarnings("unchecked") + public void retryMessageTest() throws Exception { + String message = "test message"; + + RequestOptions requestOptions = new ActiveMQRequestOptions.Builder() + .withWaitForResponses(0) + .build(); + + ResponderOptions responderOptions = new ActiveMQResponderOptions.Builder() + .build(); + + CountDownLatch receivedMessageLatch = new CountDownLatch(1); + responderServer = msbContext.getObjectFactory().createResponderServer(namespace, responderOptions, + (request, responderContext) -> { + responderContext.getAcknowledgementHandler().retryMessage(); + receivedMessageLatch.countDown(); + }, String.class) + .listen(); + + msbContext.getObjectFactory().createRequester(namespace, requestOptions, String.class) + .publish(message); + + receivedMessageLatch.await(5, TimeUnit.SECONDS); + + Assert.assertEquals(0, receivedMessageLatch.getCount()); + + // do restart + responderServer.stop(); + + // message should be returned to the queue and processed again + ResponderServer.RequestHandler handlerMock = mock(ResponderServer.RequestHandler.class); + responderServer = msbContext.getObjectFactory().createResponderServer(namespace, responderOptions, + handlerMock, String.class).listen(); + + verify(handlerMock, timeout(5000).times(1)).process(eq(message), any(ResponderContext.class)); + } +} diff --git a/activemq/src/test/java/io/github/tcdl/msb/ActiveMQMultipleConsumersTest.java b/activemq/src/test/java/io/github/tcdl/msb/ActiveMQMultipleConsumersTest.java new file mode 100644 index 00000000..be2246c9 --- /dev/null +++ b/activemq/src/test/java/io/github/tcdl/msb/ActiveMQMultipleConsumersTest.java @@ -0,0 +1,84 @@ +package io.github.tcdl.msb; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import com.typesafe.config.ConfigValueFactory; +import io.github.tcdl.msb.api.*; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.IntStream; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.*; + +public class ActiveMQMultipleConsumersTest { + + private String namespace = "activemq:multiple-consumers:test"; + private List msbContexts; + private List responderServers; + + @Before + public void setUp() { + responderServers = new LinkedList<>(); + msbContexts = new LinkedList<>(); + msbContexts.add(new MsbContextBuilder() + .enableShutdownHook(true) + .withConfig(ConfigFactory.load()) + .build()); + } + + @After + public void tearDown() throws InterruptedException { + responderServers.forEach(ResponderServer::stop); + responderServers.clear(); + msbContexts.forEach(MsbContext::shutdown); + msbContexts.clear(); + TimeUnit.SECONDS.sleep(5); + } + + @Test + @SuppressWarnings("unchecked") + public void multipleConsumersTest() throws Exception { + final int numberOfConsumers = 3; + final String message = "test message"; + + MsbContext msbContext = msbContexts.get(0); + + RequestOptions requestOptions = new ActiveMQRequestOptions.Builder() + .withWaitForResponses(0) + .build(); + + ResponderOptions responderOptions = new ActiveMQResponderOptions.Builder() + .build(); + + ResponderServer.RequestHandler handlerMock = mock(ResponderServer.RequestHandler.class); + IntStream.range(0, numberOfConsumers).forEach((i) -> { + Config config = getConfigWith(ConfigFactory.load(), "msbConfig.brokerConfig.groupId", "consumer" + (i+1)); + + MsbContext consumerMsbContext = new MsbContextBuilder() + .enableShutdownHook(true) + .withConfig(config) + .build(); + + msbContexts.add(consumerMsbContext); + + responderServers.add(consumerMsbContext.getObjectFactory().createResponderServer(namespace, responderOptions, + handlerMock, String.class).listen()); + }); + + msbContext.getObjectFactory().createRequester(namespace, requestOptions) + .publish(message); + + verify(handlerMock, timeout(5000).times(numberOfConsumers)).process(eq(message), any(ResponderContext.class)); + } + + private Config getConfigWith(Config config, String path, Object value) { + return config.withValue(path, ConfigValueFactory.fromAnyRef(value)); + } +} diff --git a/activemq/src/test/java/io/github/tcdl/msb/ActiveMQRequesterResponderTest.java b/activemq/src/test/java/io/github/tcdl/msb/ActiveMQRequesterResponderTest.java new file mode 100644 index 00000000..59e32ace --- /dev/null +++ b/activemq/src/test/java/io/github/tcdl/msb/ActiveMQRequesterResponderTest.java @@ -0,0 +1,63 @@ +package io.github.tcdl.msb; + +import com.typesafe.config.ConfigFactory; +import io.github.tcdl.msb.api.*; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.*; + +public class ActiveMQRequesterResponderTest { + + private String namespace = "activemq:req-resp:test"; + private MsbContext msbContext; + private ResponderServer responderServer; + + @Before + public void setUp() { + msbContext = new MsbContextBuilder() + .enableShutdownHook(true) + .withConfig(ConfigFactory.load()) + .build(); + } + + @After + public void tearDown() throws InterruptedException { + responderServer.stop(); + msbContext.shutdown(); + TimeUnit.SECONDS.sleep(5); + } + + @Test + @SuppressWarnings("unchecked") + public void requestResponseTest() throws Exception { + String message = "test message"; + + RequestOptions requestOptions = new ActiveMQRequestOptions.Builder() + .withWaitForResponses(1) + .build(); + + ResponderOptions responderOptions = new ActiveMQResponderOptions.Builder() + .build(); + + BiConsumer responseHandlerMock = mock(BiConsumer.class); + + responderServer = msbContext.getObjectFactory().createResponderServer(namespace, responderOptions, + (request, responderContext) -> { + responderContext.getResponder().send(request); + }, String.class) + .listen(); + + msbContext.getObjectFactory().createRequester(namespace, requestOptions, String.class) + .onResponse(responseHandlerMock) + .publish(message); + + verify(responseHandlerMock, timeout(5000).times(1)).accept(eq(message), any(MessageContext.class)); + } +} diff --git a/activemq/src/test/java/io/github/tcdl/msb/ActiveMQRoutingKeyTest.java b/activemq/src/test/java/io/github/tcdl/msb/ActiveMQRoutingKeyTest.java new file mode 100644 index 00000000..b274afd2 --- /dev/null +++ b/activemq/src/test/java/io/github/tcdl/msb/ActiveMQRoutingKeyTest.java @@ -0,0 +1,118 @@ + +package io.github.tcdl.msb; + +import com.google.common.collect.Sets; +import com.typesafe.config.ConfigFactory; +import io.github.tcdl.msb.api.*; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.*; + +public class ActiveMQRoutingKeyTest { + + private String namespace = "activemq:rounting-keys:test"; + private List msbContexts; + private List responderServers; + + @Before + public void setUp() { + msbContexts = new LinkedList<>(); + responderServers = new LinkedList<>(); + } + + @After + public void tearDown() throws InterruptedException { + responderServers.forEach(ResponderServer::stop); + responderServers.clear(); + msbContexts.forEach(MsbContext::shutdown); + msbContexts.clear(); + TimeUnit.SECONDS.sleep(5); + } + + @Test + @SuppressWarnings("unchecked") + public void requestResponseWithRoutingKeyTest() throws Exception { + final String message = "test message"; + final String routingKey = "RK"; + + RequestOptions requestOptions = new ActiveMQRequestOptions.Builder() + .withRoutingKey(routingKey) + .withWaitForResponses(0) + .build(); + + ResponderOptions responderOptions = new ActiveMQResponderOptions.Builder() + .withSubscriptionType(SubscriptionType.QUEUE) + .withBindingKeys(Sets.newHashSet(routingKey)) + .build(); + + MsbContext msbContext = new MsbContextBuilder() + .enableShutdownHook(true) + .withConfig(ConfigFactory.load()) + .build(); + msbContexts.add(msbContext); + + ResponderServer.RequestHandler handlerMock = mock(ResponderServer.RequestHandler.class); + + responderServers.add(msbContext.getObjectFactory().createResponderServer(namespace, responderOptions, + handlerMock, String.class).listen()); + + msbContext.getObjectFactory().createRequester(namespace, requestOptions) + .publish(message); + + verify(handlerMock, timeout(5000).times(1)).process(eq(message), any(ResponderContext.class)); + } + + @Test + @SuppressWarnings("unchecked") + public void requestResponseWithDifferentRoutingKeyTest() throws Exception { + final String message = "test message"; + final String routingKey1 = "RK1"; + final String routingKey2 = "RK2"; + + RequestOptions requestOptions = new ActiveMQRequestOptions.Builder() + .withRoutingKey(routingKey1) + .withWaitForResponses(0) + .build(); + + ResponderOptions responderOptions1 = new ActiveMQResponderOptions.Builder() + .withBindingKeys(Sets.newHashSet(routingKey1)) + .build(); + + ResponderOptions responderOptions2 = new ActiveMQResponderOptions.Builder() + .withBindingKeys(Sets.newHashSet(routingKey2)) + .build(); + + MsbContext msbContext1 = new MsbContextBuilder() + .enableShutdownHook(true) + .withConfig(ConfigFactory.load()) + .build(); + msbContexts.add(msbContext1); + + MsbContext msbContext2 = new MsbContextBuilder() + .enableShutdownHook(true) + .withConfig(ConfigFactory.load()) + .build(); + msbContexts.add(msbContext2); + + ResponderServer.RequestHandler handlerMock = mock(ResponderServer.RequestHandler.class); + + responderServers.add(msbContext1.getObjectFactory().createResponderServer(namespace, responderOptions1, + handlerMock, String.class).listen()); + + responderServers.add(msbContext2.getObjectFactory().createResponderServer(namespace, responderOptions2, + handlerMock, String.class).listen()); + + msbContext1.getObjectFactory().createRequester(namespace, requestOptions) + .publish(message); + + verify(handlerMock, timeout(5000).times(1)).process(eq(message), any(ResponderContext.class)); + } +} diff --git a/activemq/src/test/resources/application.conf b/activemq/src/test/resources/application.conf new file mode 100644 index 00000000..b19b1b4e --- /dev/null +++ b/activemq/src/test/resources/application.conf @@ -0,0 +1,14 @@ +msbConfig { + serviceDetails = { + name = "activemq-integration-test" + instanceId = "activemq-integration-test-001" + version = "1.0.0" + } + + brokerAdapterFactory = "io.github.tcdl.msb.adapters.activemq.ActiveMQAdapterFactory" + + brokerConfig = { + groupId = "int-test" + durable = false + } +} \ No newline at end of file diff --git a/activemq/src/test/resources/logback-test.xml b/activemq/src/test/resources/logback-test.xml new file mode 100644 index 00000000..17d61d64 --- /dev/null +++ b/activemq/src/test/resources/logback-test.xml @@ -0,0 +1,14 @@ + + + + + UTF-8 + %d{HH:mm:ss.SSS} %-5level %logger{1} [%t] tags[%X{msbTags}] corrId[%X{msbCorrelationId}] customTagKey[%X{customTagKey}] - %m%n + + + + + + + + \ No newline at end of file diff --git a/amqp/pom.xml b/amqp/pom.xml index 9000cb20..f76a41a5 100644 --- a/amqp/pom.xml +++ b/amqp/pom.xml @@ -3,7 +3,7 @@ io.github.tcdl.msb msb-java - 1.3.0-SNAPSHOT + 1.6.7-SNAPSHOT ../pom.xml 4.0.0 @@ -26,9 +26,20 @@ io.github.tcdl.msb msb-java-core + + io.github.tcdl.msb + msb-java-core + test-jar + test + com.rabbitmq amqp-client + + commons-collections + commons-collections + test + diff --git a/amqp/src/main/java/io/github/tcdl/msb/adapters/amqp/AmqpAcknowledgementAdapter.java b/amqp/src/main/java/io/github/tcdl/msb/adapters/amqp/AmqpAcknowledgementAdapter.java new file mode 100644 index 00000000..75a3a2ee --- /dev/null +++ b/amqp/src/main/java/io/github/tcdl/msb/adapters/amqp/AmqpAcknowledgementAdapter.java @@ -0,0 +1,34 @@ +package io.github.tcdl.msb.adapters.amqp; + +import com.rabbitmq.client.Channel; +import io.github.tcdl.msb.acknowledge.AcknowledgementAdapter; + +/** + * AMQP acknowledgement implementation. + */ +public class AmqpAcknowledgementAdapter implements AcknowledgementAdapter { + final Channel channel; + final String identifier; + final long deliveryTag; + + public AmqpAcknowledgementAdapter(Channel channel, String identifier, long deliveryTag) { + this.channel = channel; + this.identifier = identifier; + this.deliveryTag = deliveryTag; + } + + @Override + public void confirm() throws Exception { + channel.basicAck(deliveryTag, false); + } + + @Override + public void reject() throws Exception { + channel.basicReject(deliveryTag, false); + } + + @Override + public void retry() throws Exception { + channel.basicReject(deliveryTag, true); + } +} diff --git a/amqp/src/main/java/io/github/tcdl/msb/adapters/amqp/AmqpAdapterFactory.java b/amqp/src/main/java/io/github/tcdl/msb/adapters/amqp/AmqpAdapterFactory.java index 99959b88..6faf22f9 100644 --- a/amqp/src/main/java/io/github/tcdl/msb/adapters/amqp/AmqpAdapterFactory.java +++ b/amqp/src/main/java/io/github/tcdl/msb/adapters/amqp/AmqpAdapterFactory.java @@ -3,54 +3,49 @@ import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.Recoverable; +import com.rabbitmq.client.RecoveryListener; import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; import io.github.tcdl.msb.adapters.AdapterFactory; -import io.github.tcdl.msb.adapters.ConsumerAdapter; -import io.github.tcdl.msb.adapters.ProducerAdapter; +import io.github.tcdl.msb.api.*; +import io.github.tcdl.msb.api.exception.AdapterCreationException; import io.github.tcdl.msb.api.exception.ChannelException; import io.github.tcdl.msb.api.exception.ConfigurationException; import io.github.tcdl.msb.config.MsbConfig; import io.github.tcdl.msb.config.amqp.AmqpBrokerConfig; -import io.github.tcdl.msb.support.Utils; -import org.apache.commons.lang3.concurrent.BasicThreadFactory; +import org.apache.commons.lang3.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.Optional; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; /** * AmqpAdapterFactory is an implementation of {@link AdapterFactory} * for {@link AmqpProducerAdapter} and {@link AmqpConsumerAdapter} */ public class AmqpAdapterFactory implements AdapterFactory { - private static final Logger LOG = LoggerFactory.getLogger(AmqpAdapterFactory.class); - private static final int QUEUE_SIZE_UNLIMITED = -1; + private static final Logger LOG = LoggerFactory.getLogger(AmqpAdapterFactory.class); - private AmqpBrokerConfig amqpBrokerConfig; - private AmqpConnectionManager connectionManager; - private ExecutorService consumerThreadPool; + private volatile AmqpBrokerConfig amqpBrokerConfig; + private volatile AmqpConnectionManager connectionManager; /** * @throws ChannelException if an error is encountered during connecting to broker * @throws ConfigurationException if provided configuration is broken */ + @Override public void init(MsbConfig msbConfig) { amqpBrokerConfig = createAmqpBrokerConfig(msbConfig); + LOG.debug("MSB AMQP Broker configuration {}", amqpBrokerConfig); ConnectionFactory connectionFactory = createConnectionFactory(amqpBrokerConfig); Connection connection = createConnection(connectionFactory); connectionManager = createConnectionManager(connection); - consumerThreadPool = createConsumerThreadPool(amqpBrokerConfig); } + //TODO extract config loading from this class and then rewrite unit test for this class completely protected AmqpBrokerConfig createAmqpBrokerConfig(MsbConfig msbConfig) { Config amqpApplicationConfig = msbConfig.getBrokerConfig(); Config amqpLibConfig = ConfigFactory.load("amqp").getConfig("config.amqp"); @@ -67,13 +62,48 @@ protected AmqpBrokerConfig createAmqpBrokerConfig(MsbConfig msbConfig) { } @Override - public ProducerAdapter createProducerAdapter(String topic) { - return new AmqpProducerAdapter(topic, amqpBrokerConfig, connectionManager); + public AmqpProducerAdapter createProducerAdapter(String topic, boolean isResponseTopic, RequestOptions requestOptions) { + Validate.notNull(topic, "topic is mandatory"); + Validate.notNull(requestOptions, "requestOptions are mandatory"); + + Class requestOptionsClass = requestOptions.getClass(); + ExchangeType exchangeType; + + if (AmqpRequestOptions.class.isAssignableFrom(requestOptionsClass)) { + exchangeType = ((AmqpRequestOptions) requestOptions).getExchangeType(); + } else if (requestOptionsClass.equals(RequestOptions.class)) { + exchangeType = amqpBrokerConfig.getDefaultExchangeType(); + } else { + throw new AdapterCreationException("Illegal for this AdapterFactory RequestOptions subclass"); + } + + return new AmqpProducerAdapter(topic, exchangeType, amqpBrokerConfig, connectionManager); } @Override - public ConsumerAdapter createConsumerAdapter(String topic) { - return new AmqpConsumerAdapter(topic, amqpBrokerConfig, connectionManager, consumerThreadPool); + public AmqpConsumerAdapter createConsumerAdapter(String topic, boolean isResponseTopic) { + return new AmqpConsumerAdapter(topic, amqpBrokerConfig.getDefaultExchangeType(), + ResponderOptions.DEFAULTS.getBindingKeys(), + amqpBrokerConfig, connectionManager, isResponseTopic); + } + + @Override + public AmqpConsumerAdapter createConsumerAdapter(String topic, boolean isResponseTopic, ResponderOptions responderOptions) { + Validate.notEmpty(topic, "topic is mandatory"); + Validate.notNull(responderOptions, "responderOptions are mandatory"); + + Class responderOptionsClass = responderOptions.getClass(); + ExchangeType exchangeType; + + if (AmqpResponderOptions.class.isAssignableFrom(responderOptionsClass)) { + exchangeType = ((AmqpResponderOptions) responderOptions).getExchangeType(); + } else if (responderOptionsClass.equals(ResponderOptions.class)) { + exchangeType = amqpBrokerConfig.getDefaultExchangeType(); + } else { + throw new AdapterCreationException("Illegal for this AdapterFactory ResponderOptions subclass"); + } + + return new AmqpConsumerAdapter(topic, exchangeType, responderOptions.getBindingKeys(), amqpBrokerConfig, connectionManager, isResponseTopic); } protected ConnectionFactory createConnectionFactory(AmqpBrokerConfig adapterConfig) { @@ -87,17 +117,14 @@ protected ConnectionFactory createConnectionFactory(AmqpBrokerConfig adapterConf connectionFactory.setHost(host); connectionFactory.setPort(port); connectionFactory.setAutomaticRecoveryEnabled(true); + connectionFactory.setTopologyRecoveryEnabled(true); connectionFactory.setNetworkRecoveryInterval(adapterConfig.getNetworkRecoveryIntervalMs()); connectionFactory.setRequestedHeartbeat(adapterConfig.getHeartbeatIntervalSec()); - if (username.isPresent()) { - connectionFactory.setUsername(username.get()); - } - if (password.isPresent()) { - connectionFactory.setPassword(password.get()); - } - if (virtualHost.isPresent()) { - connectionFactory.setVirtualHost(virtualHost.get()); - } + connectionFactory.setExceptionHandler(new AmqpExceptionHandler()); + + username.ifPresent(connectionFactory::setUsername); + password.ifPresent(connectionFactory::setPassword); + virtualHost.ifPresent(connectionFactory::setVirtualHost); try { if (adapterConfig.useSSL()) { @@ -127,24 +154,36 @@ protected AmqpConnectionManager createConnectionManager(Connection connection) { */ protected Connection createConnection(ConnectionFactory connectionFactory) { try { - LOG.info(String.format("Opening AMQP connection to host = %s, port = %s, username = %s, password = xxx, virtualHost = %s...", - connectionFactory.getHost(), connectionFactory.getPort(), connectionFactory.getUsername(), connectionFactory.getVirtualHost())); + LOG.info("Opening AMQP connection to host = {}, port = {}, username = {}, password = xxx, virtualHost = {}...", + connectionFactory.getHost(), connectionFactory.getPort(), connectionFactory.getUsername(), connectionFactory.getVirtualHost()); Connection connection = connectionFactory.newConnection(); if (connection instanceof Recoverable) { // This cast is possible for connections created by a factory that supports auto-recovery - ((Recoverable) connection).addRecoveryListener(recoverable -> LOG.info("AMQP connection recovered.")); + ((Recoverable) connection).addRecoveryListener(new RecoveryListener() { + @Override + public void handleRecovery(Recoverable recoverable) { + LOG.info("AMQP connection recovered."); + } + @Override + public void handleRecoveryStarted(Recoverable recoverable) { + LOG.info("AMQP connection recovery started."); + } + }); } LOG.info("AMQP connection opened."); return connection; - } catch (IOException e) { + } catch (IOException | TimeoutException e) { throw new ChannelException("Failed to obtain connection to AMQP broker", e); } } @Override - public void shutdown() { - Utils.gracefulShutdown(consumerThreadPool, "consumer"); + public boolean isUseMsbThreadingModel() { + return true; + } + @Override + public void shutdown() { try { connectionManager.close(); } catch (IOException e) { @@ -152,26 +191,6 @@ public void shutdown() { } } - protected ExecutorService createConsumerThreadPool(AmqpBrokerConfig amqpBrokerConfig) { - BasicThreadFactory threadFactory = new BasicThreadFactory.Builder() - .namingPattern("amqp-consumer-thread-%d") - .build(); - int numberOfThreads = amqpBrokerConfig.getConsumerThreadPoolSize(); - int queueCapacity = amqpBrokerConfig.getConsumerThreadPoolQueueCapacity(); - - BlockingQueue queue; - if (queueCapacity == QUEUE_SIZE_UNLIMITED) { - queue = new LinkedBlockingQueue<>(); - } else { - queue = new ArrayBlockingQueue<>(queueCapacity); - } - - return new ThreadPoolExecutor(numberOfThreads, numberOfThreads, - 0L, TimeUnit.MILLISECONDS, - queue, - threadFactory); - } - AmqpBrokerConfig getAmqpBrokerConfig() { return amqpBrokerConfig; } @@ -180,8 +199,4 @@ AmqpConnectionManager getConnectionManager() { return connectionManager; } - ExecutorService getConsumerThreadPool() { - return consumerThreadPool; - } - } diff --git a/amqp/src/main/java/io/github/tcdl/msb/adapters/amqp/AmqpAutoRecoveringChannel.java b/amqp/src/main/java/io/github/tcdl/msb/adapters/amqp/AmqpAutoRecoveringChannel.java deleted file mode 100644 index c434cf61..00000000 --- a/amqp/src/main/java/io/github/tcdl/msb/adapters/amqp/AmqpAutoRecoveringChannel.java +++ /dev/null @@ -1,87 +0,0 @@ -package io.github.tcdl.msb.adapters.amqp; - -import com.rabbitmq.client.AMQP; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.ConfirmListener; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.util.Map; - -/** - * Wrapper for {@link Channel} that support automatic re-initialization upon errors. - */ -public class AmqpAutoRecoveringChannel { - private static final Logger LOG = LoggerFactory.getLogger(AmqpAutoRecoveringChannel.class); - - private AmqpConnectionManager connectionManager; - private Channel channel; - - /** - * Lock object used for 2 purposes: - * 1. Prevent interleaving of basicPublish with confirmSelect - * 2. Prevent interleaving of channel initialization and shutdown - */ - private final Object lock = new Object(); - - public AmqpAutoRecoveringChannel(AmqpConnectionManager connectionManager) { - this.connectionManager = connectionManager; - } - - public AMQP.Exchange.DeclareOk exchangeDeclare(String exchange, String type, boolean durable, boolean autoDelete, - Map arguments) throws IOException { - Channel channel = obtainChannelForPublisherConfirms(); - return channel.exchangeDeclare(exchange, type, durable, autoDelete, arguments); - } - - public void basicPublish(String exchange, String routingKey, AMQP.BasicProperties props, byte[] body) throws IOException { - synchronized (lock) { - Channel channel = obtainChannelForPublisherConfirms(); - channel.basicPublish(exchange, routingKey, props, body); - } - } - - private Channel obtainChannelForPublisherConfirms() throws IOException { - synchronized (lock) { - if (channel == null) { - createChannelForPublisherConfirms(connectionManager); - } - return channel; - } - } - - private void createChannelForPublisherConfirms(AmqpConnectionManager connectionManager) throws IOException { - channel = connectionManager.obtainConnection().createChannel(); - channel.confirmSelect(); - - channel.addConfirmListener(new ConfirmListener() { - @Override - public void handleAck(long deliveryTag, boolean multiple) throws IOException { - LOG.debug(String.format("Processing publisher ack (deliveryTag = %s, multiple = %b)", deliveryTag, multiple)); - } - - @Override - public void handleNack(long deliveryTag, boolean multiple) throws IOException { - LOG.debug(String.format("Processing publisher nack (deliveryTag = %s, multiple = %b)", deliveryTag, multiple)); - } - }); - - channel.addShutdownListener(cause -> { - synchronized (lock) { - LOG.debug("Handling channel shutdown..."); - if (cause.isInitiatedByApplication()) { - LOG.debug("Shutdown is initiated by application. Ignoring it."); - } else { - LOG.error("Shutdown is NOT initiated by application. Resetting the channel.", cause); - /* - We cannot re-initialize channel here directly because ShutdownListener callbacks run in the connection's thread, - so the call to createChannel causes a deadlock since it blocks waiting for a response (whilst the connection's thread - is stuck executing the listener). - */ - channel = null; - } - } - }); - } -} diff --git a/amqp/src/main/java/io/github/tcdl/msb/adapters/amqp/AmqpConsumerAdapter.java b/amqp/src/main/java/io/github/tcdl/msb/adapters/amqp/AmqpConsumerAdapter.java index d931ab31..e1b1190d 100644 --- a/amqp/src/main/java/io/github/tcdl/msb/adapters/amqp/AmqpConsumerAdapter.java +++ b/amqp/src/main/java/io/github/tcdl/msb/adapters/amqp/AmqpConsumerAdapter.java @@ -2,44 +2,43 @@ import com.rabbitmq.client.Channel; import io.github.tcdl.msb.adapters.ConsumerAdapter; +import io.github.tcdl.msb.api.ExchangeType; import io.github.tcdl.msb.api.exception.ChannelException; import io.github.tcdl.msb.config.amqp.AmqpBrokerConfig; import io.github.tcdl.msb.support.Utils; import org.apache.commons.lang3.Validate; import java.io.IOException; -import java.util.concurrent.ExecutorService; +import java.util.Optional; +import java.util.Set; + public class AmqpConsumerAdapter implements ConsumerAdapter { - private String topic; private Channel channel; - private String exchangeName; + private final String exchangeName; + private final Set bindingKeys; private String consumerTag; private AmqpBrokerConfig adapterConfig; - private ExecutorService consumerThreadPool; + private boolean isResponseTopic = false; + private Optional currentQueueName = Optional.empty(); - /** - * The constructor. - * @param topic - a topic name associated with the adapter - * @param consumerThreadPool contains incoming messages wrapped as tasks for further processing. Parameters of this thread pool determine degree of - * parallelism of incoming message processing - * @throws ChannelException if some problems during setup channel from RabbitMQ connection were occurred - */ + public AmqpConsumerAdapter(String exchangeName, ExchangeType exchangeType, Set bindingKeys, AmqpBrokerConfig amqpBrokerConfig, + AmqpConnectionManager connectionManager, boolean isResponseTopic) { + Validate.notNull(exchangeName, "Exchange name is required"); + Validate.notNull(exchangeType, "Exchange type is required"); + Validate.notEmpty(bindingKeys, "At least one routing key is required"); - public AmqpConsumerAdapter(String topic, AmqpBrokerConfig amqpBrokerConfig, AmqpConnectionManager connectionManager, ExecutorService consumerThreadPool) { - Validate.notNull(topic, "the 'topic' must not be null"); - - this.topic = topic; - this.exchangeName = topic; + this.bindingKeys = bindingKeys; + this.exchangeName = exchangeName; this.adapterConfig = amqpBrokerConfig; - this.consumerThreadPool = consumerThreadPool; + this.isResponseTopic = isResponseTopic; try { channel = connectionManager.obtainConnection().createChannel(); - channel.exchangeDeclare(exchangeName, "fanout", false /* durable */, true /* auto-delete */, null); + channel.exchangeDeclare(exchangeName, exchangeType.value(), false /* durable */, true /* auto-delete */, null); } catch (IOException e) { - throw new ChannelException("Failed to setup channel from ActiveMQ connection", e); + throw new ChannelException("Failed to setup channel", e); } } @@ -49,20 +48,32 @@ public AmqpConsumerAdapter(String topic, AmqpBrokerConfig amqpBrokerConfig, Amqp @Override public void subscribe(RawMessageHandler msgHandler) { String groupId = adapterConfig.getGroupId().orElse(Utils.generateId()); - boolean durable = adapterConfig.isDurable(); + boolean durable = isDurable(); + int prefetchCount = adapterConfig.getPrefetchCount(); - String queueName = generateQueueName(topic, groupId, durable); + String queueName = generateQueueName(exchangeName, groupId, durable); try { channel.queueDeclare(queueName, durable /* durable */, false /* exclusive */, !durable /*auto-delete */, null); - channel.queueBind(queueName, exchangeName, ""); - - consumerTag = channel.basicConsume(queueName, false /* autoAck */, new AmqpMessageConsumer(channel, consumerThreadPool, msgHandler, adapterConfig)); + channel.basicQos(prefetchCount); // Don't accept more messages if we have any unacknowledged + for (String bindingKey : bindingKeys) { + channel.queueBind(queueName, exchangeName, bindingKey); + } + consumerTag = channel.basicConsume(queueName, false /* autoAck */, new AmqpMessageConsumer(channel, msgHandler, adapterConfig)); + currentQueueName = Optional.of(queueName); } catch (IOException e) { - throw new ChannelException(String.format("Failed to subscribe to topic %s", topic), e); + throw new ChannelException(String.format("Failed to subscribe to topic %s with routing keys %s", exchangeName, bindingKeys), e); } } + protected boolean isDurable() { + if (isResponseTopic) { + //response topic is always auto-delete and not durable + return false; + } + return adapterConfig.isDurable(); + } + /** * {@inheritDoc} */ @@ -70,14 +81,44 @@ public void subscribe(RawMessageHandler msgHandler) { public void unsubscribe() { try { channel.basicCancel(consumerTag); + currentQueueName = Optional.empty(); } catch (IOException e) { - throw new ChannelException(String.format("Failed to unsubscribe from topic %s", topic), e); + throw new ChannelException(String.format("Failed to unsubscribe from topic %s", exchangeName), e); } } + /** + * {@inheritDoc} + */ + @Override + public Optional messageCount() { + return currentQueueName.map(queueName -> { + try { + return channel.messageCount(queueName); + } catch (IOException e) { + throw new ChannelException(String.format("Failed to fetch ready messages for topic %s and queue %s", exchangeName, queueName), e); + } + }); + } + + /** + * {@inheritDoc} + */ + @Override + public Optional isConnected() { + return currentQueueName.map(queueName -> { + try { + return channel.isOpen() && channel.getConnection().isOpen() && channel.consumerCount(queueName) > 0; + } catch (IOException e) { + throw new ChannelException(String.format("Failed to get consumer status for topic %s and queue %s", exchangeName, queueName), e); + } + }); + } + /** * Generate topic name to get unique topics for different microservices - * @param topic - topic name associated with the adapter + * + * @param topic - topic name associated with the adapter * @param groupId - group service Id * @param durable - queue durability */ diff --git a/amqp/src/main/java/io/github/tcdl/msb/adapters/amqp/AmqpExceptionHandler.java b/amqp/src/main/java/io/github/tcdl/msb/adapters/amqp/AmqpExceptionHandler.java new file mode 100644 index 00000000..7949ddc7 --- /dev/null +++ b/amqp/src/main/java/io/github/tcdl/msb/adapters/amqp/AmqpExceptionHandler.java @@ -0,0 +1,17 @@ +package io.github.tcdl.msb.adapters.amqp; + +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.impl.DefaultExceptionHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AmqpExceptionHandler extends DefaultExceptionHandler { + + private static Logger LOG = LoggerFactory.getLogger(AmqpExceptionHandler.class); + + @Override + protected void handleChannelKiller(Channel channel, Throwable exception, String what) { + LOG.error("{} threw exception for channel '{}'.", what, channel, exception); + super.handleChannelKiller(channel, exception, what); + } +} diff --git a/amqp/src/main/java/io/github/tcdl/msb/adapters/amqp/AmqpMessageConsumer.java b/amqp/src/main/java/io/github/tcdl/msb/adapters/amqp/AmqpMessageConsumer.java index 41e3c93c..1346e184 100644 --- a/amqp/src/main/java/io/github/tcdl/msb/adapters/amqp/AmqpMessageConsumer.java +++ b/amqp/src/main/java/io/github/tcdl/msb/adapters/amqp/AmqpMessageConsumer.java @@ -1,67 +1,71 @@ package io.github.tcdl.msb.adapters.amqp; -import com.rabbitmq.client.AMQP; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.DefaultConsumer; -import com.rabbitmq.client.Envelope; import io.github.tcdl.msb.adapters.ConsumerAdapter; +import io.github.tcdl.msb.acknowledge.AcknowledgementHandlerImpl; +import io.github.tcdl.msb.acknowledge.AcknowledgementHandlerInternal; import io.github.tcdl.msb.config.amqp.AmqpBrokerConfig; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.io.IOException; import java.nio.charset.Charset; -import java.util.concurrent.ExecutorService; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.DefaultConsumer; +import com.rabbitmq.client.Envelope; /** - * Special consumer that allows to process messages coming from single AMQP channel in parallel. - * - * REASON: - * - * AMQP library is implemented in such way that messages that arrive into a single channel are dispatched in a synchronous loop to the consumers and hence - * will not be processed in parallel. - * - * To address this issue {@link AmqpMessageConsumer} just takes incoming message, wraps it in a task and puts into a thread pool. So the actual - * processing is happening in the separate thread from that thread pool. + * Consumer that converts message body to a String before passing it to handler. + * Also rejects message in case of any exception during its processing to prevent AMQP channel from being closed. */ public class AmqpMessageConsumer extends DefaultConsumer { private static final Logger LOG = LoggerFactory.getLogger(AmqpMessageConsumer.class); - ExecutorService consumerThreadPool; ConsumerAdapter.RawMessageHandler msgHandler; private AmqpBrokerConfig amqpBrokerConfig; - public AmqpMessageConsumer(Channel channel, ExecutorService consumerThreadPool, ConsumerAdapter.RawMessageHandler msgHandler, AmqpBrokerConfig amqpBrokerConfig) { + public AmqpMessageConsumer(Channel channel, ConsumerAdapter.RawMessageHandler msgHandler, AmqpBrokerConfig amqpBrokerConfig) { super(channel); - this.consumerThreadPool = consumerThreadPool; this.msgHandler = msgHandler; this.amqpBrokerConfig = amqpBrokerConfig; } @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { + long deliveryTag = envelope.getDeliveryTag(); + AcknowledgementHandlerInternal ackHandler = createAcknowledgementHandler( + getChannel(), consumerTag, deliveryTag, envelope.isRedeliver()); try { Charset charset = amqpBrokerConfig.getCharset(); - boolean requeueRejectedMessages = amqpBrokerConfig.isRequeueRejectedMessages(); + String bodyStr = new String(body, charset); - LOG.debug(String.format("[consumer tag: %s] Message consumed from broker: %s", consumerTag, bodyStr)); - try { - consumerThreadPool.submit(new AmqpMessageProcessingTask(consumerTag, bodyStr, getChannel(), envelope.getDeliveryTag(), msgHandler)); - LOG.debug(String.format("[consumer tag: %s] Message has been put in the processing queue: %s. About to send AMQP ack...", - consumerTag, bodyStr)); - getChannel().basicAck(envelope.getDeliveryTag(), false); - LOG.debug(String.format("[consumer tag: %s] AMQP ack has been sent for message '%s'", consumerTag, bodyStr)); + LOG.debug("[consumer tag: {}] Message consumed from broker.", consumerTag); + LOG.trace("Message: {}", bodyStr); + + try { + msgHandler.onMessage(bodyStr, ackHandler); + LOG.debug("[consumer tag: {}] Raw message has been handled.", consumerTag); + LOG.trace("Message: {}", bodyStr); } catch (Exception e) { - LOG.error(String.format("[consumer tag: %s] Couldn't put message in the processing queue: %s. About to send AMQP reject...", - consumerTag, bodyStr), e); - getChannel().basicReject(envelope.getDeliveryTag(), requeueRejectedMessages); - LOG.error(String.format("[consumer tag: %s] AMQP reject has been sent for message: %s", consumerTag, bodyStr)); + LOG.error("[consumer tag: {}] Can't handle a raw message.", consumerTag, e); + LOG.trace("Message: {}", bodyStr); + throw e; } } catch (Exception e) { // Catch all exceptions to prevent AMQP channel to be closed - LOG.error(String.format("[consumer tag: %s] Got exception while processing incoming message", consumerTag), e); + LOG.error("[consumer tag: {}] Got exception while processing incoming message. About to send AMQP reject...", consumerTag, e); + ackHandler.autoReject(); } } + + AcknowledgementHandlerInternal createAcknowledgementHandler(Channel channel, String consumerTag, long deliveryTag, boolean isRequeueRejectedMessages) { + AmqpAcknowledgementAdapter adapter = new AmqpAcknowledgementAdapter(channel, consumerTag, deliveryTag); + String messageTextIdentifier = "consumer tag: " + consumerTag; + return new AcknowledgementHandlerImpl(adapter, isRequeueRejectedMessages, messageTextIdentifier); + } + } diff --git a/amqp/src/main/java/io/github/tcdl/msb/adapters/amqp/AmqpMessageProcessingTask.java b/amqp/src/main/java/io/github/tcdl/msb/adapters/amqp/AmqpMessageProcessingTask.java deleted file mode 100644 index 9db2df50..00000000 --- a/amqp/src/main/java/io/github/tcdl/msb/adapters/amqp/AmqpMessageProcessingTask.java +++ /dev/null @@ -1,43 +0,0 @@ -package io.github.tcdl.msb.adapters.amqp; - -import com.rabbitmq.client.Channel; -import io.github.tcdl.msb.adapters.ConsumerAdapter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * {@link AmqpMessageProcessingTask} wraps incoming message. This task is put into message processing thread pool (see {@link AmqpMessageConsumer}). - */ -public class AmqpMessageProcessingTask implements Runnable { - private static final Logger LOG = LoggerFactory.getLogger(AmqpMessageProcessingTask.class); - - String consumerTag; - String body; - Channel channel; - long deliveryTag; - ConsumerAdapter.RawMessageHandler msgHandler; - - public AmqpMessageProcessingTask(String consumerTag, String body, Channel channel, long deliveryTag, ConsumerAdapter.RawMessageHandler msgHandler) { - this.consumerTag = consumerTag; - this.body = body; - this.channel = channel; - this.deliveryTag = deliveryTag; - this.msgHandler = msgHandler; - } - - /** - * Passes the message to the configured handler and acknowledges it to AMQP broker. - * IMPORTANT CAVEAT: This task is meant to be run in a thread pool so it should handle all its exceptions carefully. In particular it shouldn't - * throw an exception (because it's going to be swallowed anyway and not printed) - */ - @Override - public void run() { - try { - LOG.debug(String.format("[consumer tag: %s] Starting message processing: %s", consumerTag, body)); - msgHandler.onMessage(body); - LOG.debug(String.format("[consumer tag: %s] Message has been processed: %s", consumerTag, body)); - } catch (Exception e) { - LOG.error(String.format("[consumer tag: %s] Failed to process message %s", consumerTag, body), e); - } - } -} diff --git a/amqp/src/main/java/io/github/tcdl/msb/adapters/amqp/AmqpProducerAdapter.java b/amqp/src/main/java/io/github/tcdl/msb/adapters/amqp/AmqpProducerAdapter.java index 034474fa..10f68828 100644 --- a/amqp/src/main/java/io/github/tcdl/msb/adapters/amqp/AmqpProducerAdapter.java +++ b/amqp/src/main/java/io/github/tcdl/msb/adapters/amqp/AmqpProducerAdapter.java @@ -2,33 +2,37 @@ import com.rabbitmq.client.MessageProperties; import io.github.tcdl.msb.adapters.ProducerAdapter; +import io.github.tcdl.msb.api.ExchangeType; import io.github.tcdl.msb.api.exception.ChannelException; import io.github.tcdl.msb.config.amqp.AmqpBrokerConfig; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import java.io.IOException; import java.nio.charset.Charset; public class AmqpProducerAdapter implements ProducerAdapter { - private String exchangeName; - private AmqpBrokerConfig amqpBrokerConfig; - private AmqpAutoRecoveringChannel amqpAutoRecoveringChannel; + private static final Logger LOG = LoggerFactory.getLogger(AmqpProducerAdapter.class); + private static final String ERROR_MESSAGE_TEMPLATE = "Failed to publish message into exchange '%s' with routing key '%s'"; - /** - * The constructor. - * @param topic - a topic name associated with the adapter - * @throws ChannelException if some problems during setup channel from RabbitMQ connection were occurred - */ - public AmqpProducerAdapter(String topic, AmqpBrokerConfig amqpBrokerConfig, AmqpConnectionManager connectionManager) { - Validate.notNull(topic, "the 'topic' must not be null"); + final String exchangeName; + final AmqpBrokerConfig amqpBrokerConfig; + final LoggingAmqpChannel channel; + + public AmqpProducerAdapter(String topic, ExchangeType exchangeType, AmqpBrokerConfig amqpBrokerConfig, AmqpConnectionManager connectionManager) { + Validate.notNull(topic, "Topic is mandatory"); + Validate.notNull(exchangeType, "Exchange type is mandatory"); + Validate.notNull(amqpBrokerConfig, "Broker config is mandatory"); + Validate.notNull(exchangeType, "Connection manager is mandatory"); this.exchangeName = topic; this.amqpBrokerConfig = amqpBrokerConfig; - this.amqpAutoRecoveringChannel = new AmqpAutoRecoveringChannel(connectionManager); + this.channel = LoggingAmqpChannel.instance(connectionManager); try { - amqpAutoRecoveringChannel.exchangeDeclare(exchangeName, "fanout", false /* durable */, true /* auto-delete */, null); - } catch (IOException e) { + channel.exchangeDeclare(exchangeName, exchangeType.value(), false /* durable */, true /* auto-delete */, null); + } catch (Exception e) { throw new ChannelException("Failed to setup channel from ActiveMQ connection", e); } } @@ -38,11 +42,20 @@ public AmqpProducerAdapter(String topic, AmqpBrokerConfig amqpBrokerConfig, Amqp */ @Override public void publish(String jsonMessage) { + publish(jsonMessage, StringUtils.EMPTY); + } + + @Override + public void publish(String jsonMessage, String routingKey) { + Validate.notNull(routingKey, "routing key is required"); + Charset charset = amqpBrokerConfig.getCharset(); + try { - Charset charset = amqpBrokerConfig.getCharset(); - amqpAutoRecoveringChannel.basicPublish(exchangeName, "" /* routing key */, MessageProperties.PERSISTENT_BASIC, jsonMessage.getBytes(charset)); - } catch (IOException e) { - throw new ChannelException(String.format("Failed to publish message '%s' into exchange '%s'", jsonMessage, exchangeName), e); + channel.basicPublish(exchangeName, routingKey, MessageProperties.PERSISTENT_BASIC, jsonMessage.getBytes(charset)); + } catch (Exception e) { + LOG.error(ERROR_MESSAGE_TEMPLATE, exchangeName, routingKey); + LOG.trace("Message: {}", jsonMessage); + throw new ChannelException(String.format(ERROR_MESSAGE_TEMPLATE, exchangeName, routingKey), e); } } } diff --git a/amqp/src/main/java/io/github/tcdl/msb/adapters/amqp/LoggingAmqpChannel.java b/amqp/src/main/java/io/github/tcdl/msb/adapters/amqp/LoggingAmqpChannel.java new file mode 100644 index 00000000..915b2a6c --- /dev/null +++ b/amqp/src/main/java/io/github/tcdl/msb/adapters/amqp/LoggingAmqpChannel.java @@ -0,0 +1,83 @@ +package io.github.tcdl.msb.adapters.amqp; + +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.ConfirmListener; +import io.github.tcdl.msb.api.exception.ChannelException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.Map; + +/** + * Wrapper for {@link Channel} that provides some additional debug logging. + */ +public class LoggingAmqpChannel { + private static final Logger LOG = LoggerFactory.getLogger(LoggingAmqpChannel.class); + + private AmqpConnectionManager connectionManager; + private Channel channel; + + public static LoggingAmqpChannel instance(AmqpConnectionManager connectionManager){ + LoggingAmqpChannel loggingChannel = new LoggingAmqpChannel(connectionManager); + loggingChannel.init(); + return loggingChannel; + } + + private LoggingAmqpChannel(AmqpConnectionManager connectionManager) { + this.connectionManager = connectionManager; + } + + /* + * Initialization logic resides in this method instead of constructor in order to avoid 'this' reference escape + * while object construction is not yet finished. + */ + private void init() { + try { + this.channel = connectionManager.obtainConnection().createChannel(); + } catch (IOException e) { + throw new ChannelException("Channel creation failed with exception", e); + } + + channel.addConfirmListener(new ConfirmListener() { + @Override + public void handleAck(long deliveryTag, boolean multiple) throws IOException { + LOG.debug("Processing publisher ack (deliveryTag = {}, multiple = {})", deliveryTag, multiple); + } + + @Override + public void handleNack(long deliveryTag, boolean multiple) throws IOException { + LOG.debug("Processing publisher nack (deliveryTag = {}, multiple = {})", deliveryTag, multiple); + } + }); + + channel.addShutdownListener(cause -> { + LOG.debug("Handling channel shutdown..."); + if (cause.isInitiatedByApplication()) { + LOG.debug("Shutdown is initiated by application."); + } else { + LOG.error("Shutdown is NOT initiated by application.", cause); + } + }); + } + + public AMQP.Exchange.DeclareOk exchangeDeclare(String exchange, String type, boolean durable, boolean autoDelete, Map arguments) { + LOG.debug("Declaring exchange. Name = [{}], type = [{}], durable = [{}], autoDelete = [{}], args = [{}].", + exchange, type, durable, autoDelete, arguments); + try { + return channel.exchangeDeclare(exchange, type, durable, autoDelete, arguments); + } catch (IOException e) { + throw new ChannelException("exchange.declare call failed", e); + } + } + + public void basicPublish(String exchange, String routingKey, AMQP.BasicProperties props, byte[] body) { + LOG.debug("Publishing message. Exchange name = [{}], routing key = [{}]", exchange, routingKey); + try { + channel.basicPublish(exchange, routingKey, props, body); + } catch (IOException e) { + throw new ChannelException("basic.publish call failed", e); + } + } +} diff --git a/amqp/src/main/java/io/github/tcdl/msb/api/AmqpRequestOptions.java b/amqp/src/main/java/io/github/tcdl/msb/api/AmqpRequestOptions.java new file mode 100644 index 00000000..9fe5830f --- /dev/null +++ b/amqp/src/main/java/io/github/tcdl/msb/api/AmqpRequestOptions.java @@ -0,0 +1,48 @@ +package io.github.tcdl.msb.api; + +import org.apache.commons.lang3.Validate; + +import javax.annotation.Nonnull; + + +public class AmqpRequestOptions extends RequestOptions { + + private final ExchangeType exchangeType; + + private AmqpRequestOptions(Integer ackTimeout, + Integer responseTimeout, + Integer waitForResponses, + MessageTemplate messageTemplate, + String forwardNamespace, + String routingKey, + ExchangeType exchangeType) { + + super(ackTimeout, responseTimeout, waitForResponses, messageTemplate, forwardNamespace, routingKey); + this.exchangeType = exchangeType; + } + + public ExchangeType getExchangeType() { + return exchangeType; + } + + @Override + public RequestOptions.Builder asBuilder() { + return ((AmqpRequestOptions.Builder) (new Builder().from(this))).withExchangeType(this.exchangeType); + } + + public static class Builder extends RequestOptions.Builder { + + private ExchangeType exchangeType; + + public Builder withExchangeType(ExchangeType exchangeType){ + this.exchangeType = exchangeType; + return this; + } + + @Override + public RequestOptions build() { + return new AmqpRequestOptions(ackTimeout, responseTimeout, waitForResponses, messageTemplate, + forwardNamespace, routingKey, exchangeType); + } + } +} diff --git a/amqp/src/main/java/io/github/tcdl/msb/api/AmqpResponderOptions.java b/amqp/src/main/java/io/github/tcdl/msb/api/AmqpResponderOptions.java new file mode 100644 index 00000000..ced0c117 --- /dev/null +++ b/amqp/src/main/java/io/github/tcdl/msb/api/AmqpResponderOptions.java @@ -0,0 +1,55 @@ +package io.github.tcdl.msb.api; + +import org.apache.commons.lang3.Validate; + +import javax.annotation.Nonnull; +import java.util.Collections; +import java.util.Set; + + +public class AmqpResponderOptions extends ResponderOptions{ + + public static final String MATCH_ALL_BINDING_KEY = "#"; + private final ExchangeType exchangeType; + + protected AmqpResponderOptions(Set bindingKeys, + MessageTemplate messageTemplate, + ExchangeType exchangeType) { + super(bindingKeys, messageTemplate); + this.exchangeType = exchangeType; + } + + public ExchangeType getExchangeType() { + return exchangeType; + } + + public static class Builder extends ResponderOptions.Builder { + + private ExchangeType exchangeType; + + public Builder withMessageTemplate(MessageTemplate responseMessageTemplate) { + this.messageTemplate = responseMessageTemplate; + return this; + } + + public Builder withBindingKeys(Set bindingKeys) { + this.bindingKeys = bindingKeys; + return this; + } + + public Builder withExchangeType(@Nonnull ExchangeType exchangeType){ + Validate.notNull(exchangeType); + this.exchangeType = exchangeType; + return this; + } + + @Override + public ResponderOptions build() { + Set bindingKeys = this.bindingKeys == null || this.bindingKeys.isEmpty() + ? Collections.singleton(MATCH_ALL_BINDING_KEY) + : this.bindingKeys; + + return new AmqpResponderOptions(bindingKeys, messageTemplate, exchangeType); + } + } +} diff --git a/amqp/src/main/java/io/github/tcdl/msb/api/ExchangeType.java b/amqp/src/main/java/io/github/tcdl/msb/api/ExchangeType.java new file mode 100644 index 00000000..6307de9a --- /dev/null +++ b/amqp/src/main/java/io/github/tcdl/msb/api/ExchangeType.java @@ -0,0 +1,11 @@ +package io.github.tcdl.msb.api; + +public enum ExchangeType { + + FANOUT, + TOPIC; + + public String value(){ + return this.name().toLowerCase(); + } +} diff --git a/amqp/src/main/java/io/github/tcdl/msb/config/amqp/AmqpBrokerConfig.java b/amqp/src/main/java/io/github/tcdl/msb/config/amqp/AmqpBrokerConfig.java index 162e6ab8..9117c42d 100644 --- a/amqp/src/main/java/io/github/tcdl/msb/config/amqp/AmqpBrokerConfig.java +++ b/amqp/src/main/java/io/github/tcdl/msb/config/amqp/AmqpBrokerConfig.java @@ -1,11 +1,12 @@ package io.github.tcdl.msb.config.amqp; +import io.github.tcdl.msb.api.ExchangeType; +import io.github.tcdl.msb.api.exception.ConfigurationException; +import io.github.tcdl.msb.config.ConfigurationUtil; + import java.nio.charset.Charset; import java.util.Optional; -import io.github.tcdl.msb.config.ConfigurationUtil; -import io.github.tcdl.msb.api.exception.ConfigurationException; - import com.typesafe.config.Config; public class AmqpBrokerConfig { @@ -21,16 +22,16 @@ public class AmqpBrokerConfig { private Optional groupId; private final boolean durable; - private final int consumerThreadPoolSize; - private final int consumerThreadPoolQueueCapacity; - private final boolean requeueRejectedMessages; + private ExchangeType defaultExchangeType; private final int heartbeatIntervalSec; private final long networkRecoveryIntervalMs; + private final int prefetchCount; public AmqpBrokerConfig(Charset charset, String host, int port, Optional username, Optional password, Optional virtualHost, boolean useSSL, - Optional groupId, boolean durable, int consumerThreadPoolSize, int consumerThreadPoolQueueCapacity, boolean requeueRejectedMessages, - int heartbeatIntervalSec, long networkRecoveryIntervalMs) { + Optional groupId, boolean durable, + ExchangeType defaultExchangeType, + int heartbeatIntervalSec, long networkRecoveryIntervalMs, int prefetchCount) { this.charset = charset; this.port = port; this.host = host; @@ -40,11 +41,10 @@ public AmqpBrokerConfig(Charset charset, String host, int port, this.useSSL = useSSL; this.groupId = groupId; this.durable = durable; - this.consumerThreadPoolSize = consumerThreadPoolSize; - this.consumerThreadPoolQueueCapacity = consumerThreadPoolQueueCapacity; - this.requeueRejectedMessages = requeueRejectedMessages; + this.defaultExchangeType = defaultExchangeType; this.heartbeatIntervalSec = heartbeatIntervalSec; this.networkRecoveryIntervalMs = networkRecoveryIntervalMs; + this.prefetchCount = prefetchCount; } public static class AmqpBrokerConfigBuilder { @@ -57,11 +57,10 @@ public static class AmqpBrokerConfigBuilder { private boolean useSSL; private Optional groupId; private boolean durable; - private int consumerThreadPoolSize; - private int consumerThreadPoolQueueCapacity; - private boolean requeueRejectedMessages; + private ExchangeType defaultExchangeType; private int heartbeatIntervalSec; private long networkRecoveryIntervalMs; + private int prefetchCount; /** * Initialize Builder with Config @@ -86,11 +85,10 @@ public AmqpBrokerConfigBuilder withConfig(Config config) { this.groupId = ConfigurationUtil.getOptionalString(config, "groupId"); this.durable = ConfigurationUtil.getBoolean(config, "durable"); - this.consumerThreadPoolSize = ConfigurationUtil.getInt(config, "consumerThreadPoolSize"); - this.consumerThreadPoolQueueCapacity = ConfigurationUtil.getInt(config, "consumerThreadPoolQueueCapacity"); - this.requeueRejectedMessages = ConfigurationUtil.getBoolean(config, "requeueRejectedMessages"); + this.defaultExchangeType = ExchangeType.valueOf(ConfigurationUtil.getString(config, "defaultExchangeType").toUpperCase()); this.heartbeatIntervalSec = ConfigurationUtil.getInt(config, "heartbeatIntervalSec"); this.networkRecoveryIntervalMs = ConfigurationUtil.getLong(config, "networkRecoveryIntervalMs"); + this.prefetchCount = ConfigurationUtil.getInt(config, "prefetchCount"); return this; } @@ -99,7 +97,8 @@ public AmqpBrokerConfigBuilder withConfig(Config config) { */ public AmqpBrokerConfig build() { return new AmqpBrokerConfig(charset, host, port, username, password, virtualHost, useSSL, - groupId, durable, consumerThreadPoolSize, consumerThreadPoolQueueCapacity, requeueRejectedMessages, heartbeatIntervalSec, networkRecoveryIntervalMs); + groupId, durable, defaultExchangeType, + heartbeatIntervalSec, networkRecoveryIntervalMs, prefetchCount); } } @@ -139,20 +138,12 @@ public boolean isDurable() { return durable; } - public void setGroupId(Optional groupId) { - this.groupId = groupId; + public ExchangeType getDefaultExchangeType() { + return defaultExchangeType; } - public int getConsumerThreadPoolSize() { - return consumerThreadPoolSize; - } - - public int getConsumerThreadPoolQueueCapacity() { - return consumerThreadPoolQueueCapacity; - } - - public boolean isRequeueRejectedMessages() { - return requeueRejectedMessages; + public void setGroupId(Optional groupId) { + this.groupId = groupId; } public int getHeartbeatIntervalSec() { @@ -163,12 +154,17 @@ public long getNetworkRecoveryIntervalMs() { return networkRecoveryIntervalMs; } + public int getPrefetchCount() { + return prefetchCount; + } + @Override public String toString() { return String.format("AmqpBrokerConfig [charset=%s, host=%s, port=%d, username=%s, password=xxx, virtualHost=%s, useSSL=%s, groupId=%s, durable=%s, " - + "consumerThreadPoolSize=%s, consumerThreadPoolQueueCapacity=%s, requeueRejectedMessages=%s, heartbeatIntervalSec=%s, networkRecoveryIntervalMs=%s]", - charset, host, port, username, virtualHost, useSSL, groupId, durable, consumerThreadPoolSize, consumerThreadPoolQueueCapacity, requeueRejectedMessages, - heartbeatIntervalSec, networkRecoveryIntervalMs); + + "heartbeatIntervalSec=%s, " + + "networkRecoveryIntervalMs=%s, prefetchCount=%s]", + charset, host, port, username, virtualHost, useSSL, groupId, durable, + heartbeatIntervalSec, networkRecoveryIntervalMs, prefetchCount); } } \ No newline at end of file diff --git a/amqp/src/main/resources/amqp.conf b/amqp/src/main/resources/amqp.conf index 2198035e..e9b0a315 100644 --- a/amqp/src/main/resources/amqp.conf +++ b/amqp/src/main/resources/amqp.conf @@ -7,21 +7,30 @@ config.amqp = { host = ${?MSB_BROKER_HOST} port = "5672" port = ${?MSB_BROKER_PORT} + username = "guest" username = ${?MSB_BROKER_USER_NAME} + password = "guest" password = ${?MSB_BROKER_PASSWORD} + virtualHost = "/" + # For backwards compatibility. Deprecated. todo: remove in new version. virtualHost = ${?MSB_BROKER_VIRTUAL_HOST} + virtualHost = ${?MSB_BROKER_AMQP_VHOST} useSSL = false # true / false useSSL = ${?MSB_BROKER_USE_SSL} #groupId = "msb-java" durable = false - consumerThreadPoolSize = 5 - # -1 means unlimited - consumerThreadPoolQueueCapacity = 20 - requeueRejectedMessages = true + + # AMQP exchange type to be used if other is not specified by client code. + # Currently supported exchange types are "fanout" and "topic" + defaultExchangeType = "fanout" # Interval of the heartbeats that are used to detect broken connections. Zero for none. See for more details: https://www.rabbitmq.com/heartbeats.html - heartbeatIntervalSec = 1 + heartbeatIntervalSec = 30 # Interval of connection recovery attempts. See for more details: https://www.rabbitmq.com/api-guide.html#connection-recovery - networkRecoveryIntervalMs = 5000 + networkRecoveryIntervalMs = 500 + + # Specify the size of the limit of unacknowledged messages on a queue basis + prefetchCount = 10 } + diff --git a/amqp/src/test/java/io/github/tcdl/msb/adapters/amqp/AmqpAcknowledgementAdapterTest.java b/amqp/src/test/java/io/github/tcdl/msb/adapters/amqp/AmqpAcknowledgementAdapterTest.java new file mode 100644 index 00000000..68ef6278 --- /dev/null +++ b/amqp/src/test/java/io/github/tcdl/msb/adapters/amqp/AmqpAcknowledgementAdapterTest.java @@ -0,0 +1,45 @@ +package io.github.tcdl.msb.adapters.amqp; + +import com.rabbitmq.client.Channel; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class AmqpAcknowledgementAdapterTest { + private final static String MESSAGE_TEXT_ID = "id = 123"; + private final static long DELIVERY_TAG = 12337564; + + private AmqpAcknowledgementAdapter adapter; + + @Mock + private Channel channel; + + @Before + public void setUp() { + adapter = new AmqpAcknowledgementAdapter(channel, MESSAGE_TEXT_ID, DELIVERY_TAG); + } + + @Test + public void testConfirmSuccess() throws Exception { + adapter.confirm(); + verify(channel, times(1)).basicAck(DELIVERY_TAG, false); + } + + @Test + public void testRejectSuccess() throws Exception { + adapter.reject(); + verify(channel, times(1)).basicReject(DELIVERY_TAG, false); + } + + @Test + public void testRetrySuccess() throws Exception { + adapter.retry(); + verify(channel, times(1)).basicReject(DELIVERY_TAG, true); + } + +} diff --git a/amqp/src/test/java/io/github/tcdl/msb/adapters/amqp/AmqpAdapterFactoryExecutorTest.java b/amqp/src/test/java/io/github/tcdl/msb/adapters/amqp/AmqpAdapterFactoryExecutorTest.java deleted file mode 100644 index 0b440400..00000000 --- a/amqp/src/test/java/io/github/tcdl/msb/adapters/amqp/AmqpAdapterFactoryExecutorTest.java +++ /dev/null @@ -1,95 +0,0 @@ -package io.github.tcdl.msb.adapters.amqp; - -import com.rabbitmq.client.Connection; -import com.rabbitmq.client.ConnectionFactory; -import com.rabbitmq.client.Recoverable; -import com.typesafe.config.Config; -import com.typesafe.config.ConfigFactory; -import io.github.tcdl.msb.config.MsbConfig; -import org.junit.Test; - -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.withSettings; - -public class AmqpAdapterFactoryExecutorTest { - - String basicConfig = "msbConfig {" - + " timerThreadPoolSize = 1\n" - + " validateMessage = true\n" - + " brokerAdapterFactory = \"AmqpAdapterFactory\" \n" - + " serviceDetails = {" - + " name = \"test_msb\" \n" - + " version = \"1.0.1\" \n" - + " instanceId = \"msbd06a-ed59-4a39-9f95-811c5fb6ab87\" \n" - + " } \n" - + " %s" - + "}"; - - private static class MockAdapterFactory extends AmqpAdapterFactory { - @Override - protected Connection createConnection(ConnectionFactory connectionFactory) { - return mock(Connection.class, withSettings().extraInterfaces(Recoverable.class)); - } - } - - @Test - public void testCreateConsumerThreadPoolBoundedQueue() { - String brokerConf = - " brokerConfig = { " - + " charsetName = \"UTF-8\"\n" - + " consumerThreadPoolSize = 5\n" - + " consumerThreadPoolQueueCapacity = 20\n" - + " requeueRejectedMessages = true\n" - + " }"; - - Config msbConfig = ConfigFactory.parseString(String.format(basicConfig, brokerConf)); - MsbConfig msbConfigurations = new MsbConfig(msbConfig); - - AmqpAdapterFactory adapterFactory = new MockAdapterFactory(); - adapterFactory.init(msbConfigurations); - ExecutorService consumerThreadPool = adapterFactory.createConsumerThreadPool(adapterFactory.getAmqpBrokerConfig()); - - assertNotNull(consumerThreadPool); - assertTrue(consumerThreadPool instanceof ThreadPoolExecutor); - ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) consumerThreadPool; - - BlockingQueue queue = threadPoolExecutor.getQueue(); - assertNotNull(queue); - assertTrue(queue instanceof ArrayBlockingQueue); - assertEquals(20, ((ArrayBlockingQueue) queue).remainingCapacity()); - } - - @Test - public void testCreateConsumerThreadPoolUnboundedQueue() { - String brokerConf = - " brokerConfig = { " - + " charsetName = \"UTF-8\"\n" - + " consumerThreadPoolSize = 5\n" - + " consumerThreadPoolQueueCapacity = -1\n" - + " requeueRejectedMessages = true\n" - + " }"; - Config msbConfig = ConfigFactory.parseString(String.format(basicConfig, brokerConf)); - MsbConfig msbConfigurations = new MsbConfig(msbConfig); - - AmqpAdapterFactory adapterFactory = new MockAdapterFactory(); - adapterFactory.init(msbConfigurations); - ExecutorService consumerThreadPool = adapterFactory.createConsumerThreadPool(adapterFactory.getAmqpBrokerConfig()); - - assertNotNull(consumerThreadPool); - assertTrue(consumerThreadPool instanceof ThreadPoolExecutor); - ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) consumerThreadPool; - - BlockingQueue queue = threadPoolExecutor.getQueue(); - assertNotNull(queue); - assertTrue(queue instanceof LinkedBlockingQueue); - } -} \ No newline at end of file diff --git a/amqp/src/test/java/io/github/tcdl/msb/adapters/amqp/AmqpAdapterFactoryTest.java b/amqp/src/test/java/io/github/tcdl/msb/adapters/amqp/AmqpAdapterFactoryTest.java index 4d8ff3aa..473994cc 100644 --- a/amqp/src/test/java/io/github/tcdl/msb/adapters/amqp/AmqpAdapterFactoryTest.java +++ b/amqp/src/test/java/io/github/tcdl/msb/adapters/amqp/AmqpAdapterFactoryTest.java @@ -1,37 +1,34 @@ package io.github.tcdl.msb.adapters.amqp; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.rabbitmq.client.Recoverable; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; import io.github.tcdl.msb.adapters.ConsumerAdapter; -import io.github.tcdl.msb.adapters.ProducerAdapter; +import io.github.tcdl.msb.api.ExchangeType; import io.github.tcdl.msb.config.MsbConfig; import io.github.tcdl.msb.config.amqp.AmqpBrokerConfig; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; import java.io.IOException; import java.nio.charset.Charset; +import java.util.Collections; import java.util.Optional; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.TimeUnit; -import org.junit.Before; -import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.verify; -import com.rabbitmq.client.Connection; -import com.rabbitmq.client.ConnectionFactory; -import com.typesafe.config.Config; -import com.typesafe.config.ConfigFactory; +@RunWith(MockitoJUnitRunner.class) +public class AmqpAdapterFactoryTest { -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyInt; -import static org.mockito.Mockito.withSettings; + private static final Config CONFIG = ConfigFactory.load("reference.conf"); -public class AmqpAdapterFactoryTest { final Charset charset = Charset.forName("UTF-8"); final String host = "127.0.0.1"; final int port = 5672; @@ -41,72 +38,49 @@ public class AmqpAdapterFactoryTest { final boolean useSSL = false; final String groupId = "msb-java"; final boolean durable = false; - final int consumerThreadPoolSize = 5; - final int consumerThreadPoolQueueCapacity = 20; - final boolean requeueRejectedMessages = true; final int heartbeatIntervalSec = 1; final long networkRecoveryIntervalMs = 5000; - - AmqpBrokerConfig amqpConfig; - AmqpAdapterFactory amqpAdapterFactory; + final int prefetchCount = 1; + + @Mock AmqpConnectionManager mockConnectionManager; + + @Mock ConnectionFactory mockConnectionFactory; + + @Mock Connection mockConnection; - ExecutorService mockConsumerThreadPool; + + AmqpBrokerConfig amqpConfig; + AmqpAdapterFactory amqpAdapterFactory; MsbConfig msbConfigurations; - + @Before public void setUp() { - String configStr = "msbConfig {" - + " timerThreadPoolSize = 1\n" - + " brokerAdapterFactory = \"AmqpAdapterFactory\" \n" - + " validateMessage = true\n" - + " serviceDetails = {" - + " name = \"test_msb\" \n" - + " version = \"1.0.1\" \n" - + " instanceId = \"msbd06a-ed59-4a39-9f95-811c5fb6ab87\" \n" - + " } \n" - + "}"; - Config msbConfig = ConfigFactory.parseString(configStr); - msbConfigurations = new MsbConfig(msbConfig); - - mockConnectionFactory = mock(ConnectionFactory.class); - mockConnection = mock(Connection.class, withSettings().extraInterfaces(Recoverable.class)); - mockConnectionManager = mock(AmqpConnectionManager.class); - mockConsumerThreadPool = mock(ExecutorService.class); - - //Define conditions for ExecutorService termination - try { - when(mockConsumerThreadPool.awaitTermination(anyInt(), any(TimeUnit.class))).thenReturn(true); - } catch (InterruptedException e) { - fail("Can't create mockConsumerThreadPool"); - } - + + msbConfigurations = new MsbConfig(CONFIG); + amqpConfig = new AmqpBrokerConfig(charset, host, port, - Optional.of(username), Optional.of(password), Optional.of(virtualHost), useSSL, Optional.of(groupId), durable, - consumerThreadPoolSize, consumerThreadPoolQueueCapacity, requeueRejectedMessages, heartbeatIntervalSec, networkRecoveryIntervalMs); - + Optional.of(username), Optional.of(password), Optional.of(virtualHost), useSSL, Optional.of(groupId), durable, ExchangeType.FANOUT, + heartbeatIntervalSec, networkRecoveryIntervalMs, prefetchCount); + amqpAdapterFactory = new AmqpAdapterFactory() { - @Override - public ProducerAdapter createProducerAdapter(String topic) { - return new AmqpProducerAdapter(topic, amqpConfig, mockConnectionManager); - } @Override - public ConsumerAdapter createConsumerAdapter(String topic) { - return new AmqpConsumerAdapter(topic, amqpConfig, mockConnectionManager, mockConsumerThreadPool); + public AmqpConsumerAdapter createConsumerAdapter(String topic, boolean isResponseTopic) { + return new AmqpConsumerAdapter(topic, ExchangeType.FANOUT, Collections.emptySet(), amqpConfig, mockConnectionManager, isResponseTopic); } @Override protected ConnectionFactory createConnectionFactory() { return mockConnectionFactory; } - + @Override protected AmqpBrokerConfig createAmqpBrokerConfig(MsbConfig msbConfig) { return amqpConfig; } - + @Override protected AmqpConnectionManager createConnectionManager(Connection connection) { return mockConnectionManager; @@ -117,10 +91,6 @@ protected Connection createConnection(ConnectionFactory connectionFactory) { return mockConnection; } - @Override - protected ExecutorService createConsumerThreadPool(AmqpBrokerConfig amqpBrokerConfig) { - return mockConsumerThreadPool; - } }; } @@ -139,7 +109,6 @@ public void testInit() { amqpAdapterFactory.init(msbConfigurations); assertEquals(amqpAdapterFactory.getAmqpBrokerConfig(), amqpConfig); assertEquals(amqpAdapterFactory.getConnectionManager(), mockConnectionManager); - assertEquals(amqpAdapterFactory.getConsumerThreadPool(), mockConsumerThreadPool); } @Test @@ -152,7 +121,6 @@ public void testInitGroupIdWithServiceName() { public void testShutdown() { amqpAdapterFactory.init(msbConfigurations); amqpAdapterFactory.shutdown(); - verify(mockConsumerThreadPool).shutdown(); try { verify(mockConnectionManager).close(); } catch (IOException e) { diff --git a/amqp/src/test/java/io/github/tcdl/msb/adapters/amqp/AmqpConsumerAdapterTest.java b/amqp/src/test/java/io/github/tcdl/msb/adapters/amqp/AmqpConsumerAdapterTest.java index 7d0fd43a..1cedd268 100644 --- a/amqp/src/test/java/io/github/tcdl/msb/adapters/amqp/AmqpConsumerAdapterTest.java +++ b/amqp/src/test/java/io/github/tcdl/msb/adapters/amqp/AmqpConsumerAdapterTest.java @@ -1,69 +1,117 @@ package io.github.tcdl.msb.adapters.amqp; +import com.google.common.collect.Sets; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.Consumer; -import io.github.tcdl.msb.config.amqp.AmqpBrokerConfig; import io.github.tcdl.msb.adapters.ConsumerAdapter; +import io.github.tcdl.msb.api.ExchangeType; +import io.github.tcdl.msb.api.ResponderOptions; +import io.github.tcdl.msb.api.exception.ChannelException; +import io.github.tcdl.msb.config.amqp.AmqpBrokerConfig; +import org.apache.commons.collections.CollectionUtils; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; import java.io.IOException; import java.nio.charset.Charset; +import java.util.Collections; import java.util.Optional; -import java.util.concurrent.ExecutorService; +import java.util.Set; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyBoolean; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; public class AmqpConsumerAdapterTest { private Channel mockChannel; private AmqpConnectionManager mockAmqpConnectionManager; - private ExecutorService mockConsumerThreadPool; + @Before public void setUp() throws Exception { Connection mockConnection = mock(Connection.class); mockChannel = mock(Channel.class); mockAmqpConnectionManager = mock(AmqpConnectionManager.class); - + when(mockAmqpConnectionManager.obtainConnection()).thenReturn(mockConnection); when(mockConnection.createChannel()).thenReturn(mockChannel); - - mockConsumerThreadPool = mock(ExecutorService.class); } @Test - public void testTopicExchangeCreated() throws Exception { + public void testFanoutExchangeCreated() throws Exception { String topicName = "myTopic"; - AmqpConsumerAdapter adapter = createAdapter(topicName, "myGroupId", false); + String groupId = "groupId"; + AmqpConsumerAdapter adapter = createAdapterWithNonDurableConf(topicName, groupId, false); - adapter.subscribe(jsonMessage -> { + adapter.subscribe((jsonMessage, ackHandler) -> { }); verify(mockChannel).exchangeDeclare(topicName, "fanout", false, true, null); } + @Test + public void testTopicExchangeCreated() throws Exception { + String topicName = "myTopic"; + String groupId = "groupId"; + String bindingKey = "binding-key"; + + new AmqpConsumerAdapter(topicName, ExchangeType.TOPIC, Collections.singleton(bindingKey), brokerConfig(groupId, true), mockAmqpConnectionManager, true); + verify(mockChannel).exchangeDeclare(topicName, "topic", false, true, null); + + } + @Test(expected = RuntimeException.class) public void testInitializationError() throws IOException { when(mockChannel.exchangeDeclare(anyString(), anyString(), anyBoolean(), anyBoolean(), any())).thenThrow(new IOException()); - createAdapter("myTopic", "myGroupId", false); + createAdapterWithNonDurableConf("myTopic", "myGroupId", false); + } + + @Test + public void testSubscribeMultipleRoutingKeysMultipleBindings() throws Exception { + String topicName = "myTopic"; + String groupId = "groupId"; + + Set bindingKeys = Sets.newHashSet("routing-key-1", "routing-key-2"); + + AmqpConsumerAdapter amqpConsumerAdapter = new AmqpConsumerAdapter(topicName, ExchangeType.TOPIC, bindingKeys, brokerConfig(groupId, true), mockAmqpConnectionManager, false); + amqpConsumerAdapter.subscribe((jsonMessage, acknowledgementHandler) -> { + }); + + ArgumentCaptor routingKeysCaptor = ArgumentCaptor.forClass(String.class); + verify(mockChannel, times(2)).queueBind(eq("myTopic.groupId.d"), eq(topicName), routingKeysCaptor.capture()); + + assertTrue(CollectionUtils.isEqualCollection(bindingKeys, routingKeysCaptor.getAllValues())); } @Test public void testSubscribeTransientQueueCreated() throws IOException { - AmqpConsumerAdapter adapter = createAdapter("myTopic", "myGroupId", false); - - adapter.subscribe(jsonMessage -> { + AmqpConsumerAdapter adapter = createAdapterWithNonDurableConf("myTopic", "myGroupId", false); + + adapter.subscribe((jsonMessage, ackHandler) -> { + }); + + // Verify that the queue has been declared with correct name and settings + verify(mockChannel).queueDeclare("myTopic.myGroupId.t", /* queue name */ + false, /* durable */ + false, /* exclusive */ + true, /* auto-delete */ + null); + // Verify that the queue has been bound to the exchange + verify(mockChannel).queueBind("myTopic.myGroupId.t", "myTopic", ""); + } + + @Test + public void testSubscribeTransientQueueCreatedWhenIsResponseTopic() throws IOException { + AmqpConsumerAdapter adapter = createAdapterWithDurableConf("myTopic", "myGroupId", true); + + adapter.subscribe((jsonMessage, ackHandler) -> { }); // Verify that the queue has been declared with correct name and settings @@ -78,9 +126,9 @@ public void testSubscribeTransientQueueCreated() throws IOException { @Test public void testSubscribeDurableQueueCreated() throws IOException { - AmqpConsumerAdapter adapter = createAdapter("myTopic", "myGroupId", true); + AmqpConsumerAdapter adapter = createAdapterWithDurableConf("myTopic", "myGroupId", false); - adapter.subscribe(jsonMessage -> { + adapter.subscribe((jsonMessage, ackHandler) -> { }); // Verify that the queue has been declared with correct name and settings @@ -93,9 +141,19 @@ public void testSubscribeDurableQueueCreated() throws IOException { verify(mockChannel).queueBind("myTopic.myGroupId.d", "myTopic", ""); } + + @Test(expected = ChannelException.class) + public void testSubscribeException() throws IOException { + AmqpConsumerAdapter adapter = createAdapterWithDurableConf("myTopic", "myGroupId", false); + when(mockChannel.basicConsume(anyString(), anyBoolean(), any(AmqpMessageConsumer.class))) + .thenThrow(IOException.class); + adapter.subscribe((jsonMessage, ackHandler) -> { + }); + } + @Test public void testRegisteredHandlerInvoked() throws IOException { - AmqpConsumerAdapter adapter = createAdapter("myTopic", "myGroupId", false); + AmqpConsumerAdapter adapter = createAdapterWithNonDurableConf("myTopic", "myGroupId", false); ConsumerAdapter.RawMessageHandler mockHandler = mock(ConsumerAdapter.RawMessageHandler.class); adapter.subscribe(mockHandler); @@ -106,26 +164,216 @@ public void testRegisteredHandlerInvoked() throws IOException { AmqpMessageConsumer consumer = amqpConsumerCaptor.getValue(); assertEquals(mockChannel, consumer.getChannel()); - assertEquals(mockConsumerThreadPool, consumer.consumerThreadPool); assertEquals(mockHandler, consumer.msgHandler); } @Test public void testUnsubscribe() throws IOException { - AmqpConsumerAdapter adapter = createAdapter("myTopic", "myGroupId", false); + AmqpConsumerAdapter adapter = createAdapterWithNonDurableConf("myTopic", "myGroupId", false); String consumerTag = "my consumer tag"; when(mockChannel.basicConsume(anyString(), anyBoolean(), any(Consumer.class))).thenReturn(consumerTag); - adapter.subscribe(jsonMessage -> { + adapter.subscribe((jsonMessage, ackHandler) -> { }); adapter.unsubscribe(); verify(mockChannel).basicCancel(consumerTag); } - private AmqpConsumerAdapter createAdapter(String topic, String groupId, boolean durable) { + @Test(expected = ChannelException.class) + public void testUnsubscribeException() throws IOException { + AmqpConsumerAdapter adapter = createAdapterWithNonDurableConf("myTopic", "myGroupId", false); + String consumerTag = "my consumer tag"; + when(mockChannel.basicConsume(anyString(), anyBoolean(), any(Consumer.class))) + .thenThrow(IOException.class); + adapter.subscribe((jsonMessage, ackHandler) -> { + }); + adapter.unsubscribe(); + } + + @Test + public void testIsDurableFalseIfResponseTopicAndNonDurableConfig() throws IOException { + boolean isResponseTopic = true; + + AmqpConsumerAdapter adapter = createAdapterWithNonDurableConf("myTopic", "myGroupId", isResponseTopic); + + assertFalse(adapter.isDurable()); + } + + @Test + public void testIsDurableFalseIfNotResponseTopicAndNonDurableConfig() throws IOException { + boolean isResponseTopic = false; + + AmqpConsumerAdapter adapter = createAdapterWithNonDurableConf("myTopic", "myGroupId", isResponseTopic); + + assertFalse(adapter.isDurable()); + } + + @Test + public void testIsDurableFalseIfResponseTopicAndDurableConfig() throws IOException { + boolean isResponseTopic = true; + + AmqpConsumerAdapter adapter = createAdapterWithDurableConf("myTopic", "myGroupId", isResponseTopic); + + assertFalse(adapter.isDurable()); + } + + @Test + public void testIsDurableTrueIfNotResponseTopicAndDurableConfig() throws IOException { + boolean isResponseTopic = false; + + AmqpConsumerAdapter adapter = createAdapterWithDurableConf("myTopic", "myGroupId", isResponseTopic); + + assertTrue(adapter.isDurable()); + } + + @Test + public void testMessageCount() throws Exception { + String topicName = "myTopic"; + String groupId = "groupId"; + AmqpConsumerAdapter adapter = createAdapterWithNonDurableConf(topicName, groupId, false); + + when(mockChannel.messageCount(anyString())).thenReturn(42L); + + adapter.subscribe((jsonMessage, ackHandler) -> { + }); + + verify(mockChannel).exchangeDeclare(topicName, "fanout", false, true, null); + + Optional answer = Optional.of(42L); + Optional result = adapter.messageCount(); + verify(mockChannel, times(1)).messageCount(anyString()); + assertEquals(answer, result); + } + + @Test + public void testMessageCountNeverSubscribed() throws Exception { + String topicName = "myTopic"; + String groupId = "groupId"; + AmqpConsumerAdapter adapter = createAdapterWithNonDurableConf(topicName, groupId, false); + + + Optional result = adapter.messageCount(); + verify(mockChannel, never()).messageCount(anyString()); + assertEquals(Optional.empty(), result); + } + + @Test + public void testMessageCountAfterUnsubscribe() throws Exception { + String topicName = "myTopic"; + String groupId = "groupId"; + AmqpConsumerAdapter adapter = createAdapterWithNonDurableConf(topicName, groupId, false); + + when(mockChannel.messageCount(anyString())).thenReturn(42L); + + adapter.subscribe((jsonMessage, ackHandler) -> { + }); + + verify(mockChannel).exchangeDeclare(topicName, "fanout", false, true, null); + + Optional expectedAnswerWhileSubscribed = Optional.of(42L); + Optional expectedAnswerWhileUnsubscribed = Optional.empty(); + + Optional resultWhileSubscribed = adapter.messageCount(); + + adapter.unsubscribe(); + + Optional resultWhileUnsubscribed = adapter.messageCount(); + + verify(mockChannel, times(1)).messageCount(anyString()); + assertEquals(expectedAnswerWhileSubscribed, resultWhileSubscribed); + assertEquals(expectedAnswerWhileUnsubscribed, resultWhileUnsubscribed); + } + + @Test + public void testIsConnected() throws Exception { + String topicName = "myTopic"; + String groupId = "groupId"; + AmqpConsumerAdapter adapter = createAdapterWithNonDurableConf(topicName, groupId, false); + + Connection mockConnection = mock(Connection.class); + when(mockConnection.isOpen()).thenReturn(true); + when(mockChannel.isOpen()).thenReturn(true); + when(mockChannel.getConnection()).thenReturn(mockConnection); + when(mockChannel.consumerCount(anyString())).thenReturn(1L); + + adapter.subscribe((jsonMessage, ackHandler) -> { + }); + + verify(mockChannel).exchangeDeclare(topicName, "fanout", false, true, null); + + Optional answer = Optional.of(true); + Optional result = adapter.isConnected(); + verify(mockChannel, times(1)).isOpen(); + verify(mockConnection, times(1)).isOpen(); + verify(mockChannel, times(1)).consumerCount(anyString()); + assertEquals(answer, result); + } + + @Test + public void testIsConnectedNeverSubscribed() throws Exception { + String topicName = "myTopic"; + String groupId = "groupId"; + AmqpConsumerAdapter adapter = createAdapterWithNonDurableConf(topicName, groupId, false); + + Optional result = adapter.isConnected(); + verify(mockChannel, never()).isOpen(); + assertEquals(Optional.empty(), result); + } + + @Test + public void testIsConnectedAfterUnsubscribe() throws Exception { + String topicName = "myTopic"; + String groupId = "groupId"; + AmqpConsumerAdapter adapter = createAdapterWithNonDurableConf(topicName, groupId, false); + + Connection mockConnection = mock(Connection.class); + when(mockConnection.isOpen()).thenReturn(true); + when(mockChannel.isOpen()).thenReturn(true); + when(mockChannel.getConnection()).thenReturn(mockConnection); + when(mockChannel.consumerCount(anyString())).thenReturn(1L); + + adapter.subscribe((jsonMessage, ackHandler) -> { + }); + + verify(mockChannel).exchangeDeclare(topicName, "fanout", false, true, null); + + Optional expectedAnswerWhileSubscribed = Optional.of(true); + Optional expectedAnswerWhileUnsubscribed = Optional.empty(); + + Optional resultWhileSubscribed = adapter.isConnected(); + + adapter.unsubscribe(); + + Optional resultWhileUnsubscribed = adapter.isConnected(); + + verify(mockChannel, times(1)).isOpen(); + verify(mockConnection, times(1)).isOpen(); + verify(mockChannel, times(1)).consumerCount(anyString()); + + assertEquals(expectedAnswerWhileSubscribed, resultWhileSubscribed); + assertEquals(expectedAnswerWhileUnsubscribed, resultWhileUnsubscribed); + } + + private AmqpConsumerAdapter createAdapterWithNonDurableConf(String topic, String groupId, boolean isResponseTopic) { + boolean isDurableConf = false; + AmqpBrokerConfig nondurableAmqpConfig = new AmqpBrokerConfig(Charset.forName("UTF-8"), "127.0.0.1", 10, Optional.empty(), Optional.empty(), Optional.empty(), + false, Optional.of(groupId), isDurableConf, ExchangeType.FANOUT, 1, 5000, 1); + return new AmqpConsumerAdapter(topic, ExchangeType.FANOUT, ResponderOptions.DEFAULTS.getBindingKeys(), nondurableAmqpConfig, mockAmqpConnectionManager, isResponseTopic); + } + + private AmqpConsumerAdapter createAdapterWithDurableConf(String topic, String groupId, boolean isResponseTopic) { + boolean isDurableConf = true; AmqpBrokerConfig nondurableAmqpConfig = new AmqpBrokerConfig(Charset.forName("UTF-8"), "127.0.0.1", 10, Optional.empty(), Optional.empty(), Optional.empty(), - false, Optional.of(groupId), durable, 5, 20, true, 1, 5000); - return new AmqpConsumerAdapter(topic, nondurableAmqpConfig, mockAmqpConnectionManager, mockConsumerThreadPool); + false, Optional.of(groupId), isDurableConf, ExchangeType.FANOUT, 1, 5000, 1); + return new AmqpConsumerAdapter(topic, ExchangeType.FANOUT, ResponderOptions.DEFAULTS.getBindingKeys(), nondurableAmqpConfig, mockAmqpConnectionManager, isResponseTopic); + } + + private AmqpBrokerConfig brokerConfig(String groupId, boolean durable) { + return new AmqpBrokerConfig( + Charset.forName("UTF-8"), + "127.0.0.1", 10, Optional.empty(), Optional.empty(), Optional.empty(), + false, Optional.of(groupId), durable, ExchangeType.FANOUT, 1, 5000, 1 + ); } } \ No newline at end of file diff --git a/amqp/src/test/java/io/github/tcdl/msb/adapters/amqp/AmqpMessageConsumerTest.java b/amqp/src/test/java/io/github/tcdl/msb/adapters/amqp/AmqpMessageConsumerTest.java index 804d33f6..aeca9ee5 100644 --- a/amqp/src/test/java/io/github/tcdl/msb/adapters/amqp/AmqpMessageConsumerTest.java +++ b/amqp/src/test/java/io/github/tcdl/msb/adapters/amqp/AmqpMessageConsumerTest.java @@ -2,49 +2,53 @@ import com.rabbitmq.client.Channel; import com.rabbitmq.client.Envelope; +import io.github.tcdl.msb.acknowledge.AcknowledgementHandlerImpl; import io.github.tcdl.msb.adapters.ConsumerAdapter; import io.github.tcdl.msb.config.amqp.AmqpBrokerConfig; import org.junit.Before; import org.junit.Test; -import org.mockito.ArgumentCaptor; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; import java.io.IOException; import java.nio.charset.Charset; -import java.util.concurrent.ExecutorService; import java.util.concurrent.RejectedExecutionException; -import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyBoolean; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.*; +@RunWith(MockitoJUnitRunner.class) public class AmqpMessageConsumerTest { - private static final boolean REQUEUE_REJECTED_MESSAGES = true; - + @Mock private Channel mockChannel; - private ExecutorService mockExecutorService; + + @Mock private ConsumerAdapter.RawMessageHandler mockMessageHandler; + + @Mock private AmqpBrokerConfig mockBrokerConfig; + @Mock + private AcknowledgementHandlerImpl amqpAcknowledgementHandler; + private AmqpMessageConsumer amqpMessageConsumer; @Before public void setUp() { - mockChannel = mock(Channel.class); - mockExecutorService = mock(ExecutorService.class); - mockMessageHandler = mock(ConsumerAdapter.RawMessageHandler.class); - mockBrokerConfig = mock(AmqpBrokerConfig.class); - when(mockBrokerConfig.getCharset()).thenReturn(Charset.forName("UTF-8")); - when(mockBrokerConfig.isRequeueRejectedMessages()).thenReturn(REQUEUE_REJECTED_MESSAGES); - amqpMessageConsumer = new AmqpMessageConsumer(mockChannel, mockExecutorService, mockMessageHandler, mockBrokerConfig); + amqpMessageConsumer = new AmqpMessageConsumer(mockChannel, mockMessageHandler, mockBrokerConfig) { + @Override + AcknowledgementHandlerImpl createAcknowledgementHandler(Channel channel, String consumerTag, long deliveryTag, boolean isRequeueRejectedMessages) { + return amqpAcknowledgementHandler; + } + }; } @Test @@ -58,20 +62,8 @@ public void testMessageProcessing() throws IOException { // method under test amqpMessageConsumer.handleDelivery(consumerTag, envelope, null, messageStr.getBytes()); - // verify that a new task has been submitted - ArgumentCaptor taskCaptor = ArgumentCaptor.forClass(AmqpMessageProcessingTask.class); - verify(mockExecutorService).submit(taskCaptor.capture()); + verify(mockMessageHandler, times(1)).onMessage(eq(messageStr), eq(amqpAcknowledgementHandler)); - // verify that the right task was submitted - AmqpMessageProcessingTask task = taskCaptor.getValue(); - assertEquals(consumerTag, task.consumerTag); - assertEquals(messageStr, task.body); - assertEquals(deliveryTag, task.deliveryTag); - assertEquals(mockMessageHandler, task.msgHandler); - assertEquals(mockChannel, task.channel); - - // verify that ack has been sent - mockChannel.basicAck(deliveryTag, false); } @Test @@ -80,11 +72,28 @@ public void testMessageCannotBeSubmittedForProcessing() throws IOException { Envelope envelope = mock(Envelope.class); when(envelope.getDeliveryTag()).thenReturn(deliveryTag); - doThrow(new RejectedExecutionException()).when(mockExecutorService).submit(any(Runnable.class)); + doThrow(new RejectedExecutionException()).when(mockMessageHandler).onMessage(anyString(), any()); + + try { + amqpMessageConsumer.handleDelivery("consumer tag", envelope, null, "some message".getBytes()); + verify(amqpAcknowledgementHandler, times(1)).autoReject(); + } catch (Exception e) { + fail(); + } + } + + @Test + public void testMessageCannotBeProcessedBeforeSubmit() throws IOException { + long deliveryTag = 1234L; + Envelope envelope = mock(Envelope.class); + when(envelope.getDeliveryTag()).thenReturn(deliveryTag); + + when(mockBrokerConfig.getCharset()).thenThrow( + new RuntimeException("Something really unexpected happened even before task submit attempt")); try { amqpMessageConsumer.handleDelivery("consumer tag", envelope, null, "some message".getBytes()); - verify(mockChannel).basicReject(deliveryTag, REQUEUE_REJECTED_MESSAGES); + verify(amqpAcknowledgementHandler, times(1)).autoReject(); } catch (Exception e) { fail(); } @@ -96,12 +105,12 @@ public void testRejectFailed() throws IOException { Envelope envelope = mock(Envelope.class); when(envelope.getDeliveryTag()).thenReturn(deliveryTag); - doThrow(new RejectedExecutionException()).when(mockExecutorService).submit(any(Runnable.class)); + doThrow(new RejectedExecutionException()).when(mockMessageHandler).onMessage(anyString(), any()); doThrow(new RuntimeException()).when(mockChannel).basicReject(eq(deliveryTag), anyBoolean()); try { amqpMessageConsumer.handleDelivery("consumer tag", envelope, null, "some message".getBytes()); - verify(mockChannel).basicReject(eq(deliveryTag), anyBoolean()); + verify(amqpAcknowledgementHandler, times(1)).autoReject(); } catch (Exception e) { fail(); } @@ -117,12 +126,11 @@ public void testProperCharsetUsed() throws IOException { Envelope envelope = mock(Envelope.class); when(envelope.getDeliveryTag()).thenReturn(1234L); - AmqpMessageConsumer consumer = new AmqpMessageConsumer(mockChannel, mockExecutorService, mockMessageHandler, mockBrokerConfig); + AmqpMessageConsumer consumer = new AmqpMessageConsumer(mockChannel, mockMessageHandler, mockBrokerConfig); consumer.handleDelivery("some tag", envelope, null, encodedMessage); - ArgumentCaptor taskCaptor = ArgumentCaptor.forClass(AmqpMessageProcessingTask.class); - verify(mockExecutorService).submit(taskCaptor.capture()); - assertEquals(expectedDecodedMessage, taskCaptor.getValue().body); + verify(mockMessageHandler, times(1)).onMessage(eq(expectedDecodedMessage), any()); + } } \ No newline at end of file diff --git a/amqp/src/test/java/io/github/tcdl/msb/adapters/amqp/AmqpMessageProcessingTaskTest.java b/amqp/src/test/java/io/github/tcdl/msb/adapters/amqp/AmqpMessageProcessingTaskTest.java deleted file mode 100644 index 7a76ab05..00000000 --- a/amqp/src/test/java/io/github/tcdl/msb/adapters/amqp/AmqpMessageProcessingTaskTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package io.github.tcdl.msb.adapters.amqp; - -import com.rabbitmq.client.Channel; -import io.github.tcdl.msb.adapters.ConsumerAdapter; -import org.junit.Before; -import org.junit.Test; - -import java.io.IOException; - -import static org.junit.Assert.fail; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; - -public class AmqpMessageProcessingTaskTest { - - private String messageStr = "some message"; - - private Channel mockChannel; - private ConsumerAdapter.RawMessageHandler mockMessageHandler; - - private AmqpMessageProcessingTask task; - - @Before - public void setUp() { - mockChannel = mock(Channel.class); - mockMessageHandler = mock(ConsumerAdapter.RawMessageHandler.class); - task = new AmqpMessageProcessingTask("consumer tag", messageStr, mockChannel, 123L, mockMessageHandler); - } - - @Test - public void testMessageProcessing() throws IOException { - task.run(); - - verify(mockMessageHandler).onMessage(messageStr); - } - - @Test - public void testExceptionDuringProcessing() { - doThrow(new RuntimeException()).when(mockMessageHandler).onMessage(anyString()); - - try { - task.run(); - // Verify that AMQP ack has not been sent - verifyNoMoreInteractions(mockChannel); - } catch (Exception e) { - fail(); - } - } -} \ No newline at end of file diff --git a/amqp/src/test/java/io/github/tcdl/msb/adapters/amqp/AmqpProducerAdapterTest.java b/amqp/src/test/java/io/github/tcdl/msb/adapters/amqp/AmqpProducerAdapterTest.java index 7fb18aae..b163a5c2 100644 --- a/amqp/src/test/java/io/github/tcdl/msb/adapters/amqp/AmqpProducerAdapterTest.java +++ b/amqp/src/test/java/io/github/tcdl/msb/adapters/amqp/AmqpProducerAdapterTest.java @@ -4,8 +4,10 @@ import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.MessageProperties; +import io.github.tcdl.msb.api.ExchangeType; import io.github.tcdl.msb.api.exception.ChannelException; import io.github.tcdl.msb.config.amqp.AmqpBrokerConfig; +import org.apache.commons.lang3.StringUtils; import org.junit.Before; import org.junit.Test; import org.mockito.AdditionalMatchers; @@ -16,11 +18,10 @@ import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyBoolean; import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; public class AmqpProducerAdapterTest { + public static final String TOPIC_NAME = "myTopic"; private Channel mockChannel; private AmqpConnectionManager mockAmqpConnectionManager; private AmqpBrokerConfig mockAmqpBrokerConfig; @@ -39,29 +40,44 @@ public void setUp() throws IOException { } @Test - public void testExchangeCreated() throws IOException { - String topicName = "myTopic"; + public void testExchangeWithCorrectTypeCreated() throws IOException { + new AmqpProducerAdapter(TOPIC_NAME, ExchangeType.FANOUT, mockAmqpBrokerConfig, mockAmqpConnectionManager); + verify(mockChannel).exchangeDeclare(TOPIC_NAME, "fanout", false, true, null); - new AmqpProducerAdapter(topicName, mockAmqpBrokerConfig, mockAmqpConnectionManager); - - verify(mockChannel).exchangeDeclare(topicName, "fanout", false, true, null); + new AmqpProducerAdapter(TOPIC_NAME, ExchangeType.TOPIC, mockAmqpBrokerConfig, mockAmqpConnectionManager); + verify(mockChannel).exchangeDeclare(TOPIC_NAME, "topic", false, true, null); } @Test(expected = RuntimeException.class) public void testInitializationError() throws IOException { when(mockChannel.exchangeDeclare(anyString(), anyString(), anyBoolean(), anyBoolean(), any())).thenThrow(new IOException()); - new AmqpProducerAdapter("myTopic", mockAmqpBrokerConfig, mockAmqpConnectionManager); + new AmqpProducerAdapter(TOPIC_NAME, ExchangeType.FANOUT, mockAmqpBrokerConfig, mockAmqpConnectionManager); } @Test public void testPublish() throws ChannelException, IOException { - String topicName = "myTopic"; String message = "message"; - AmqpProducerAdapter producerAdapter = new AmqpProducerAdapter(topicName, mockAmqpBrokerConfig, mockAmqpConnectionManager); + AmqpProducerAdapter producerAdapter = new AmqpProducerAdapter(TOPIC_NAME, ExchangeType.FANOUT, mockAmqpBrokerConfig, mockAmqpConnectionManager); + producerAdapter.publish(message); + verify(mockChannel).basicPublish(TOPIC_NAME, StringUtils.EMPTY, MessageProperties.PERSISTENT_BASIC, message.getBytes()); + } + @Test(expected = ChannelException.class) + public void testPublishExceptionally() throws Exception { + String message = "message"; + AmqpProducerAdapter producerAdapter = new AmqpProducerAdapter(TOPIC_NAME, ExchangeType.FANOUT, mockAmqpBrokerConfig, mockAmqpConnectionManager); + doThrow(new RuntimeException()).when(mockChannel).basicPublish(anyString(), anyString(), any(AMQP.BasicProperties.class), any(byte[].class)); producerAdapter.publish(message); + } + + @Test + public void testPublishWithRoutingKey() throws Exception{ + String message = "message"; + String routingKey = "routingKey"; + AmqpProducerAdapter producerAdapter = new AmqpProducerAdapter(TOPIC_NAME, ExchangeType.TOPIC, mockAmqpBrokerConfig, mockAmqpConnectionManager); - verify(mockChannel).basicPublish(topicName, "" /* routing key */, MessageProperties.PERSISTENT_BASIC, message.getBytes()); + producerAdapter.publish(message, routingKey); + verify(mockChannel).basicPublish(TOPIC_NAME, routingKey, MessageProperties.PERSISTENT_BASIC, message.getBytes()); } @Test @@ -70,7 +86,7 @@ public void testProperCharsetUsed() throws IOException { String message = "ö"; byte[] expectedEncodedMessage = new byte[] { 0, 0, 0, -10 }; // In UTF-32 ö is mapped to 000000f6 - AmqpProducerAdapter producerAdapter = new AmqpProducerAdapter("myTopic", mockAmqpBrokerConfig, mockAmqpConnectionManager); + AmqpProducerAdapter producerAdapter = new AmqpProducerAdapter("myTopic", ExchangeType.FANOUT, mockAmqpBrokerConfig, mockAmqpConnectionManager); producerAdapter.publish(message); diff --git a/amqp/src/test/java/io/github/tcdl/msb/api/AmqpResponderOptionsBuilderTest.java b/amqp/src/test/java/io/github/tcdl/msb/api/AmqpResponderOptionsBuilderTest.java new file mode 100644 index 00000000..8f902d88 --- /dev/null +++ b/amqp/src/test/java/io/github/tcdl/msb/api/AmqpResponderOptionsBuilderTest.java @@ -0,0 +1,16 @@ +package io.github.tcdl.msb.api; + +import org.junit.Test; + +import java.util.Collections; + +import static org.junit.Assert.*; + +public class AmqpResponderOptionsBuilderTest { + + @Test + public void build_shouldSetHashBindingKeyByDefault() throws Exception { + ResponderOptions responderOptions = new AmqpResponderOptions.Builder().build(); + assertEquals(Collections.singleton("#"), responderOptions.getBindingKeys()); + } +} \ No newline at end of file diff --git a/amqp/src/test/java/io/github/tcdl/msb/config/amqp/AmqpBrokerConfigTest.java b/amqp/src/test/java/io/github/tcdl/msb/config/amqp/AmqpBrokerConfigTest.java index 4b4caf00..a295ba9b 100644 --- a/amqp/src/test/java/io/github/tcdl/msb/config/amqp/AmqpBrokerConfigTest.java +++ b/amqp/src/test/java/io/github/tcdl/msb/config/amqp/AmqpBrokerConfigTest.java @@ -1,16 +1,17 @@ package io.github.tcdl.msb.config.amqp; -import com.typesafe.config.Config; -import com.typesafe.config.ConfigFactory; -import io.github.tcdl.msb.api.exception.ConfigurationException; -import org.junit.Test; - -import java.nio.charset.Charset; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import io.github.tcdl.msb.api.exception.ConfigurationException; + +import java.nio.charset.Charset; + +import org.junit.Test; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; public class AmqpBrokerConfigTest { @@ -23,11 +24,10 @@ public class AmqpBrokerConfigTest { final boolean useSSL = false; final String groupId = "msb-java"; final boolean durable = false; - final int consumerThreadPoolSize = 5; - final int consumerThreadPoolQueueCapacity = 20; - final boolean requeueRejectedMessages = true; + final String exchangeType = "fanout"; final int heartbeatIntervalSec = 1; final long networkRecoveryIntervalMs = 5000; + final int prefetchCount = 1; @Test public void testBuildAmqpBrokerConfig() { @@ -41,11 +41,10 @@ public void testBuildAmqpBrokerConfig() { + " useSSL = \"" + useSSL + "\"\n" + " groupId = \"" + groupId + "\"\n" + " durable = " + durable + "\n" - + " consumerThreadPoolSize = " + consumerThreadPoolSize + "\n" - + " consumerThreadPoolQueueCapacity = " + consumerThreadPoolQueueCapacity + "\n" - + " requeueRejectedMessages = " + requeueRejectedMessages + "\n" + + " defaultExchangeType = " + exchangeType + "\n" + " heartbeatIntervalSec = " + heartbeatIntervalSec + "\n" + " networkRecoveryIntervalMs = " + networkRecoveryIntervalMs + "\n" + + " prefetchCount = " + prefetchCount + "\n" + "}"; Config amqpConfig = ConfigFactory.parseString(configStr).getConfig("config.amqp"); @@ -57,9 +56,6 @@ public void testBuildAmqpBrokerConfig() { assertEquals(brokerConfig.getPort(), port); assertEquals(brokerConfig.getGroupId().get(), groupId); assertEquals(brokerConfig.isDurable(), durable); - assertEquals(brokerConfig.getConsumerThreadPoolSize(), consumerThreadPoolSize); - assertEquals(brokerConfig.getConsumerThreadPoolQueueCapacity(), consumerThreadPoolQueueCapacity); - assertEquals(brokerConfig.isRequeueRejectedMessages(), requeueRejectedMessages); assertEquals(brokerConfig.getUsername().get(), username); assertEquals(brokerConfig.getPassword().get(), password); @@ -68,6 +64,9 @@ public void testBuildAmqpBrokerConfig() { assertEquals(heartbeatIntervalSec, brokerConfig.getHeartbeatIntervalSec()); assertEquals(networkRecoveryIntervalMs, brokerConfig.getNetworkRecoveryIntervalMs()); + + assertEquals(prefetchCount, brokerConfig.getPrefetchCount()); + } @Test @@ -78,11 +77,10 @@ public void testOptionalConfigurationOptions() { + " port = \"" + port + "\"\n" + " useSSL = \"" + useSSL + "\"\n" + " durable = " + durable + "\n" - + " consumerThreadPoolSize = " + consumerThreadPoolSize + "\n" - + " consumerThreadPoolQueueCapacity = " + consumerThreadPoolQueueCapacity + "\n" - + " requeueRejectedMessages = " + requeueRejectedMessages + "\n" + + " defaultExchangeType = " + exchangeType + "\n" + " heartbeatIntervalSec = " + heartbeatIntervalSec + "\n" + " networkRecoveryIntervalMs = " + networkRecoveryIntervalMs + "\n" + + " prefetchCount = " + prefetchCount + "\n" + "}"; Config amqpConfig = ConfigFactory.parseString(configStr).getConfig("config.amqp"); @@ -108,11 +106,9 @@ public void testHostConfigurationOption() { + " useSSL = \"" + useSSL + "\"\n" + " groupId = \"" + groupId + "\"\n" + " durable = " + durable + "\n" - + " consumerThreadPoolSize = " + consumerThreadPoolSize + "\n" - + " consumerThreadPoolQueueCapacity = " + consumerThreadPoolQueueCapacity + "\n" - + " requeueRejectedMessages = " + requeueRejectedMessages + "\n" + " heartbeatIntervalSec = " + heartbeatIntervalSec + "\n" + " networkRecoveryIntervalMs = " + networkRecoveryIntervalMs + "\n" + + " prefetchCount = " + prefetchCount + "\n" + "}"; testMandatoryConfigurationOption(configStr, "host"); @@ -129,11 +125,9 @@ public void testPortConfigurationOption() { + " useSSL = \"" + useSSL + "\"\n" + " groupId = \"" + groupId + "\"\n" + " durable = " + durable + "\n" - + " consumerThreadPoolSize = " + consumerThreadPoolSize + "\n" - + " consumerThreadPoolQueueCapacity = " + consumerThreadPoolQueueCapacity + "\n" - + " requeueRejectedMessages = " + requeueRejectedMessages + "\n" + " heartbeatIntervalSec = " + heartbeatIntervalSec + "\n" + " networkRecoveryIntervalMs = " + networkRecoveryIntervalMs + "\n" + + " prefetchCount = " + prefetchCount + "\n" + "}"; testMandatoryConfigurationOption(configStr, "port"); @@ -150,58 +144,14 @@ public void testDurableConfigurationOption() { + " virtualHost = \"" + virtualHost + "\"\n" + " useSSL = \"" + useSSL + "\"\n" + " groupId = \"" + groupId + "\"\n" - + " consumerThreadPoolSize = " + consumerThreadPoolSize + "\n" - + " consumerThreadPoolQueueCapacity = " + consumerThreadPoolQueueCapacity + "\n" - + " requeueRejectedMessages = " + requeueRejectedMessages + "\n" + " heartbeatIntervalSec = " + heartbeatIntervalSec + "\n" + " networkRecoveryIntervalMs = " + networkRecoveryIntervalMs + "\n" + + " prefetchCount = " + prefetchCount + "\n" + "}"; testMandatoryConfigurationOption(configStr, "durable"); } - @Test - public void testConsumerThreadPoolSizeConfigurationOption() { - String configStr = "config.amqp {" - + " charsetName = \"" + charsetName + "\"\n" - + " host = \"" + host + "\"\n" - + " port = \"" + port + "\"\n" - + " username = \"" + username + "\"\n" - + " password = \"" + password + "\"\n" - + " virtualHost = \"" + virtualHost + "\"\n" - + " useSSL = \"" + useSSL + "\"\n" - + " groupId = \"" + groupId + "\"\n" - + " durable = " + durable + "\n" - + " consumerThreadPoolQueueCapacity = " + consumerThreadPoolQueueCapacity + "\n" - + " requeueRejectedMessages = " + requeueRejectedMessages + "\n" - + " heartbeatIntervalSec = " + heartbeatIntervalSec + "\n" - + " networkRecoveryIntervalMs = " + networkRecoveryIntervalMs + "\n" - + "}"; - - testMandatoryConfigurationOption(configStr, "consumerThreadPoolSize"); - } - - @Test - public void testConsumerThreadPoolQueueCapacityConfigurationOption() { - String configStr = "config.amqp {" - + " charsetName = \"" + charsetName + "\"\n" - + " host = \"" + host + "\"\n" - + " port = \"" + port + "\"\n" - + " username = \"" + username + "\"\n" - + " password = \"" + password + "\"\n" - + " virtualHost = \"" + virtualHost + "\"\n" - + " useSSL = \"" + useSSL + "\"\n" - + " groupId = \"" + groupId + "\"\n" - + " durable = " + durable + "\n" - + " consumerThreadPoolSize = " + consumerThreadPoolSize + "\n" - + " requeueRejectedMessages = " + requeueRejectedMessages + "\n" - + " heartbeatIntervalSec = " + heartbeatIntervalSec + "\n" - + " networkRecoveryIntervalMs = " + networkRecoveryIntervalMs + "\n" - + "}"; - - testMandatoryConfigurationOption(configStr, "consumerThreadPoolQueueCapacity"); - } - @Test public void testCharsetConfigurationOption() { String configStr = "config.amqp {" @@ -213,11 +163,9 @@ public void testCharsetConfigurationOption() { + " useSSL = \"" + useSSL + "\"\n" + " groupId = \"" + groupId + "\"\n" + " durable = " + durable + "\n" - + " consumerThreadPoolSize = " + consumerThreadPoolSize + "\n" - + " consumerThreadPoolQueueCapacity = " + consumerThreadPoolQueueCapacity + "\n" - + " requeueRejectedMessages = " + requeueRejectedMessages + "\n" + " heartbeatIntervalSec = " + heartbeatIntervalSec + "\n" + " networkRecoveryIntervalMs = " + networkRecoveryIntervalMs + "\n" + + " prefetchCount = " + prefetchCount + "\n" + "}"; testMandatoryConfigurationOption(configStr, "charsetName"); @@ -237,10 +185,9 @@ public void testInvalidCharset() { + " useSSL = \"" + useSSL + "\"\n" + " groupId = \"" + groupId + "\"\n" + " durable = " + durable + "\n" - + " consumerThreadPoolSize = " + consumerThreadPoolSize + "\n" - + " consumerThreadPoolQueueCapacity = " + consumerThreadPoolQueueCapacity + "\n" + " heartbeatIntervalSec = " + heartbeatIntervalSec + "\n" + " networkRecoveryIntervalMs = " + networkRecoveryIntervalMs + "\n" + + " prefetchCount = " + prefetchCount + "\n" + "}"; AmqpBrokerConfig.AmqpBrokerConfigBuilder builder = createConfigBuilder(configStr); @@ -258,17 +205,16 @@ public void testUseSSLConfigurationOption() { + " virtualHost = \"" + virtualHost + "\"\n" + " groupId = \"" + groupId + "\"\n" + " durable = " + durable + "\n" - + " consumerThreadPoolSize = " + consumerThreadPoolSize + "\n" - + " consumerThreadPoolQueueCapacity = " + consumerThreadPoolQueueCapacity + "\n" + " heartbeatIntervalSec = " + heartbeatIntervalSec + "\n" + " networkRecoveryIntervalMs = " + networkRecoveryIntervalMs + "\n" + + " prefetchCount = " + prefetchCount + "\n" + "}"; testMandatoryConfigurationOption(configStr, "useSSL"); } @Test - public void testRequeueRejectedMessagesOption() { + public void testHeartbeatIntervalOption() { String configStr = "config.amqp {" + " charsetName = \"" + charsetName + "\"\n" + " host = \"" + host + "\"\n" @@ -279,17 +225,16 @@ public void testRequeueRejectedMessagesOption() { + " useSSL = \"" + useSSL + "\"\n" + " groupId = \"" + groupId + "\"\n" + " durable = " + durable + "\n" - + " consumerThreadPoolSize = " + consumerThreadPoolSize + "\n" - + " consumerThreadPoolQueueCapacity = " + consumerThreadPoolQueueCapacity + "\n" - + " heartbeatIntervalSec = " + heartbeatIntervalSec + "\n" + + " defaultExchangeType = " + exchangeType + "\n" + " networkRecoveryIntervalMs = " + networkRecoveryIntervalMs + "\n" + + " prefetchCount = " + prefetchCount + "\n" + "}"; - testMandatoryConfigurationOption(configStr, "requeueRejectedMessages"); + testMandatoryConfigurationOption(configStr, "heartbeatIntervalSec"); } @Test - public void testHeartbeatIntervalOption() { + public void testNetworkRecoveryIntervalOption() { String configStr = "config.amqp {" + " charsetName = \"" + charsetName + "\"\n" + " host = \"" + host + "\"\n" @@ -300,17 +245,16 @@ public void testHeartbeatIntervalOption() { + " useSSL = \"" + useSSL + "\"\n" + " groupId = \"" + groupId + "\"\n" + " durable = " + durable + "\n" - + " consumerThreadPoolSize = " + consumerThreadPoolSize + "\n" - + " consumerThreadPoolQueueCapacity = " + consumerThreadPoolQueueCapacity + "\n" - + " requeueRejectedMessages = " + requeueRejectedMessages + "\n" - + " networkRecoveryIntervalMs = " + networkRecoveryIntervalMs + "\n" + + " defaultExchangeType = " + exchangeType + "\n" + + " heartbeatIntervalSec = " + heartbeatIntervalSec + "\n" + + " prefetchCount = " + prefetchCount + "\n" + "}"; - testMandatoryConfigurationOption(configStr, "heartbeatIntervalSec"); + testMandatoryConfigurationOption(configStr, "networkRecoveryIntervalMs"); } @Test - public void testNetworkRecoveryIntervalOption() { + public void testPrefetchCountOption() { String configStr = "config.amqp {" + " charsetName = \"" + charsetName + "\"\n" + " host = \"" + host + "\"\n" @@ -321,13 +265,12 @@ public void testNetworkRecoveryIntervalOption() { + " useSSL = \"" + useSSL + "\"\n" + " groupId = \"" + groupId + "\"\n" + " durable = " + durable + "\n" - + " consumerThreadPoolSize = " + consumerThreadPoolSize + "\n" - + " consumerThreadPoolQueueCapacity = " + consumerThreadPoolQueueCapacity + "\n" - + " requeueRejectedMessages = " + requeueRejectedMessages + "\n" + + " defaultExchangeType = " + exchangeType + "\n" + " heartbeatIntervalSec = " + heartbeatIntervalSec + "\n" + + " networkRecoveryIntervalMs = " + networkRecoveryIntervalMs + "\n" + "}"; - testMandatoryConfigurationOption(configStr, "networkRecoveryIntervalMs"); + testMandatoryConfigurationOption(configStr, "prefetchCount"); } private void testMandatoryConfigurationOption(String configStr, String path) { diff --git a/amqp/src/test/resources/log4j.xml b/amqp/src/test/resources/log4j.xml deleted file mode 100644 index 693ccc76..00000000 --- a/amqp/src/test/resources/log4j.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/cli/README.md b/cli/README.md index 9fb2529d..d2bab1ef 100644 --- a/cli/README.md +++ b/cli/README.md @@ -1,22 +1,31 @@ # MSB-Java CLI -Microservice bus - Java CLI +Microservice bus - Java CLI ## Build: ``` -mvn clean compile assembly:single +mvn clean install ``` - ## Run: + +without options to show help: ``` -java -jar +java -jar msb-java-cli-*.jar +``` +with topic: +``` +java -jar msb-java-cli-*.jar --topic pingpong:namespace +``` +with external config: +``` +java -Dconfig.file=./application.conf -jar msb-java-cli-*.jar --topic pingpong:namespace ``` - ## Usage Listens to a topic on the bus and prints JSON to stdout. By default it will also listen for response topics detected on messages, and JSON is pretty-printed. For [Newline-delimited JSON](http://en.wikipedia.org/wiki/Line_Delimited_JSON) compatibility, specify `-p false`. Options: -- **--topic** or **-t** +- **--topic** or **-t** (By adding '.fanout' or '.topic' to topic name you can specify a type of the exchange, Default:'.fanout', Example: --topic pingpong:namespace.fanout) - **--follow** or **-f** listen for following topics, empty to disable (Default: response, ack) - **--pretty** or **-p** set to false to use as a newline-delimited json stream, (Default: true) +- **-Dconfig.file=** specify external config file if needed diff --git a/cli/pom.xml b/cli/pom.xml index 221ca44d..3e46d823 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -3,7 +3,7 @@ io.github.tcdl.msb msb-java - 1.3.0-SNAPSHOT + 1.6.7-SNAPSHOT ../pom.xml 4.0.0 @@ -30,22 +30,33 @@ io.github.tcdl.msb msb-java-amqp + + ch.qos.logback + logback-classic + runtime + org.apache.maven.plugins - maven-assembly-plugin - - - - io.github.tcdl.msb.cli.CliTool - - - - jar-with-dependencies - - + maven-shade-plugin + 2.3 + + + package + + shade + + + + + io.github.tcdl.msb.cli.CliTool + + + + + diff --git a/cli/src/main/java/io/github/tcdl/msb/cli/CliMessageHandler.java b/cli/src/main/java/io/github/tcdl/msb/cli/CliMessageHandler.java index 18709352..b497bc06 100644 --- a/cli/src/main/java/io/github/tcdl/msb/cli/CliMessageHandler.java +++ b/cli/src/main/java/io/github/tcdl/msb/cli/CliMessageHandler.java @@ -1,15 +1,18 @@ package io.github.tcdl.msb.cli; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import io.github.tcdl.msb.api.exception.JsonConversionException; import io.github.tcdl.msb.adapters.ConsumerAdapter; +import io.github.tcdl.msb.acknowledge.AcknowledgementHandlerInternal; +import io.github.tcdl.msb.api.ExchangeType; +import io.github.tcdl.msb.api.exception.JsonConversionException; import java.io.IOException; import java.util.List; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; + /** * This handler dumps messages from the given topics. * @@ -30,7 +33,7 @@ public CliMessageHandler(CliMessageSubscriber subscriber, List follow, b * @throws JsonConversionException if some problems during parsing JSON */ @Override - public void onMessage(String jsonMessage) { + public void onMessage(String jsonMessage, AcknowledgementHandlerInternal handler) { ObjectMapper objectMapper = new ObjectMapper(); objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); @@ -50,7 +53,7 @@ public void onMessage(String jsonMessage) { String responseTopicName = jsonMessageObject.get("topics").get("response").asText(); try { - subscriber.subscribe(responseTopicName, this); + subscriber.subscribe(responseTopicName, ExchangeType.FANOUT, this); } catch (Exception e) { // Just ignore the exception } diff --git a/cli/src/main/java/io/github/tcdl/msb/cli/CliMessageSubscriber.java b/cli/src/main/java/io/github/tcdl/msb/cli/CliMessageSubscriber.java index 4a8c5d81..b6ddf994 100644 --- a/cli/src/main/java/io/github/tcdl/msb/cli/CliMessageSubscriber.java +++ b/cli/src/main/java/io/github/tcdl/msb/cli/CliMessageSubscriber.java @@ -1,7 +1,11 @@ package io.github.tcdl.msb.cli; +import com.google.common.collect.Sets; import io.github.tcdl.msb.adapters.AdapterFactory; import io.github.tcdl.msb.adapters.ConsumerAdapter; +import io.github.tcdl.msb.api.AmqpResponderOptions; +import io.github.tcdl.msb.api.ExchangeType; +import io.github.tcdl.msb.api.ResponderOptions; import java.util.HashSet; import java.util.Set; @@ -20,10 +24,15 @@ public CliMessageSubscriber(AdapterFactory adapterFactory) { /** * Subscribes the given handler to the given topic */ - public void subscribe(String topicName, CliMessageHandler handler) { + public void subscribe(String topicName, ExchangeType exchangeType, CliMessageHandler handler) { synchronized (registeredTopics) { if (!registeredTopics.contains(topicName)) { - ConsumerAdapter adapter = adapterFactory.createConsumerAdapter(topicName); + ResponderOptions responderOptions = new AmqpResponderOptions.Builder() + .withExchangeType(exchangeType) + .withBindingKeys(Sets.newHashSet("*")) + .build(); + ConsumerAdapter adapter = adapterFactory.createConsumerAdapter(topicName, false, responderOptions); + adapter.subscribe(handler); registeredTopics.add(topicName); } diff --git a/cli/src/main/java/io/github/tcdl/msb/cli/CliTool.java b/cli/src/main/java/io/github/tcdl/msb/cli/CliTool.java index 625c9d64..81a1dc79 100644 --- a/cli/src/main/java/io/github/tcdl/msb/cli/CliTool.java +++ b/cli/src/main/java/io/github/tcdl/msb/cli/CliTool.java @@ -2,17 +2,23 @@ import com.typesafe.config.ConfigFactory; import io.github.tcdl.msb.adapters.AdapterFactoryLoader; +import io.github.tcdl.msb.api.ExchangeType; import io.github.tcdl.msb.config.MsbConfig; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; /** * This tool allows to wiretap the given topics and log the messages from them. See {@link #printUsage()} for parameters supported. */ public class CliTool { + private static String FANOUT_EXCHANGE_PREFIX = ".fanout"; + private static String TOPIC_EXCHANGE_PREFIX = ".topic"; + public static void main(String[] args) { MsbConfig msbConfig = new MsbConfig(ConfigFactory.load()); AdapterFactoryLoader adapterFactoryLoader = new AdapterFactoryLoader(msbConfig); @@ -27,21 +33,43 @@ public static void main(String[] args) { boolean prettyOutput = getPrettyOutput(args); List follow = getFollow(args); + List exchanges = topics.stream() + .map(CliTool::parseExchange) + .collect(Collectors.toList()); + CliMessageSubscriber subscriptionManager = new CliMessageSubscriber(adapterFactoryLoader.getAdapterFactory()); - subscribe(subscriptionManager, topics, follow, prettyOutput); + subscribe(subscriptionManager, exchanges, follow, prettyOutput); } private static void printUsage() { System.out.println("Usage: CliTool <--topic|-t topic1,topic2> [--pretty true|false] [--follow response]\n" - + "--topic (required) specifies topic(s) to listen to\n" + + "--topic (required) specifies topic(s) to listen to. By adding '.fanout' or '.topic' you can specify a type of the exchange\n" + "--pretty (defaults to 'true') display formatted or not formatted messages\n" + "--follow (defaults to 'response') allows to inspect incoming messages and subscribe to response topics automatically"); } - static void subscribe(CliMessageSubscriber subscriptionManager, List topics, List follow, boolean prettyOutput) { - for (String topicName : topics) { - subscriptionManager.subscribe(topicName, new CliMessageHandler(subscriptionManager, follow, prettyOutput)); + static void subscribe(CliMessageSubscriber subscriptionManager, List exchanges, List follow, boolean prettyOutput) { + for (MsbExchange exchange : exchanges) { + subscriptionManager.subscribe(exchange.getNamespace(), exchange.getType(), new CliMessageHandler(subscriptionManager, follow, prettyOutput)); + } + } + + static MsbExchange parseExchange(String topic) { + String namespace; + ExchangeType type; + + if (topic.endsWith(FANOUT_EXCHANGE_PREFIX)) { + type = ExchangeType.FANOUT; + namespace = topic.substring(0, topic.length() - FANOUT_EXCHANGE_PREFIX.length()); + } else if (topic.endsWith(TOPIC_EXCHANGE_PREFIX)) { + type = ExchangeType.TOPIC; + namespace = topic.substring(0, topic.length() - TOPIC_EXCHANGE_PREFIX.length()); + } else { + type = ExchangeType.FANOUT; + namespace = topic; } + + return new MsbExchange(namespace, type); } static List getTopics(String[] args) { diff --git a/cli/src/main/java/io/github/tcdl/msb/cli/MsbExchange.java b/cli/src/main/java/io/github/tcdl/msb/cli/MsbExchange.java new file mode 100644 index 00000000..c1ac9ea1 --- /dev/null +++ b/cli/src/main/java/io/github/tcdl/msb/cli/MsbExchange.java @@ -0,0 +1,25 @@ +package io.github.tcdl.msb.cli; + +import io.github.tcdl.msb.api.ExchangeType; + +/** + * Represents msb namespace with specific exchange type. + */ +public class MsbExchange { + + private String namespace; + private ExchangeType type; + + public MsbExchange(String namespace, ExchangeType type) { + this.namespace = namespace; + this.type = type; + } + + public String getNamespace() { + return namespace; + } + + public ExchangeType getType() { + return type; + } +} diff --git a/cli/src/main/resources/application.conf b/cli/src/main/resources/application.conf index 829cf2b2..5e168911 100644 --- a/cli/src/main/resources/application.conf +++ b/cli/src/main/resources/application.conf @@ -12,18 +12,19 @@ msbConfig { # Thread pool used for scheduling ack and response timeout tasks timerThreadPoolSize: 2 + threadingConfig = { + consumerThreadPoolSize = 5 + # -1 means unlimited + consumerThreadPoolQueueCapacity = 20 + } + # Broker Adapter Defaults brokerConfig = { host = "127.0.0.1" port = "5672" groupId = "cli-tool" durable = false - consumerThreadPoolSize = 5 - # -1 means unlimited - consumerThreadPoolQueueCapacity = 20 - requeueRejectedMessages = true + prefetchCount = 0 } } - - diff --git a/cli/src/main/resources/log4j.xml b/cli/src/main/resources/log4j.xml deleted file mode 100644 index b5a43681..00000000 --- a/cli/src/main/resources/log4j.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/cli/src/main/resources/logback.xml b/cli/src/main/resources/logback.xml new file mode 100644 index 00000000..6e35c0af --- /dev/null +++ b/cli/src/main/resources/logback.xml @@ -0,0 +1,14 @@ + + + + + UTF-8 + %d{HH:mm:ss.SSS} %-5level %logger [%t] %c{1} - %m%n + + + + + + + + \ No newline at end of file diff --git a/cli/src/test/java/io/github/tcdl/msb/cli/CliMessageHandlerTest.java b/cli/src/test/java/io/github/tcdl/msb/cli/CliMessageHandlerTest.java index f5d9f18b..c398d46d 100644 --- a/cli/src/test/java/io/github/tcdl/msb/cli/CliMessageHandlerTest.java +++ b/cli/src/test/java/io/github/tcdl/msb/cli/CliMessageHandlerTest.java @@ -1,14 +1,15 @@ package io.github.tcdl.msb.cli; -import org.junit.Test; - -import java.util.Collections; - import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; +import java.util.Collections; + +import io.github.tcdl.msb.api.ExchangeType; +import org.junit.Test; + public class CliMessageHandlerTest { @Test public void testSubscriptionToResponseQueue() { @@ -19,10 +20,10 @@ public void testSubscriptionToResponseQueue() { "{ \"topics\": {\n" + " \"to\": \"search:parsers:facets:v1\",\n" + " \"response\": \"search:parsers:facets:v1:response:3c3dec275b326c6500010843\"\n" - + " }}" + + " }}", null ); - verify(subscriber).subscribe("search:parsers:facets:v1:response:3c3dec275b326c6500010843", handler); + verify(subscriber).subscribe("search:parsers:facets:v1:response:3c3dec275b326c6500010843", ExchangeType.FANOUT, handler); } @Test @@ -33,7 +34,7 @@ public void testNoSubscriptionIfMissingResponseQueue() { handler.onMessage( "{ \"topics\": {\n" + " \"to\": \"search:parsers:facets:v1\"\n" - + " }}" + + " }}", null ); verifyNoMoreInteractions(subscriber); @@ -48,7 +49,7 @@ public void testNoSubscriptionIfNullResponseQueue() { "{ \"topics\": {\n" + " \"to\": \"search:parsers:facets:v1\",\n" + " \"response\": null\n" - + " }}" + + " }}", null ); verifyNoMoreInteractions(subscriber); @@ -59,13 +60,13 @@ public void testSubscriptionNonExistingQueue() { CliMessageSubscriber subscriber = mock(CliMessageSubscriber.class); CliMessageHandler handler = new CliMessageHandler(subscriber, Collections.singletonList("response"), true); - doThrow(new RuntimeException()).when(subscriber).subscribe("non-existent-queue", handler); + doThrow(new RuntimeException()).when(subscriber).subscribe("non-existent-queue", ExchangeType.FANOUT, handler); handler.onMessage( "{ \"topics\": {\n" + " \"to\": \"search:parsers:facets:v1\",\n" + " \"response\": \"non-existent-queue\"\n" - + " }}" + + " }}", null ); // The point of this test is to verify that no exception is thrown in such case diff --git a/cli/src/test/java/io/github/tcdl/msb/cli/CliMessageSubscriberTest.java b/cli/src/test/java/io/github/tcdl/msb/cli/CliMessageSubscriberTest.java index ff52936f..1961ef10 100644 --- a/cli/src/test/java/io/github/tcdl/msb/cli/CliMessageSubscriberTest.java +++ b/cli/src/test/java/io/github/tcdl/msb/cli/CliMessageSubscriberTest.java @@ -2,13 +2,14 @@ import io.github.tcdl.msb.adapters.AdapterFactory; import io.github.tcdl.msb.adapters.ConsumerAdapter; +import io.github.tcdl.msb.api.ExchangeType; +import io.github.tcdl.msb.api.ResponderOptions; import org.junit.Before; import org.junit.Test; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.*; public class CliMessageSubscriberTest { @@ -24,7 +25,7 @@ public class CliMessageSubscriberTest { public void setUp() { mockAdapterFactory = mock(AdapterFactory.class); mockConsumerAdapter = mock(ConsumerAdapter.class); - when(mockAdapterFactory.createConsumerAdapter(TOPIC_NAME)).thenReturn(mockConsumerAdapter); + when(mockAdapterFactory.createConsumerAdapter(eq(TOPIC_NAME), eq(false), any(ResponderOptions.class))).thenReturn(mockConsumerAdapter); mockMessageHandler = mock(CliMessageHandler.class); @@ -41,7 +42,7 @@ public void testDuplicateSubscription() { testInitialSubscription(TOPIC_NAME); // make another subscription to the same topic - subscriptionManager.subscribe(TOPIC_NAME, mockMessageHandler); + subscriptionManager.subscribe(TOPIC_NAME, ExchangeType.FANOUT, mockMessageHandler); // verify that nothing happens verifyNoMoreInteractions(mockAdapterFactory); @@ -50,9 +51,9 @@ public void testDuplicateSubscription() { private void testInitialSubscription(String topicName) { // method under test - subscriptionManager.subscribe(topicName, mockMessageHandler); + subscriptionManager.subscribe(topicName, ExchangeType.FANOUT, mockMessageHandler); - verify(mockAdapterFactory).createConsumerAdapter(topicName); + verify(mockAdapterFactory).createConsumerAdapter(eq(topicName), eq(false), any(ResponderOptions.class)); verify(mockConsumerAdapter).subscribe(mockMessageHandler); } } \ No newline at end of file diff --git a/cli/src/test/java/io/github/tcdl/msb/cli/CliToolTest.java b/cli/src/test/java/io/github/tcdl/msb/cli/CliToolTest.java index ffff2540..918c63f9 100644 --- a/cli/src/test/java/io/github/tcdl/msb/cli/CliToolTest.java +++ b/cli/src/test/java/io/github/tcdl/msb/cli/CliToolTest.java @@ -1,5 +1,6 @@ package io.github.tcdl.msb.cli; +import io.github.tcdl.msb.api.ExchangeType; import org.junit.Test; import java.util.Arrays; @@ -85,13 +86,15 @@ public void getPrettyOutputDefaultValue() { @Test public void testSubscribe() { CliMessageSubscriber subscriptionManager = mock(CliMessageSubscriber.class); - List topics = Arrays.asList("topic1", "topic2"); + List topics = Arrays.asList( + new MsbExchange("topic1", ExchangeType.FANOUT), + new MsbExchange("topic2", ExchangeType.TOPIC)); List follow = Collections.singletonList("response"); CliTool.subscribe(subscriptionManager, topics, follow, false); - verify(subscriptionManager).subscribe(eq("topic1"), any(CliMessageHandler.class)); - verify(subscriptionManager).subscribe(eq("topic2"), any(CliMessageHandler.class)); + verify(subscriptionManager).subscribe(eq("topic1"), eq(ExchangeType.FANOUT), any(CliMessageHandler.class)); + verify(subscriptionManager).subscribe(eq("topic2"), eq(ExchangeType.TOPIC), any(CliMessageHandler.class)); } private String[] strToArr(String argString) { diff --git a/cli/src/test/resources/log4j.xml b/cli/src/test/resources/log4j.xml deleted file mode 100644 index ab8fed55..00000000 --- a/cli/src/test/resources/log4j.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/core/pom.xml b/core/pom.xml index 2761c876..8e633e39 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -3,7 +3,7 @@ io.github.tcdl.msb msb-java - 1.3.0-SNAPSHOT + 1.6.7-SNAPSHOT ../pom.xml 4.0.0 @@ -21,6 +21,7 @@ tcdl https://github.com/tcdl + org.apache.commons diff --git a/core/src/main/java/io/github/tcdl/msb/ChannelManager.java b/core/src/main/java/io/github/tcdl/msb/ChannelManager.java index c5c07e69..b8160eea 100644 --- a/core/src/main/java/io/github/tcdl/msb/ChannelManager.java +++ b/core/src/main/java/io/github/tcdl/msb/ChannelManager.java @@ -1,128 +1,156 @@ package io.github.tcdl.msb; -import java.time.Clock; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - import com.fasterxml.jackson.databind.ObjectMapper; import io.github.tcdl.msb.adapters.AdapterFactory; -import io.github.tcdl.msb.adapters.AdapterFactoryLoader; import io.github.tcdl.msb.adapters.ConsumerAdapter; import io.github.tcdl.msb.adapters.ProducerAdapter; -import io.github.tcdl.msb.api.Callback; +import io.github.tcdl.msb.api.RequestOptions; +import io.github.tcdl.msb.api.ResponderOptions; import io.github.tcdl.msb.api.exception.ConsumerSubscriptionException; +import io.github.tcdl.msb.collector.CollectorManager; import io.github.tcdl.msb.config.MsbConfig; -import io.github.tcdl.msb.api.message.Message; -import io.github.tcdl.msb.monitor.agent.ChannelMonitorAgent; -import io.github.tcdl.msb.monitor.agent.NoopChannelMonitorAgent; +import io.github.tcdl.msb.impl.SimpleMessageHandlerResolverImpl; import io.github.tcdl.msb.support.JsonValidator; import io.github.tcdl.msb.support.Utils; +import io.github.tcdl.msb.threading.MessageHandlerInvoker; import org.apache.commons.lang3.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.time.Clock; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + /** * {@link ChannelManager} creates consumers or producers on demand and manages them. */ public class ChannelManager { private static final Logger LOG = LoggerFactory.getLogger(ChannelManager.class); + private static final String RESPONDER_LOGGING_NAME = "Responder server"; - private MsbConfig msbConfig; - private Clock clock; - private JsonValidator validator; - private ObjectMapper messageMapper; - private AdapterFactory adapterFactory; - private ChannelMonitorAgent channelMonitorAgent; + private final MsbConfig msbConfig; + private final Clock clock; + private final JsonValidator validator; + private final ObjectMapper messageMapper; + private final AdapterFactory adapterFactory; + private final MessageHandlerInvoker messageHandlerInvoker; - private Map producersByTopic; - private Map consumersByTopic; + private final Map producersByTopic; + private final Map consumersByTopic; - public ChannelManager(MsbConfig msbConfig, Clock clock, JsonValidator validator, ObjectMapper messageMapper) { + public ChannelManager(MsbConfig msbConfig, Clock clock, JsonValidator validator, ObjectMapper messageMapper, AdapterFactory adapterFactory, MessageHandlerInvoker messageHandlerInvoker) { this.msbConfig = msbConfig; this.clock = clock; this.validator = validator; this.messageMapper = messageMapper; - this.adapterFactory = new AdapterFactoryLoader(msbConfig).getAdapterFactory(); + this.adapterFactory = adapterFactory; + this.messageHandlerInvoker = messageHandlerInvoker; + this.producersByTopic = new ConcurrentHashMap<>(); this.consumersByTopic = new ConcurrentHashMap<>(); - - channelMonitorAgent = new NoopChannelMonitorAgent(); } - public Producer findOrCreateProducer(final String topic) { - Validate.notNull(topic, "field 'topic' is null"); + public Producer findOrCreateProducer(String topic, boolean isResponseTopic, RequestOptions requestOptions) { + Validate.notEmpty(topic, "Topic is mandatory"); + Validate.notNull(requestOptions, "RequestOptions are mandatory"); + Producer producer = producersByTopic.computeIfAbsent(topic, key -> { - Producer newProducer = createProducer(key); - channelMonitorAgent.producerTopicCreated(key); + Producer newProducer = createProducer(key, isResponseTopic, requestOptions); return newProducer; }); return producer; } + public Optional getAvailableMessageCount(String topic) { + return Optional.ofNullable(consumersByTopic.get(topic)).flatMap(Consumer::messageCount); + } + + public Optional isConnected(String topic) { + return Optional.ofNullable(consumersByTopic.get(topic)).flatMap(Consumer::isConnected); + } + /** * Start consuming messages on specified topic with handler. * Calls to subscribe() and unsubscribe() have to be properly synchronized by client code not to lose messages. * - * @param topic * @param messageHandler handler for processing messages * @throws ConsumerSubscriptionException if subscriber for topic already exist */ - public synchronized boolean subscribe(String topic, MessageHandler messageHandler) { - Validate.notNull(topic, "field 'topic' is null"); - Validate.notNull(messageHandler, "field 'messageHandler' is null"); - if (consumersByTopic.get(topic) != null) { - throw new ConsumerSubscriptionException("Subscriber for this topic: " + topic + " already exist"); - } else { - Consumer newConsumer = createConsumer(topic, messageHandler); - channelMonitorAgent.consumerTopicCreated(topic); - consumersByTopic.put(topic, newConsumer); - return false; - } + public synchronized void subscribe(String topic, ResponderOptions responderOptions, MessageHandler messageHandler) { + Validate.notBlank(topic, "Topic should not be empty"); + Validate.notNull(responderOptions, "ResponderOptions are mandatory"); + + MessageHandlerResolver messageHandlerResolver = new SimpleMessageHandlerResolverImpl(messageHandler, RESPONDER_LOGGING_NAME); + subscribe(topic, false, responderOptions, messageHandlerResolver); + } + + /** + * {@link ChannelManager#subscribe(String, ResponderOptions, MessageHandler)} with default responderOptions + */ + public void subscribe(String topic, MessageHandler messageHandler) { + subscribe(topic, ResponderOptions.DEFAULTS, messageHandler); + } + + /** + * Start consuming response messages. + * + * @param topic response topic + * @param collectorManager resolver of {@link MessageHandler} for processing messages + * @throws ConsumerSubscriptionException if subscriber for topic already exist + */ + public synchronized void subscribeForResponses(String topic, CollectorManager collectorManager) { + Validate.notBlank(topic, "Topic should not be empty"); + Validate.notNull(collectorManager, "field 'collectorManager' is null"); + + subscribe(topic, true, ResponderOptions.DEFAULTS, collectorManager); } /** * Stop consuming messages on specified topic. * Calls to subscribe() and unsubscribe() have to be properly synchronized by client code not to lose messages. - * - * @param topic */ - public void unsubscribe(String topic) { - Consumer consumer = consumersByTopic.remove(topic); + public synchronized void unsubscribe(String topic) { + if (consumersByTopic.get(topic) != null) { + stopConsumer(consumersByTopic.remove(topic)); + } + } + + private void subscribe(String topic, boolean isResponseTopic, ResponderOptions responderOptions, MessageHandlerResolver messageHandlerResolver) { + if (consumersByTopic.get(topic) != null) { + throw new ConsumerSubscriptionException("Subscriber for topic " + topic + " already exist"); + } else { + Consumer newConsumer = createConsumer(topic, isResponseTopic, responderOptions, messageHandlerResolver); + newConsumer.subscribe(); + consumersByTopic.put(topic, newConsumer); + } + } + + private void stopConsumer(Consumer consumer) { if (consumer != null) { consumer.end(); - channelMonitorAgent.consumerTopicRemoved(topic); } } - private Producer createProducer(String topic) { + private Producer createProducer(String topic, boolean isResponseTopic, RequestOptions requestOptions) { Utils.validateTopic(topic); - - ProducerAdapter adapter = getAdapterFactory().createProducerAdapter(topic); - Callback handler = message -> channelMonitorAgent.producerMessageSent(topic); - return new Producer(adapter, topic, handler, messageMapper); + ProducerAdapter adapter = this.adapterFactory.createProducerAdapter(topic, isResponseTopic, requestOptions); + return new Producer(adapter, topic, messageMapper); } - private Consumer createConsumer(String topic, MessageHandler messageHandler) { + private Consumer createConsumer(String topic, boolean isResponseTopic, ResponderOptions responderOptions, MessageHandlerResolver messageHandlerResolver) { Utils.validateTopic(topic); - - ConsumerAdapter adapter = getAdapterFactory().createConsumerAdapter(topic); - - return new Consumer(adapter, topic, messageHandler, msbConfig, clock, channelMonitorAgent, validator, messageMapper); + ConsumerAdapter adapter = this.adapterFactory.createConsumerAdapter(topic, isResponseTopic, responderOptions); + return new Consumer(adapter, messageHandlerInvoker, topic, messageHandlerResolver, msbConfig, clock, validator, messageMapper); } public void shutdown() { LOG.info("Shutting down..."); + consumersByTopic.values().forEach(this::stopConsumer); + messageHandlerInvoker.shutdown(); adapterFactory.shutdown(); LOG.info("Shutdown complete"); } - - public AdapterFactory getAdapterFactory() { - return this.adapterFactory; - } - - public void setChannelMonitorAgent(ChannelMonitorAgent channelMonitorAgent) { - this.channelMonitorAgent = channelMonitorAgent; - } } diff --git a/core/src/main/java/io/github/tcdl/msb/Consumer.java b/core/src/main/java/io/github/tcdl/msb/Consumer.java index 80b064c0..d3fa9d09 100644 --- a/core/src/main/java/io/github/tcdl/msb/Consumer.java +++ b/core/src/main/java/io/github/tcdl/msb/Consumer.java @@ -1,22 +1,25 @@ package io.github.tcdl.msb; import com.fasterxml.jackson.databind.ObjectMapper; +import io.github.tcdl.msb.acknowledge.AcknowledgementHandlerInternal; import io.github.tcdl.msb.adapters.ConsumerAdapter; -import io.github.tcdl.msb.api.exception.JsonConversionException; -import io.github.tcdl.msb.api.exception.JsonSchemaValidationException; import io.github.tcdl.msb.api.message.Message; import io.github.tcdl.msb.api.message.MetaMessage; +import io.github.tcdl.msb.collector.ConsumedMessagesAwareMessageHandler; import io.github.tcdl.msb.config.MsbConfig; -import io.github.tcdl.msb.monitor.agent.ChannelMonitorAgent; import io.github.tcdl.msb.support.JsonValidator; import io.github.tcdl.msb.support.Utils; +import io.github.tcdl.msb.threading.MessageHandlerInvoker; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.slf4j.MDC; import java.time.Clock; import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.Optional; /** * {@link Consumer} is a component responsible for consuming messages from the bus. @@ -26,46 +29,56 @@ public class Consumer { private static final Logger LOG = LoggerFactory.getLogger(Consumer.class); private final ConsumerAdapter rawAdapter; + private final MessageHandlerInvoker messageHandlerInvoker; private final String topic; - private MsbConfig msbConfig; - private ChannelMonitorAgent channelMonitorAgent; - private Clock clock; - private MessageHandler messageHandler; - private JsonValidator validator; - private ObjectMapper messageMapper; + private final MsbConfig msbConfig; + private final Clock clock; + private final MessageHandlerResolver messageHandlerResolver; + private final JsonValidator validator; + private final ObjectMapper messageMapper; + private final String loggingTag; + private final boolean isSplitTagsForMdcLogging; /** * @param rawAdapter instance of {@link ConsumerAdapter} that allows to receive messages from message bus * @param topic - * @param messageHandler interface that user can implement to handle received message + * @param messageHandlerResolver resolves {@link MessageHandler} instance that user can implement to handle received messages. * @param msbConfig consumer configs * @param clock - * @param channelMonitorAgent * @param validator validates incoming messages * @param messageMapper message deserializer */ - public Consumer(ConsumerAdapter rawAdapter, String topic, MessageHandler messageHandler, MsbConfig msbConfig, - Clock clock, ChannelMonitorAgent channelMonitorAgent, JsonValidator validator, ObjectMapper messageMapper) { + public Consumer(ConsumerAdapter rawAdapter, MessageHandlerInvoker messageHandlerInvoker, + String topic, MessageHandlerResolver messageHandlerResolver, MsbConfig msbConfig, + Clock clock, JsonValidator validator, ObjectMapper messageMapper) { LOG.debug("Creating consumer for topic: {}", topic); Validate.notNull(rawAdapter, "the 'rawAdapter' must not be null"); + Validate.notNull(messageHandlerInvoker, "the 'messageHandlerInvokeStrategy' must not be null"); Validate.notNull(topic, "the 'topic' must not be null"); - Validate.notNull(messageHandler, "the 'messageHandler' must not be null"); + Validate.notNull(messageHandlerResolver, "the 'messageHandlerResolver' must not be null"); Validate.notNull(msbConfig, "the 'msbConfig' must not be null"); Validate.notNull(clock, "the 'clock' must not be null"); - Validate.notNull(channelMonitorAgent, "the 'channelMonitorAgent' must not be null"); Validate.notNull(validator, "the 'validator' must not be null"); Validate.notNull(messageMapper, "the 'messageMapper' must not be null"); this.rawAdapter = rawAdapter; + this.messageHandlerInvoker = messageHandlerInvoker; this.topic = topic; - this.messageHandler = messageHandler; + this.messageHandlerResolver = messageHandlerResolver; this.msbConfig = msbConfig; this.clock = clock; - this.channelMonitorAgent = channelMonitorAgent; this.validator = validator; this.messageMapper = messageMapper; + this.loggingTag = String.format("[Consumer for: '%s' on topic: '%s']", messageHandlerResolver.getLoggingName(), topic); + this.isSplitTagsForMdcLogging = !StringUtils.isEmpty(msbConfig.getMdcLoggingSplitTagsBy()); + } + + /** + * Start consuming messages + */ + public void subscribe() { this.rawAdapter.subscribe(this::handleRawMessage); } @@ -73,38 +86,106 @@ public Consumer(ConsumerAdapter rawAdapter, String topic, MessageHandler message * Stop consuming messages for specified topic. */ public void end() { - LOG.debug("Shutting down consumer for topic {}", topic); + LOG.debug("{} Shutting down consumer for topic {}", loggingTag, topic); rawAdapter.unsubscribe(); } /** - * Process incoming message. + * Returns the number of messages in the queue, ready to be delivered to consumers. + * If the queue has not been subscribed to yet, this will return {@link Optional#empty()}. + * @return the number of messages in ready state + */ + public Optional messageCount() { + return rawAdapter.messageCount(); + } + + /** + * Returns a connection status of the consumer + * @return if a consumer connected to the broker + */ + public Optional isConnected() { + return rawAdapter.isConnected(); + } + + /** + * Process raw incoming message JSON. If Message JSON is invalid or the message has been expired, the message + * will be rejected by means of {@link AcknowledgementHandlerInternal}. * * @param jsonMessage message to process */ - protected void handleRawMessage(String jsonMessage) { - LOG.debug("Topic [{}] message received [{}]", this.topic, jsonMessage); - channelMonitorAgent.consumerMessageReceived(topic); + protected void handleRawMessage(String jsonMessage, AcknowledgementHandlerInternal acknowledgeHandler) { + LOG.debug("{} message received.", loggingTag); + LOG.trace("Message: {}", jsonMessage); + + Message message; + + try { + message = parseMessage(jsonMessage); + } catch (Exception e) { + LOG.error("{} ", loggingTag, e); + LOG.trace("Unable to process consumed message: {}", jsonMessage); + acknowledgeHandler.autoReject(); + return; + } + + ConsumedMessagesAwareMessageHandler consumedMessagesAwareMessageHandler = null; try { - if (msbConfig.getSchema() != null && !Utils.isServiceTopic(topic) && msbConfig.isValidateMessage()) { - LOG.debug("Validating schema for {}", jsonMessage); - validator.validate(jsonMessage, msbConfig.getSchema()); + if(msbConfig.isMdcLogging()) { + saveMdc(message); + } + + if (isMessageExpired(message)) { + LOG.warn("[correlation id: {}, message id: {}] {} Expired message. ", message.getCorrelationId(), message.getId(), loggingTag); + LOG.trace("Message: {}", jsonMessage); + acknowledgeHandler.autoReject(); + return; } - LOG.debug("Parsing message {}", jsonMessage); - Message message = Utils.fromJson(jsonMessage, Message.class, messageMapper); - LOG.debug("Message has been successfully parsed {}", jsonMessage); - if (!isMessageExpired(message)) { - messageHandler.handleMessage(message); + Optional optionalMessageHandler = messageHandlerResolver.resolveMessageHandler(message); + if(optionalMessageHandler.isPresent()) { + MessageHandler messageHandler = optionalMessageHandler.get(); + if(messageHandler instanceof ConsumedMessagesAwareMessageHandler) { + consumedMessagesAwareMessageHandler = ((ConsumedMessagesAwareMessageHandler) messageHandler); + consumedMessagesAwareMessageHandler.notifyMessageConsumed(); + } + messageHandlerInvoker.execute(messageHandler, message, acknowledgeHandler); } else { - LOG.warn("Expired message: {}", jsonMessage); + LOG.warn("{} Can't resolve message handler.", loggingTag); + LOG.trace("Message: {}", jsonMessage); + acknowledgeHandler.autoReject(); + } + } catch (Exception e) { + LOG.warn("[correlation id: {}, message id: {}] {} Error while trying to handle a message. ", + message.getCorrelationId(), message.getId(), loggingTag, e); + LOG.trace("Message: {}", jsonMessage); + acknowledgeHandler.autoRetry(); + if(consumedMessagesAwareMessageHandler != null) { + consumedMessagesAwareMessageHandler.notifyConsumedMessageIsLost(); + } + } finally { + if(msbConfig.isMdcLogging()) { + clearMdc(); } - } catch (JsonConversionException | JsonSchemaValidationException e) { - LOG.error("Unable to process consumed message {}", jsonMessage, e); } } + private Message parseMessage(String jsonMessage) { + if (msbConfig.getSchema() != null && !Utils.isServiceTopic(topic) && msbConfig.isValidateMessage()) { + LOG.debug("{} Validating schema.", loggingTag); + LOG.trace("Message: {}", jsonMessage); + validator.validate(jsonMessage, msbConfig.getSchema()); + } + LOG.debug("{} Parsing message.", loggingTag); + LOG.trace("Message: {}", jsonMessage); + + Message result = Utils.fromJson(jsonMessage, Message.class, messageMapper); + LOG.debug("[correlation id: {}, message id: {}] {} Message has been successfully parsed.", + result.getCorrelationId(), result.getId(), loggingTag); + LOG.trace("Message: {}", jsonMessage); + return result; + } + private boolean isMessageExpired(Message message) { MetaMessage meta = message.getMeta(); if (meta == null || meta.getTtl() == null) { @@ -117,4 +198,22 @@ private boolean isMessageExpired(Message message) { return expiryTime.isBefore(now); } + + private void saveMdc(Message message) { + String tags = StringUtils.join(message.getTags(), ","); + MDC.put(msbConfig.getMdcLoggingKeyMessageTags(), tags); + MDC.put(msbConfig.getMdcLoggingKeyCorrelationId(), message.getCorrelationId()); + if(isSplitTagsForMdcLogging) { + for(String tag: message.getTags()) { + String[] parts = StringUtils.split(tag, msbConfig.getMdcLoggingSplitTagsBy(), 2); + if(parts.length == 2) { + MDC.put(parts[0], parts[1]); + } + } + } + } + + private void clearMdc() { + MDC.clear(); + } } \ No newline at end of file diff --git a/core/src/main/java/io/github/tcdl/msb/MessageHandler.java b/core/src/main/java/io/github/tcdl/msb/MessageHandler.java index 05626021..6e8ab31f 100644 --- a/core/src/main/java/io/github/tcdl/msb/MessageHandler.java +++ b/core/src/main/java/io/github/tcdl/msb/MessageHandler.java @@ -1,5 +1,6 @@ package io.github.tcdl.msb; +import io.github.tcdl.msb.api.AcknowledgementHandler; import io.github.tcdl.msb.api.message.Message; public interface MessageHandler { @@ -8,6 +9,7 @@ public interface MessageHandler { * The handleMessage method is invoked when a message is successfully parsed and is ready for processing. * * @param message the message content + * @param acknowledgeHandler confirm/reject message handler */ - void handleMessage(Message message); + void handleMessage(Message message, AcknowledgementHandler acknowledgeHandler); } diff --git a/core/src/main/java/io/github/tcdl/msb/MessageHandlerResolver.java b/core/src/main/java/io/github/tcdl/msb/MessageHandlerResolver.java new file mode 100644 index 00000000..62d0938a --- /dev/null +++ b/core/src/main/java/io/github/tcdl/msb/MessageHandlerResolver.java @@ -0,0 +1,25 @@ +package io.github.tcdl.msb; + +import io.github.tcdl.msb.api.message.Message; + +import java.util.Optional; + +/** + * Implementations of this interface gives an ability to resolve {@link MessageHandler} by an incoming {@link Message}. + */ +public interface MessageHandlerResolver { + + /** + * Resolve {@link MessageHandler} by an incoming {@link Message}. + * @param message + * @return + */ + Optional resolveMessageHandler(Message message); + + /** + * Get an arbitrary text name of the MessageHandlerResolver instance that + * will be used used in log messages. + * @return + */ + String getLoggingName(); +} diff --git a/core/src/main/java/io/github/tcdl/msb/Producer.java b/core/src/main/java/io/github/tcdl/msb/Producer.java index 1fff2443..e8518bba 100644 --- a/core/src/main/java/io/github/tcdl/msb/Producer.java +++ b/core/src/main/java/io/github/tcdl/msb/Producer.java @@ -7,6 +7,7 @@ import io.github.tcdl.msb.api.exception.JsonConversionException; import io.github.tcdl.msb.api.message.Message; import io.github.tcdl.msb.support.Utils; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -19,32 +20,27 @@ public class Producer { private static final Logger LOG = LoggerFactory.getLogger(Producer.class); private final ProducerAdapter rawAdapter; - private final Callback messageHandler; private final ObjectMapper messageMapper; - public Producer(ProducerAdapter rawAdapter, String topic, Callback messageHandler, ObjectMapper messageMapper) { + public Producer(ProducerAdapter rawAdapter, String topic, ObjectMapper messageMapper) { LOG.debug("Creating producer for topic: {}", topic); Validate.notNull(rawAdapter, "the 'rawAdapter' must not be null"); Validate.notNull(topic, "the 'topic' must not be null"); - Validate.notNull(messageHandler, "the 'messageHandler' must not be null"); Validate.notNull(messageMapper, "the 'messageMapper' must not be null"); this.rawAdapter = rawAdapter; - this.messageHandler = messageHandler; this.messageMapper = messageMapper; } public void publish(Message message) { + String routingKey = message.getTopics().getRoutingKey(); try { String jsonMessage = Utils.toJson(message, messageMapper); - LOG.debug("Publishing message to adapter : {}", jsonMessage); - rawAdapter.publish(jsonMessage); - messageHandler.call(message); + LOG.trace("Publishing message to adapter : {}", jsonMessage); + rawAdapter.publish(jsonMessage, routingKey != null ? routingKey : StringUtils.EMPTY); } catch (ChannelException | JsonConversionException e) { LOG.error("Exception while message publish to adapter", e); throw e; } } - - } diff --git a/core/src/main/java/io/github/tcdl/msb/RunOnShutdownScheduledExecutorDecorator.java b/core/src/main/java/io/github/tcdl/msb/RunOnShutdownScheduledExecutorDecorator.java index 874cd50e..931e6365 100644 --- a/core/src/main/java/io/github/tcdl/msb/RunOnShutdownScheduledExecutorDecorator.java +++ b/core/src/main/java/io/github/tcdl/msb/RunOnShutdownScheduledExecutorDecorator.java @@ -27,7 +27,7 @@ public class RunOnShutdownScheduledExecutorDecorator { private String name; public RunOnShutdownScheduledExecutorDecorator(String name, int corePoolSize, ThreadFactory threadFactory) { - LOG.info(String.format("[scheduled thread pool decorator '%s'] Starting with %d threads ", name, corePoolSize)); + LOG.info("[scheduled thread pool decorator '{}'] Starting with {} threads ", name, corePoolSize); this.name = name; scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(corePoolSize, threadFactory); @@ -57,10 +57,10 @@ public synchronized void shutdown() { So we we can assume that {@link #tasks} cannot be modified at this point */ - LOG.info(String.format("[scheduled thread pool decorator '%s'] Executing pending tasks...", name)); + LOG.info("[scheduled thread pool decorator '{}'] Executing pending tasks...", name); tasks.values().forEach(java.lang.Runnable::run); tasks.clear(); - LOG.info(String.format("[scheduled thread pool decorator '%s'] Completed pending tasks execution.", name)); + LOG.info("[scheduled thread pool decorator '{}'] Completed pending tasks execution.", name); } /** diff --git a/core/src/main/java/io/github/tcdl/msb/acknowledge/AcknowledgementAdapter.java b/core/src/main/java/io/github/tcdl/msb/acknowledge/AcknowledgementAdapter.java new file mode 100644 index 00000000..551f192e --- /dev/null +++ b/core/src/main/java/io/github/tcdl/msb/acknowledge/AcknowledgementAdapter.java @@ -0,0 +1,25 @@ +package io.github.tcdl.msb.acknowledge; + +/** + * Adapter that provides low-level acknowledgement management methods for {@link AcknowledgementHandlerImpl}. + */ +public interface AcknowledgementAdapter { + + /** + * Confirm a message. + * @throws Exception + */ + void confirm() throws Exception; + + /** + * Reject a message. + * @throws Exception + */ + void reject() throws Exception; + + /** + * Requeue a message. + * @throws Exception + */ + void retry() throws Exception; +} diff --git a/core/src/main/java/io/github/tcdl/msb/acknowledge/AcknowledgementHandlerImpl.java b/core/src/main/java/io/github/tcdl/msb/acknowledge/AcknowledgementHandlerImpl.java new file mode 100644 index 00000000..b0f2b884 --- /dev/null +++ b/core/src/main/java/io/github/tcdl/msb/acknowledge/AcknowledgementHandlerImpl.java @@ -0,0 +1,132 @@ +package io.github.tcdl.msb.acknowledge; + +import java.util.concurrent.atomic.AtomicBoolean; + +import io.github.tcdl.msb.api.AcknowledgementHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class provides acknowledgement mechanism implementation that handles + * both implicit (provided by {@link AcknowledgementHandlerInternal} and used by the library) + * and explicit (provided by {@link AcknowledgementHandler} and used by library clients) messages acknowledge means. + */ +public class AcknowledgementHandlerImpl implements AcknowledgementHandlerInternal { + + private static final String ACK_WAS_ALREADY_SENT = "[{}] Acknowledgement was already sent during message processing."; + + private static final Logger LOG = LoggerFactory.getLogger(AcknowledgementHandlerImpl.class); + + final AcknowledgementAdapter acknowledgementAdapter; + final boolean isMessageRedelivered; + final String messageTextIdentifier; + + final AtomicBoolean acknowledgementSent = new AtomicBoolean(false); + volatile boolean autoAcknowledgement = true; + + public AcknowledgementHandlerImpl(AcknowledgementAdapter acknowledgementAdapter, + boolean isMessageRedelivered, String messageTextIdentifier) { + this.acknowledgementAdapter = acknowledgementAdapter; + this.isMessageRedelivered = isMessageRedelivered; + this.messageTextIdentifier = messageTextIdentifier; + } + + public boolean isAutoAcknowledgement() { + return autoAcknowledgement; + } + + public void setAutoAcknowledgement(boolean autoAcknowledgement) { + this.autoAcknowledgement = autoAcknowledgement; + } + + @Override + public void confirmMessage() { + executeAck("confirm", () -> { + acknowledgementAdapter.confirm(); + LOG.debug("[{}] A message was confirmed", messageTextIdentifier); + }); + } + + @Override + public void retryMessage() { + executeAck("requeue", () -> { + acknowledgementAdapter.retry(); + LOG.debug("[{}] A message was rejected with requeue", messageTextIdentifier); + }); + } + + @Override + public void retryMessageFirstTime() { + if (!isMessageRedelivered) { + retryMessage(); + } else { + rejectMessage(); + } + } + + @Override + public void rejectMessage() { + executeAck("reject", () -> { + acknowledgementAdapter.reject(); + LOG.debug("[{}] A message was discarded", messageTextIdentifier); + }); + } + + private void executeAck(String actionName, AckAction ackAction) { + if (acknowledgementSent.compareAndSet(false, true)) { + try { + ackAction.perform(); + } catch (Exception e) { + LOG.error("[{}] Got exception when trying to {} a message:", messageTextIdentifier, actionName, e); + } + } else { + LOG.error(ACK_WAS_ALREADY_SENT, messageTextIdentifier); + } + } + + @Override + public void autoConfirm() { + executeAutoAck(() -> { + confirmMessage(); + LOG.debug("[{}] A message was automatically confirmed after message processing", messageTextIdentifier); + }); + } + + @Override + public void autoReject() { + executeAutoAck(() -> { + rejectMessage(); + LOG.debug("[{}] A message was automatically rejected due to error during message processing", messageTextIdentifier); + }); + } + + @Override + public void autoRetry() { + executeAutoAck(() -> { + if (!isMessageRedelivered) { + retryMessage(); + LOG.debug("[{}] A message was rejected with requeue", messageTextIdentifier); + } else { + rejectMessage(); + LOG.warn("[{}] Can't requeue message because it already was redelivered once, discarding it instead", messageTextIdentifier); + } + }); + } + + private void executeAutoAck(AutoAckAction ackAction) { + if (autoAcknowledgement && !acknowledgementSent.get()) { + ackAction.perform(); + } + } + + @FunctionalInterface + private interface AckAction { + void perform() throws Exception; + } + + @FunctionalInterface + private interface AutoAckAction { + void perform(); + } + +} diff --git a/core/src/main/java/io/github/tcdl/msb/acknowledge/AcknowledgementHandlerInternal.java b/core/src/main/java/io/github/tcdl/msb/acknowledge/AcknowledgementHandlerInternal.java new file mode 100644 index 00000000..2a8c5a77 --- /dev/null +++ b/core/src/main/java/io/github/tcdl/msb/acknowledge/AcknowledgementHandlerInternal.java @@ -0,0 +1,24 @@ +package io.github.tcdl.msb.acknowledge; + +import io.github.tcdl.msb.api.AcknowledgementHandler; + +/** + * Interface for message acknowledgement used internally for implicit messages acknowledge. + */ +public interface AcknowledgementHandlerInternal extends AcknowledgementHandler { + + /** + * Implicit message acknowledge request invoked after client callback execution. + */ + void autoConfirm(); + + /** + * Implicit message reject request invoked when a message is expired or corrupted. + */ + void autoReject(); + + /** + * Implicit message requeue request invoked when there was an exception during a client callback execution. + */ + void autoRetry(); +} diff --git a/core/src/main/java/io/github/tcdl/msb/adapters/AdapterFactory.java b/core/src/main/java/io/github/tcdl/msb/adapters/AdapterFactory.java index 77b65355..b76de4be 100644 --- a/core/src/main/java/io/github/tcdl/msb/adapters/AdapterFactory.java +++ b/core/src/main/java/io/github/tcdl/msb/adapters/AdapterFactory.java @@ -1,8 +1,10 @@ package io.github.tcdl.msb.adapters; -import io.github.tcdl.msb.config.MsbConfig; +import io.github.tcdl.msb.api.RequestOptions; +import io.github.tcdl.msb.api.ResponderOptions; import io.github.tcdl.msb.api.exception.ChannelException; import io.github.tcdl.msb.api.exception.ConfigurationException; +import io.github.tcdl.msb.config.MsbConfig; /** * MSBAdapterFactory interface represents a common way for creation a particular AdapterFactory @@ -22,15 +24,44 @@ public interface AdapterFactory { * @param topic topic name * @return Producer Adapter associated with a topic * @throws ChannelException if some problems during creation were occurred + * @deprecated use {@link AdapterFactory#createProducerAdapter(String, boolean, RequestOptions)} + */ + @Deprecated + default ProducerAdapter createProducerAdapter(String topic) { + return createProducerAdapter(topic, false, RequestOptions.DEFAULTS); + } + + /** + * @param topic topic name + * @param isResponseTopic specify if this topic used to handle response + * @param requestOptions specific options depending on adapter implementation + * @return Producer Adapter associated with a topic + * @throws ChannelException if some problems during creation were occurred */ - ProducerAdapter createProducerAdapter(String topic); + ProducerAdapter createProducerAdapter(String topic, boolean isResponseTopic, RequestOptions requestOptions); /** * @param topic topic name + * @param isResponseTopic specify if topic for responses * @return Consumer Adapter associated with a topic * @throws ChannelException if some problems during creation were occurred */ - ConsumerAdapter createConsumerAdapter(String topic); + ConsumerAdapter createConsumerAdapter(String topic, boolean isResponseTopic); + + /** + * Creates ConsumerAdapter associated with a topic. + * @param topic topic name + * @param isResponseTopic specify if this topic used to handle response + * @param responderOptions specific options depending on adapter implementation + * @throws ChannelException if a problems has occurred during creation + */ + ConsumerAdapter createConsumerAdapter(String topic, boolean isResponseTopic, ResponderOptions responderOptions); + + /** + * @return true if custom MSB threading model should be used. + * @return false if {@link io.github.tcdl.msb.MessageHandler} should be invoked directly. + */ + boolean isUseMsbThreadingModel(); /** * Closes all resources used by amqp producers and consumers. Should be called for graceful shutdown. diff --git a/core/src/main/java/io/github/tcdl/msb/adapters/ConsumerAdapter.java b/core/src/main/java/io/github/tcdl/msb/adapters/ConsumerAdapter.java index 1024a498..ba63d8b2 100644 --- a/core/src/main/java/io/github/tcdl/msb/adapters/ConsumerAdapter.java +++ b/core/src/main/java/io/github/tcdl/msb/adapters/ConsumerAdapter.java @@ -1,7 +1,10 @@ package io.github.tcdl.msb.adapters; +import io.github.tcdl.msb.acknowledge.AcknowledgementHandlerInternal; import io.github.tcdl.msb.api.exception.ChannelException; +import java.util.Optional; + /** * {@link ConsumerAdapter} allows to receive messages from message bus. One adapter instance is associated with specific topic. * @@ -22,6 +25,19 @@ public interface ConsumerAdapter { */ void unsubscribe(); + /** + * Returns the number of messages in the queue, ready to be delivered to consumers. + * If the queue has not been subscribed to yet, this will return {@link Optional#empty()}. + * @return the number of messages in ready state + */ + Optional messageCount(); + + /** + * Returns a connection status of the consumer + * @return if a consumer connected to the broker + */ + Optional isConnected(); + /** * Callback interface for incoming message handler */ @@ -30,7 +46,9 @@ interface RawMessageHandler { * Is called once a message arrives on the topic. * * @param jsonMessage incomming JSON message + * @param acknowledgementHandler confirm/reject message handler */ - void onMessage(String jsonMessage); + void onMessage(String jsonMessage, AcknowledgementHandlerInternal acknowledgementHandler); } + } diff --git a/core/src/main/java/io/github/tcdl/msb/adapters/ProducerAdapter.java b/core/src/main/java/io/github/tcdl/msb/adapters/ProducerAdapter.java index 9ab4981f..d97c290a 100644 --- a/core/src/main/java/io/github/tcdl/msb/adapters/ProducerAdapter.java +++ b/core/src/main/java/io/github/tcdl/msb/adapters/ProducerAdapter.java @@ -16,4 +16,12 @@ public interface ProducerAdapter { * @throws ChannelException if some problems during publishing message to Broker were occurred */ void publish(String jsonMessage); + + /** + * Publishes the message to the associated topic with specified routing key + * + * @param jsonMessage message to publish in JSON format + * @param routingKey non null String of max length 255 bytes to be used for message routing + */ + void publish(String jsonMessage, String routingKey); } diff --git a/core/src/main/java/io/github/tcdl/msb/adapters/mock/MockAdapter.java b/core/src/main/java/io/github/tcdl/msb/adapters/mock/MockAdapter.java deleted file mode 100644 index d4114ff9..00000000 --- a/core/src/main/java/io/github/tcdl/msb/adapters/mock/MockAdapter.java +++ /dev/null @@ -1,150 +0,0 @@ -package io.github.tcdl.msb.adapters.mock; - -import com.fasterxml.jackson.databind.JsonNode; -import io.github.tcdl.msb.adapters.ConsumerAdapter; -import io.github.tcdl.msb.adapters.ProducerAdapter; -import io.github.tcdl.msb.api.exception.JsonConversionException; -import io.github.tcdl.msb.api.exception.JsonSchemaValidationException; -import io.github.tcdl.msb.support.JsonValidator; -import org.apache.commons.lang3.concurrent.BasicThreadFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.util.Map; -import java.util.Queue; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -/** - * MockAdapter class represents implementation of {@link ProducerAdapter} and {@link ConsumerAdapter} - * for test purposes. - */ -public class MockAdapter implements ProducerAdapter, ConsumerAdapter { - - private static final Logger LOG = LoggerFactory.getLogger(MockAdapter.class); - private static final int CONSUMING_INTERVAL = 20; - - static Map> messageMap = new ConcurrentHashMap<>(); - - private String topic; - private ExecutorService executorService; - private Queue activeConsumerExecutors; - private JsonValidator.JsonReader jsonReader = new JsonValidator.JsonReader(); - - public MockAdapter(String topic) { - LOG.debug("Created Mock Adapter for publishing to topic: " + topic); - this.topic = topic; - } - - public MockAdapter(String topic, Queue activeConsumerExecutors) { - LOG.debug("Created Mock Adapter for consuming form topic: " + topic); - this.topic = topic; - this.activeConsumerExecutors = activeConsumerExecutors; - this.executorService = activateConsumerThreadPool(topic); - } - - @Override - /** - * @throws ChannelException if an error is encountered during publishing to broker - */ - public void publish(String jsonMessage) { - LOG.debug("Received request {}", jsonMessage); - try { - JsonNode messageAsNode = jsonReader.read(jsonMessage); - if (!messageAsNode.has("topics")) { - throw new JsonSchemaValidationException(String.format("missing topics in message %s", jsonMessage)); - } - - JsonNode topicsAsNode = messageAsNode.get("topics"); - if (!topicsAsNode.has("to")) { - throw new JsonSchemaValidationException(String.format("missing topics.to in message %s", jsonMessage)); - } - - String topicsTo = topicsAsNode.get("to").asText(); - pushRequestMessage(topicsTo, jsonMessage); - } catch (IOException e) { - LOG.error("Received message can not be parsed"); - } - } - - @Override - public void subscribe(RawMessageHandler messageHandler) { - if (executorService == null) { - LOG.warn("Mock Adapter not initialized for consuming"); - } else { - executorService.execute(() -> { - { - String jsonMessage = null; - while (!executorService.isShutdown()) { - jsonMessage = pollJsonMessageForTopic(topic); - - if (messageHandler != null && jsonMessage != null) { - LOG.debug("Process message for topic {} [{}]", topic, jsonMessage); - messageHandler.onMessage(jsonMessage); - } else { - try { - Thread.sleep(CONSUMING_INTERVAL); - } catch (Exception e) { - LOG.debug("Finish listen for subscribed topic"); - } - } - } - } - - }); - } - } - - @Override - public void unsubscribe() { - LOG.debug("Unsubscribe"); - if (executorService == null) { - LOG.warn("Mock Adapter not initialized for consuming"); - } - activeConsumerExecutors.remove(executorService); - executorService.shutdown(); - } - - public static String pollJsonMessageForTopic(String topic) { - String jsonMessage = null; - if (messageMap.get(topic) != null) { - jsonMessage = messageMap.get(topic).poll(); - } - - if (jsonMessage == null) { - LOG.debug("No message found for topic {}", topic); - } - return jsonMessage; - } - - public static void pushRequestMessage(String topicTo, String jsonMessage) { - Queue messagesQueue = messageMap.get(topicTo); - if (messagesQueue == null) { - messagesQueue = new ConcurrentLinkedQueue<>(); - Queue curQ = messageMap.putIfAbsent(topicTo, messagesQueue); - if (curQ != null) { - messagesQueue = curQ; - } - } - try { - messagesQueue.add(jsonMessage); - LOG.debug("Message for topic {} published: [{}]", topicTo, jsonMessage); - } catch (JsonConversionException e) { - LOG.error("Pushed message can not be parsed"); - } - } - - private ExecutorService activateConsumerThreadPool(String topic) { - BasicThreadFactory threadFactory = new BasicThreadFactory.Builder() - .namingPattern("mock-consumer-thread-%d") - .build(); - - ExecutorService executorService = Executors.newFixedThreadPool(1, threadFactory); - activeConsumerExecutors.add(executorService); - return executorService; - } - -} diff --git a/core/src/main/java/io/github/tcdl/msb/adapters/mock/MockAdapterFactory.java b/core/src/main/java/io/github/tcdl/msb/adapters/mock/MockAdapterFactory.java deleted file mode 100644 index 6ef10eb0..00000000 --- a/core/src/main/java/io/github/tcdl/msb/adapters/mock/MockAdapterFactory.java +++ /dev/null @@ -1,45 +0,0 @@ -package io.github.tcdl.msb.adapters.mock; - -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.ExecutorService; - -import io.github.tcdl.msb.adapters.AdapterFactory; -import io.github.tcdl.msb.adapters.ConsumerAdapter; -import io.github.tcdl.msb.adapters.ProducerAdapter; -import io.github.tcdl.msb.config.MsbConfig; -import io.github.tcdl.msb.support.Utils; - -/** - * MockAdapterFactory is an implementation of {@link AdapterFactory} - * for {@link MockAdapter} - */ -public class MockAdapterFactory implements AdapterFactory { - - Queue consumerExecutors = new ConcurrentLinkedQueue<>(); - - @Override - public void init(MsbConfig msbConfig) { - // No-op - } - - @Override - public ProducerAdapter createProducerAdapter(String topic) { - return new MockAdapter(topic); - } - - @Override - public ConsumerAdapter createConsumerAdapter(String topic) { - return new MockAdapter(topic, consumerExecutors); - } - - @Override - public void shutdown() { - for (ExecutorService executorService : consumerExecutors) { - Utils.gracefulShutdown(executorService, "consumer"); - } - - consumerExecutors.clear(); - } - -} diff --git a/core/src/main/java/io/github/tcdl/msb/api/AcknowledgementHandler.java b/core/src/main/java/io/github/tcdl/msb/api/AcknowledgementHandler.java new file mode 100644 index 00000000..59c4b416 --- /dev/null +++ b/core/src/main/java/io/github/tcdl/msb/api/AcknowledgementHandler.java @@ -0,0 +1,55 @@ +package io.github.tcdl.msb.api; + +/** + * Callback interface for explicit message acknowledgement + * + */ +public interface AcknowledgementHandler { + + /** + * Set autoAcknowledgement value. + * @param autoAcknowledgement + * If autoAcknowledgement is true: + * 1. A message can be confirmed/rejected by microservice developer in ResponderServer.process() (see {@link ResponderServer}) + * or Requester.onAcknowledge(), Requester.onResponse, Requester.onRawResponse() (see {@link Requester}) methods. + * 2. If a message is not confirmed/rejected during a message processing, + * acknowledgement will be automatically sent just after completion these methods by rules: + * - message confirmed if message processed successfully, + * - message declined if message has incorrect structure and can't be processed + * - message rejected with requeue if error happens during processing + * If autoAcknowledgement is false: + * microservice developer MUST explicitly confirm/reject a message. + * autoAcknowledgement must be set to false if a message processing need to be continued in another thread. In this case + * a message should be explicitly confirmed/rejected by microservice developer + * autoAcknowledgement is true by default. + */ + void setAutoAcknowledgement(boolean autoAcknowledgement); + + /** + * @return current autoAcknowledgement value + */ + boolean isAutoAcknowledgement(); + + /** + * Inform server that a message was confirmed by consumer. + * Server should consider message acknowledged once delivered + */ + void confirmMessage(); + + /** + * Inform server that a message was rejected with requeue by consumer. + */ + void retryMessage(); + + /** + * Inform server that a message was rejected by customer. Message should be requeued only + * if it was not delivered before. + */ + void retryMessageFirstTime(); + + /** + * Inform server that a message was rejected by consumer without requeue + */ + void rejectMessage(); + +} diff --git a/core/src/main/java/io/github/tcdl/msb/api/MessageContext.java b/core/src/main/java/io/github/tcdl/msb/api/MessageContext.java new file mode 100644 index 00000000..dd66f51b --- /dev/null +++ b/core/src/main/java/io/github/tcdl/msb/api/MessageContext.java @@ -0,0 +1,18 @@ +package io.github.tcdl.msb.api; + +import io.github.tcdl.msb.api.message.Message; + +/** + * Provides access to Message Context. + */ +public interface MessageContext { + /** + * @return AcknowledgementHandler for explicit confirm/reject incoming messages + */ + AcknowledgementHandler getAcknowledgementHandler(); + + /** + * @return original message to send a response to + */ + Message getOriginalMessage(); +} diff --git a/core/src/main/java/io/github/tcdl/msb/api/MessageTemplate.java b/core/src/main/java/io/github/tcdl/msb/api/MessageTemplate.java index 6d9bec5b..b7f78719 100644 --- a/core/src/main/java/io/github/tcdl/msb/api/MessageTemplate.java +++ b/core/src/main/java/io/github/tcdl/msb/api/MessageTemplate.java @@ -67,11 +67,25 @@ public MessageTemplate withTags(String... tags) { return this; } + public void addTags(String... tags) { + this.tags.addAll(Arrays.asList(tags)); + } + + /** + * @deprecated because of misleading signature and complete duplication of {@link #withTags(String...)} logic. + * Method will be removed in version 1.7 + * todo remove it + */ + @Deprecated public MessageTemplate addTag(String... tags) { this.tags.addAll(Arrays.asList(tags)); return this; } + public void addTag(String tag) { + this.tags.add(tag); + } + @Override public String toString() { return "MessageTemplate [ttl=" + ttl + ", tags=" + StringUtils.join(tags, ",") + "]"; diff --git a/core/src/main/java/io/github/tcdl/msb/api/MsbContext.java b/core/src/main/java/io/github/tcdl/msb/api/MsbContext.java index 2099e78b..bfc96e35 100644 --- a/core/src/main/java/io/github/tcdl/msb/api/MsbContext.java +++ b/core/src/main/java/io/github/tcdl/msb/api/MsbContext.java @@ -16,4 +16,9 @@ public interface MsbContext { */ void shutdown(); + /** + * Add a callback that will be invoked before MsbContext shutdown. + * @param shutdownCallback + */ + void addShutdownCallback(Runnable shutdownCallback); } diff --git a/core/src/main/java/io/github/tcdl/msb/api/MsbContextBuilder.java b/core/src/main/java/io/github/tcdl/msb/api/MsbContextBuilder.java index 95b6b34c..34a1cd9e 100644 --- a/core/src/main/java/io/github/tcdl/msb/api/MsbContextBuilder.java +++ b/core/src/main/java/io/github/tcdl/msb/api/MsbContextBuilder.java @@ -8,15 +8,23 @@ import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; import io.github.tcdl.msb.ChannelManager; +import io.github.tcdl.msb.adapters.AdapterFactory; +import io.github.tcdl.msb.adapters.AdapterFactoryLoader; import io.github.tcdl.msb.api.exception.MsbException; +import io.github.tcdl.msb.callback.MutableCallbackHandler; import io.github.tcdl.msb.collector.CollectorManagerFactory; import io.github.tcdl.msb.collector.TimeoutManager; import io.github.tcdl.msb.config.MsbConfig; import io.github.tcdl.msb.impl.MsbContextImpl; import io.github.tcdl.msb.impl.ObjectFactoryImpl; import io.github.tcdl.msb.message.MessageFactory; -import io.github.tcdl.msb.monitor.agent.DefaultChannelMonitorAgent; import io.github.tcdl.msb.support.JsonValidator; +import io.github.tcdl.msb.threading.ConsumerExecutorFactoryImpl; +import io.github.tcdl.msb.threading.DirectInvocationCapableInvoker; +import io.github.tcdl.msb.threading.MessageGroupStrategy; +import io.github.tcdl.msb.threading.MessageHandlerInvoker; +import io.github.tcdl.msb.threading.MessageHandlerInvokerFactory; +import io.github.tcdl.msb.threading.MessageHandlerInvokerFactoryImpl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,9 +39,11 @@ public class MsbContextBuilder { private static final Logger LOG = LoggerFactory.getLogger(MsbContextBuilder.class); private Config config; + private MsbConfig msbConfig; private boolean enableShutdownHook; - private boolean enableChannelMonitorAgent; private ObjectMapper payloadMapper = createMessageEnvelopeMapper(); + private MessageGroupStrategy messageGroupStrategy; + private MessageHandlerInvokerFactory messageHandlerInvokerFactory; public MsbContextBuilder() { super(); @@ -49,6 +59,24 @@ public MsbContextBuilder withConfig(Config config) { return this; } + public MsbContextBuilder withMsbConfig(MsbConfig config) { + this.msbConfig = config; + return this; + } + + + /** + * Provide a custom {@link MessageGroupStrategy} instance in order to process messages with the same groupId + * in a single-threaded mode. + * @param messageGroupStrategy + * @return + */ + + public MsbContextBuilder withMessageGroupStrategy(MessageGroupStrategy messageGroupStrategy) { + this.messageGroupStrategy = messageGroupStrategy; + return this; + } + /** * Specifies if to shutdown current context during JVM exit. * @param enableShutdownHook if set to true will shutdown context regardless of @@ -61,22 +89,23 @@ public MsbContextBuilder enableShutdownHook(boolean enableShutdownHook) { } /** - * Specifies if monitoring agent is enabled. - * @param enableChannelMonitorAgent - true if monitoring agent is enabled and false otherwise + * Specifies payload object mapper to serialize/deserialize message payload + * @param payloadMapper if not provided default object mapper will be used * @return MsbContextBuilder */ - public MsbContextBuilder enableChannelMonitorAgent(boolean enableChannelMonitorAgent) { - this.enableChannelMonitorAgent = enableChannelMonitorAgent; + public MsbContextBuilder withPayloadMapper(ObjectMapper payloadMapper) { + this.payloadMapper = payloadMapper; return this; } /** - * Specifies payload object mapper to serialize/deserialize message payload - * @param payloadMapper if not provided default object mapper will be used + * Specifies message handler invoker factory + * + * @param messageHandlerInvokerFactory if not provided default factory will be used * @return MsbContextBuilder */ - public MsbContextBuilder withPayloadMapper(ObjectMapper payloadMapper) { - this.payloadMapper = payloadMapper; + public MsbContextBuilder withMessageHandlerInvokerFactory(MessageHandlerInvokerFactory messageHandlerInvokerFactory) { + this.messageHandlerInvokerFactory = messageHandlerInvokerFactory; return this; } @@ -93,23 +122,29 @@ public MsbContextBuilder withPayloadMapper(ObjectMapper payloadMapper) { public MsbContext build() { Clock clock = Clock.systemDefaultZone(); JsonValidator validator = new JsonValidator(); - if (config == null) { - config = ConfigFactory.load(); + if (msbConfig == null) { + if (config == null) { + config = ConfigFactory.load(); + } + msbConfig = new MsbConfig(config); + } + if (messageHandlerInvokerFactory == null) { + messageHandlerInvokerFactory = new MessageHandlerInvokerFactoryImpl(new ConsumerExecutorFactoryImpl()); } - MsbConfig msbConfig = new MsbConfig(config); ObjectMapper messageEnvelopeMapper = createMessageEnvelopeMapper(); - ChannelManager channelManager = new ChannelManager(msbConfig, clock, validator, messageEnvelopeMapper); + AdapterFactory adapterFactory = new AdapterFactoryLoader(msbConfig).getAdapterFactory(); + MessageHandlerInvoker messageHandlerInvoker = createMessageHandlerInvoker(adapterFactory, msbConfig); + ChannelManager channelManager = new ChannelManager(msbConfig, clock, validator, messageEnvelopeMapper, adapterFactory, messageHandlerInvoker); MessageFactory messageFactory = new MessageFactory(msbConfig.getServiceDetails(), clock, payloadMapper); TimeoutManager timeoutManager = new TimeoutManager(msbConfig.getTimerThreadPoolSize()); CollectorManagerFactory collectorManagerFactory = new CollectorManagerFactory(channelManager); - MsbContextImpl msbContext = new MsbContextImpl(msbConfig, messageFactory, channelManager, clock, timeoutManager, payloadMapper, collectorManagerFactory); + MsbContextImpl msbContext = new MsbContextImpl(msbConfig, messageFactory, channelManager, + clock, timeoutManager, + payloadMapper, collectorManagerFactory, + new MutableCallbackHandler()); - if (enableChannelMonitorAgent) { - DefaultChannelMonitorAgent.start(msbContext); - } - if (enableShutdownHook) { Runtime.getRuntime().addShutdownHook(new Thread("MSB shutdown hook") { @Override @@ -127,6 +162,24 @@ public void run() { return msbContext; } + private MessageHandlerInvoker createMessageHandlerInvoker(AdapterFactory adapterFactory, MsbConfig msbConfig) { + MessageHandlerInvoker directMessageHandlerInvoker = messageHandlerInvokerFactory.createDirectHandlerInvoker(); + if (!adapterFactory.isUseMsbThreadingModel()) { + return directMessageHandlerInvoker; + } + MessageHandlerInvoker consumerMessageHandlerInvoker; + if (messageGroupStrategy == null) { + consumerMessageHandlerInvoker = messageHandlerInvokerFactory.createExecutorBasedHandlerInvoker( + msbConfig.getConsumerThreadPoolSize(), msbConfig.getConsumerThreadPoolQueueCapacity()); + } else { + consumerMessageHandlerInvoker = messageHandlerInvokerFactory.createGroupedExecutorBasedHandlerInvoker( + msbConfig.getConsumerThreadPoolSize(), + msbConfig.getConsumerThreadPoolQueueCapacity(), + messageGroupStrategy); + } + return new DirectInvocationCapableInvoker(consumerMessageHandlerInvoker, directMessageHandlerInvoker); + } + /** * @return creates an instance of "default" object mapper that is used to parse message envelope (without payload) */ diff --git a/core/src/main/java/io/github/tcdl/msb/api/ObjectFactory.java b/core/src/main/java/io/github/tcdl/msb/api/ObjectFactory.java index 8a4acb79..c3c37af0 100644 --- a/core/src/main/java/io/github/tcdl/msb/api/ObjectFactory.java +++ b/core/src/main/java/io/github/tcdl/msb/api/ObjectFactory.java @@ -2,13 +2,11 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; -import io.github.tcdl.msb.api.monitor.AggregatorStats; -import io.github.tcdl.msb.api.monitor.ChannelMonitorAggregator; import java.lang.reflect.Type; /** - * Provides methods for creation client-facing API classes. + * Provides methods for creation client-facing API objects. */ public interface ObjectFactory { @@ -36,31 +34,54 @@ public Type getType() { } /** - * @param namespace topic name to send a request to - * @param requestOptions options to configure a requester - * @param payloadTypeReference expected payload type of response messages - * @return new instance of a {@link Requester} with original message + * @param namespace topic name to send a request to + * @param requestOptions options to configure a requester + * @param payloadTypeReference expected payload type of response messages + * @return new instance of a {@link Requester} */ Requester createRequester(String namespace, RequestOptions requestOptions, TypeReference payloadTypeReference); + /** + * Same as + * {@link #createRequesterForSingleResponse(String, Class, RequestOptions)} + * with default request options + */ + default Requester createRequesterForSingleResponse(String namespace, Class payloadClass) { + return createRequesterForSingleResponse(namespace, payloadClass, RequestOptions.DEFAULTS); + } + + /** + * Creates requester for single response + * + * @param namespace topic name to send a request to + * @param payloadClass expected payload class of response messages + * @param baseRequestOptions request options to be used as a source of response timeout and {@link MessageTemplate}. + * Response time however will be 1 even if {@code baseRequestOptions} define other value. + * @return new instance of a {@link Requester} + */ + Requester createRequesterForSingleResponse(String namespace, Class payloadClass, RequestOptions baseRequestOptions); + /** * Convenience method that specifies incoming payload type as {@link JsonNode} * - * See {@link #createRequester(String, RequestOptions, TypeReference)} + * @deprecated use {@link #createResponderServer(String, ResponderOptions, ResponderServer.RequestHandler, ResponderServer.ErrorHandler, TypeReference)} */ + @Deprecated default ResponderServer createResponderServer(String namespace, MessageTemplate messageTemplate, - ResponderServer.RequestHandler requestHandler) { - return createResponderServer(namespace, messageTemplate, requestHandler, JsonNode.class); + ResponderServer.RequestHandler requestHandler) { + return createResponderServer(namespace, new ResponderOptions.Builder().withMessageTemplate(messageTemplate).build(), requestHandler, JsonNode.class); } /** * Convenience method that allows to specify incoming payload type via {@link Class} * - * See {@link #createRequester(String, RequestOptions, TypeReference)} + * @deprecated use {@link #createResponderServer(String, ResponderOptions, ResponderServer.RequestHandler, ResponderServer.ErrorHandler, TypeReference)} */ + @Deprecated default ResponderServer createResponderServer(String namespace, MessageTemplate messageTemplate, - ResponderServer.RequestHandler requestHandler, Class payloadClass) { - return createResponderServer(namespace, messageTemplate, requestHandler, new TypeReference() { + ResponderServer.RequestHandler requestHandler, Class payloadClass) { + ResponderOptions responderOptions = new ResponderOptions.Builder().withMessageTemplate(messageTemplate).build(); + return createResponderServer(namespace, responderOptions, requestHandler, new TypeReference() { @Override public Type getType() { return payloadClass; @@ -69,30 +90,116 @@ public Type getType() { } /** - * @param namespace topic on a bus for listening on incoming requests - * @param messageTemplate template used for creating response messages - * @param requestHandler handler for processing the request - * @param payloadTypeReference expected payload type of incoming messages + * @param namespace topic on a bus for listening on incoming requests + * @param responderOptions {@link ResponderOptions} to be used + * @param requestHandler handler for processing the request + * @param errorHandler handler for errors to be called after default + * @param payloadTypeReference expected payload type of incoming messages * @return new instance of a {@link ResponderServer} that unmarshals payload into specified payload type */ - ResponderServer createResponderServer(String namespace, MessageTemplate messageTemplate, - ResponderServer.RequestHandler requestHandler, TypeReference payloadTypeReference); + ResponderServer createResponderServer(String namespace, ResponderOptions responderOptions, + ResponderServer.RequestHandler requestHandler, ResponderServer.ErrorHandler errorHandler, TypeReference payloadTypeReference); /** - * @return instance of converter to convert any objects - * using object mapper from {@link MsbContext} + * Convenience method that specifies incoming payload type as {@link JsonNode} + *

+ * See {@link #createResponderServer(String, ResponderOptions, ResponderServer.RequestHandler, ResponderServer.ErrorHandler, TypeReference)} */ - PayloadConverter getPayloadConverter(); + default ResponderServer createResponderServer(String namespace, ResponderOptions responderOptions, + ResponderServer.RequestHandler requestHandler, ResponderServer.ErrorHandler errorHandler) { + return createResponderServer(namespace, responderOptions, requestHandler, errorHandler, new TypeReference() { + @Override + public Type getType() { + return JsonNode.class; + } + }); + } /** - * @param aggregatorStatsHandler this handler is invoked whenever statistics is updated via announcement channel or heartbeats. - * THE HANDLER SHOULD BE THREAD SAFE because it may be invoked from parallel threads. - * @return new instance of {@link ChannelMonitorAggregator} + * See {@link #createResponderServer(String, ResponderOptions, ResponderServer.RequestHandler, ResponderServer.ErrorHandler, TypeReference)} */ - ChannelMonitorAggregator createChannelMonitorAggregator(Callback aggregatorStatsHandler); + default ResponderServer createResponderServer(String namespace, ResponderOptions responderOptions, + ResponderServer.RequestHandler requestHandler, TypeReference payloadTypeReference) { + return createResponderServer(namespace, responderOptions, requestHandler, null, payloadTypeReference); + } /** - * Shuts down the factory and all the objects that were created by it. + * See {@link #createResponderServer(String, ResponderOptions, ResponderServer.RequestHandler, ResponderServer.ErrorHandler, TypeReference)} */ - void shutdown(); + default ResponderServer createResponderServer(String namespace, ResponderOptions responderOptions, + ResponderServer.RequestHandler requestHandler, Class payloadClass) { + return createResponderServer(namespace, responderOptions, requestHandler, new TypeReference() { + @Override + public Type getType() { + return payloadClass; + + } + }); + } + + default ResponderServer createResponderServer(String namespace, ResponderOptions responderOptions, + ResponderServer.RequestHandler requestHandler, ResponderServer.ErrorHandler errorHandler, Class payloadClass) { + return createResponderServer(namespace, responderOptions, requestHandler, errorHandler, new TypeReference() { + @Override + public Type getType() { + return payloadClass; + + } + }); + } + + /** + * @deprecated use {@link #createResponderServer(String, ResponderOptions, ResponderServer.RequestHandler, ResponderServer.ErrorHandler, TypeReference)} + */ + @Deprecated + default ResponderServer createResponderServer(String namespace, MessageTemplate messageTemplate, + ResponderServer.RequestHandler requestHandler, ResponderServer.ErrorHandler errorHandler, TypeReference payloadTypeReference) { + ResponderOptions responderOptions = new ResponderOptions.Builder().withMessageTemplate(messageTemplate).build(); + return createResponderServer(namespace, responderOptions, requestHandler, errorHandler, payloadTypeReference); + } + + /** + * @deprecated use {@link #createResponderServer(String, ResponderOptions, ResponderServer.RequestHandler, ResponderServer.ErrorHandler, TypeReference)} + */ + @Deprecated + default ResponderServer createResponderServer(String namespace, MessageTemplate messageTemplate, + ResponderServer.RequestHandler requestHandler, ResponderServer.ErrorHandler errorHandler, Class payloadClass) { + ResponderOptions responderOptions = new ResponderOptions.Builder().withMessageTemplate(messageTemplate).build(); + return createResponderServer(namespace, responderOptions, requestHandler, errorHandler, new TypeReference() { + @Override + public Type getType() { + return payloadClass; + } + }); + } + + /** + * See {@link #createRequesterForFireAndForget(java.lang.String, io.github.tcdl.msb.api.RequestOptions)} + */ + default Requester createRequesterForFireAndForget(String namespace){ + return createRequesterForFireAndForget(namespace, RequestOptions.DEFAULTS); + } + + /** + * Creates requester that doesn't wait for any responses or acknowledgments + * + * @return new instance of a {@link Requester} with original message + */ + Requester createRequesterForFireAndForget(String namespace, RequestOptions requestOptions); + + /** + * @deprecated use {@link ObjectFactory#createResponderServer(java.lang.String, io.github.tcdl.msb.api.ResponderOptions, io.github.tcdl.msb.api.ResponderServer.RequestHandler, io.github.tcdl.msb.api.ResponderServer.ErrorHandler, com.fasterxml.jackson.core.type.TypeReference)} + */ + @Deprecated + default ResponderServer createResponderServer(String namespace, MessageTemplate messageTemplate, + ResponderServer.RequestHandler requestHandler, TypeReference payloadTypeReference) { + ResponderOptions responderOptions = new ResponderOptions.Builder().withMessageTemplate(messageTemplate).build(); + return createResponderServer(namespace, responderOptions, requestHandler, payloadTypeReference); + } + + /** + * @return instance of converter to convert any objects + * using object mapper from {@link MsbContext} + */ + PayloadConverter getPayloadConverter(); } \ No newline at end of file diff --git a/core/src/main/java/io/github/tcdl/msb/api/RequestOptions.java b/core/src/main/java/io/github/tcdl/msb/api/RequestOptions.java index b25ea36d..57342746 100644 --- a/core/src/main/java/io/github/tcdl/msb/api/RequestOptions.java +++ b/core/src/main/java/io/github/tcdl/msb/api/RequestOptions.java @@ -1,20 +1,25 @@ package io.github.tcdl.msb.api; +import org.apache.commons.lang3.StringUtils; + /** * Specifies waiting policy (for acknowledgements and responses) for requests sent using {@link Requester}. */ public class RequestOptions { + public static final int WAIT_FOR_RESPONSES_UNTIL_TIMEOUT = -1; + + public static final RequestOptions DEFAULTS = new Builder().build(); /** - * Min time (in milliseconds) to wait for acknowledgements. + * Max time (in milliseconds) to wait for acknowledgements. */ - private Integer ackTimeout; + private final Integer ackTimeout; /** * Max time (in milliseconds) to wait for responses and acknowledgements. Once this timeout is reached we stop waiting for responses even if * {@link #waitForResponses} has not been reached. Beware that acks may adjust this timeout. */ - private Integer responseTimeout; + private final Integer responseTimeout; /** * Number of responses to wait for. Once this number is reached (and {@link #ackTimeout} passed) we stop waiting for responses even if @@ -23,15 +28,24 @@ public class RequestOptions { * 0 means not to wait for responses at all. * -1 means to wait until {@link #responseTimeout} is reached. */ - private Integer waitForResponses; + private final Integer waitForResponses; + + /** + * A namespace for messages forwarding performed by a consumer. + */ + private final String forwardNamespace; + + private final MessageTemplate messageTemplate; - private MessageTemplate messageTemplate; + private final String routingKey; - private RequestOptions(Integer ackTimeout, Integer responseTimeout, Integer waitForResponses, MessageTemplate messageTemplate) { + protected RequestOptions(Integer ackTimeout, Integer responseTimeout, Integer waitForResponses, MessageTemplate messageTemplate, String forwardNamespace, String routingKey) { this.ackTimeout = ackTimeout; this.responseTimeout = responseTimeout; this.waitForResponses = waitForResponses; this.messageTemplate = messageTemplate; + this.forwardNamespace = forwardNamespace; + this.routingKey = routingKey; } public Integer getAckTimeout() { @@ -42,37 +56,54 @@ public Integer getResponseTimeout() { return responseTimeout; } - public Integer getWaitForResponses() { - if (waitForResponses == null) { - return 0; + public int getWaitForResponses() { + if (waitForResponses == null || waitForResponses == -1) { + // use for infinite number or expected responses + return WAIT_FOR_RESPONSES_UNTIL_TIMEOUT; } else { return waitForResponses; } } - public boolean isWaitForResponses() { - return getWaitForResponses() != 0; - } - public MessageTemplate getMessageTemplate() { return messageTemplate; } + public String getForwardNamespace() { + return forwardNamespace; + } + + public String getRoutingKey() { + return routingKey; + } + + public Builder asBuilder() { + return new RequestOptions.Builder().from(this); + } + @Override public String toString() { return "RequestOptions [ackTimeout=" + ackTimeout + ", responseTimeout=" + responseTimeout + ", waitForResponses=" + waitForResponses + + ", forwardNamespace=" + forwardNamespace + (messageTemplate != null ? messageTemplate : "") + "]"; } public static class Builder { - private Integer ackTimeout; - private Integer responseTimeout; - private Integer waitForResponses; - private MessageTemplate messageTemplate; + protected String routingKey; + protected Integer ackTimeout; + protected Integer responseTimeout; + protected Integer waitForResponses; + protected MessageTemplate messageTemplate; + protected String forwardNamespace; + + public Builder withRoutingKey(String routingKey) { + this.routingKey = routingKey; + return this; + } public Builder withAckTimeout(Integer ackTimeout) { this.ackTimeout = ackTimeout; @@ -94,8 +125,28 @@ public Builder withMessageTemplate(MessageTemplate messageTemplate) { return this; } + public Builder withForwardNamespace(String forward) { + this.forwardNamespace = forward; + return this; + } + + /** + * Convenience method to prepare Builder with properties equal to {@literal source} properties. + * Is useful for cases when almost same RequestOptions except one or two properties are needed. + */ + protected Builder from(RequestOptions source) { + this.ackTimeout = source.ackTimeout; + this.responseTimeout = source.responseTimeout; + this.waitForResponses = source.waitForResponses; + this.messageTemplate = source.messageTemplate; + this.forwardNamespace = source.forwardNamespace; + this.routingKey = source.routingKey; + return this; + } + public RequestOptions build() { - return new RequestOptions(ackTimeout, responseTimeout, waitForResponses, messageTemplate); + return new RequestOptions(ackTimeout, responseTimeout, waitForResponses, messageTemplate, forwardNamespace, + routingKey != null ? routingKey : StringUtils.EMPTY); } } } diff --git a/core/src/main/java/io/github/tcdl/msb/api/Requester.java b/core/src/main/java/io/github/tcdl/msb/api/Requester.java index 008b0524..41f9ad4d 100644 --- a/core/src/main/java/io/github/tcdl/msb/api/Requester.java +++ b/core/src/main/java/io/github/tcdl/msb/api/Requester.java @@ -5,6 +5,9 @@ import io.github.tcdl.msb.api.message.Acknowledge; import io.github.tcdl.msb.api.message.Message; +import java.util.concurrent.CompletableFuture; +import java.util.function.BiConsumer; + /** * {@link Requester} enable user send message to bus and process responses for this messages if any expected. * @@ -31,11 +34,11 @@ public interface Requester { * In case Requester created with expectation for responses then process them. * * @param requestPayload payload which will be sent to bus - * @param tag to add to the message + * @param tags to add to the message * @throws ChannelException if an error is encountered during publishing to bus * @throws JsonConversionException if unable to parse message to JSON before sending to bus */ - void publish(Object requestPayload, String tag); + void publish(Object requestPayload, String... tags); /** * Wraps a payload with protocol information, preserves original message and sends to bus. @@ -43,11 +46,11 @@ public interface Requester { * * @param requestPayload payload which will be sent to bus * @param originalMessage - * @param tag to add to the message + * @param tags to add to the message * @throws ChannelException if an error is encountered during publishing to bus * @throws JsonConversionException if unable to parse message to JSON before sending to bus */ - void publish(Object requestPayload, Message originalMessage, String tag); + void publish(Object requestPayload, Message originalMessage, String... tags); /** * Wraps a payload with protocol information, preserves original message and sends to bus. @@ -60,13 +63,52 @@ public interface Requester { */ void publish(Object requestPayload, Message originalMessage); + /** + * Overloaded version of + * {@link Requester#request(java.lang.Object, io.github.tcdl.msb.api.message.Message, java.lang.String...)} + */ + CompletableFuture request(Object requestPayload); + + /** + * Overloaded version of + * {@link Requester#request(java.lang.Object, io.github.tcdl.msb.api.message.Message, java.lang.String...)} + */ + CompletableFuture request(Object requestPayload, String... tags); + + /** + * Overloaded version of + * {@link Requester#request(java.lang.Object, io.github.tcdl.msb.api.message.Message, java.lang.String...)} + */ + CompletableFuture request(Object requestPayload, Message originalMessage); + + /** + * Similar to + * {@link io.github.tcdl.msb.api.Requester#publish(java.lang.Object, io.github.tcdl.msb.api.message.Message, java.lang.String...)} + * but expects exactly one response. CompletableFuture response type adds a lot of flexibility to client implementation. + + * All handlers passed to + *

    + *
  • {@link Requester#onAcknowledge(java.util.function.BiConsumer)}
  • + *
  • {@link Requester#onResponse(java.util.function.BiConsumer)}
  • + *
  • {@link Requester#onRawResponse(java.util.function.BiConsumer)}
  • + *
  • {@link Requester#onEnd(io.github.tcdl.msb.api.Callback)}
  • + *
  • {@link Requester#onError(java.util.function.BiConsumer)}
  • + *
+ * are DISCARDED + * + * @return {@link CompletableFuture} that will be completed when first response is received. + * CompletableFuture will be canceled if timeout occurs or acknowledge with different from 1 remaining responses + * is received. + */ + CompletableFuture request(Object requestPayload, Message originalMessage, String... tags); + /** * Registers a callback to be called when {@link Message} with {@link Acknowledge} part set is received. * * @param acknowledgeHandler callback to be called * @return requester */ - Requester onAcknowledge(Callback acknowledgeHandler); + Requester onAcknowledge(BiConsumer acknowledgeHandler); /** * Registers a callback to be called when response {@link Message} with payload part set of type {@literal T} is received. @@ -75,7 +117,7 @@ public interface Requester { * @return requester * @throws JsonConversionException if unable to convert payload to type {@literal T} */ - Requester onResponse(Callback responseHandler); + Requester onResponse(BiConsumer responseHandler); /** * Registers a callback to be called when response {@link Message} with payload part set of is received. @@ -83,13 +125,24 @@ public interface Requester { * @param responseHandler callback to be called * @return requester */ - Requester onRawResponse(Callback responseHandler); + Requester onRawResponse(BiConsumer responseHandler); /** * Registers a callback to be called when all expected responses for request message are processes or awaiting timeout for responses occurred. + * Will be invoked only after all incoming responses will be processed. * * @param endHandler callback to be called * @return requester */ Requester onEnd(Callback endHandler); + + /** + * Registers a callback to be called if an error is encountered during receiving a response from the bus + * Will be invoked only after all incoming responses will be processed. + * + * @param errorHandler callback to be called + * @return requester + */ + Requester onError(BiConsumer errorHandler); + } diff --git a/core/src/main/java/io/github/tcdl/msb/api/Responder.java b/core/src/main/java/io/github/tcdl/msb/api/Responder.java index d8539859..cf6c754d 100644 --- a/core/src/main/java/io/github/tcdl/msb/api/Responder.java +++ b/core/src/main/java/io/github/tcdl/msb/api/Responder.java @@ -1,6 +1,5 @@ package io.github.tcdl.msb.api; -import io.github.tcdl.msb.api.message.Message; /** * Responsible for creating responses and acknowledgements and sending them to the bus. @@ -22,8 +21,4 @@ public interface Responder { */ void send(Object responsePayload); - /** - * @return original message to send a response to - */ - Message getOriginalMessage(); } \ No newline at end of file diff --git a/core/src/main/java/io/github/tcdl/msb/api/ResponderContext.java b/core/src/main/java/io/github/tcdl/msb/api/ResponderContext.java new file mode 100644 index 00000000..c7221db8 --- /dev/null +++ b/core/src/main/java/io/github/tcdl/msb/api/ResponderContext.java @@ -0,0 +1,14 @@ +package io.github.tcdl.msb.api; + + +/** + * Provides access to Responder Context. + */ +public interface ResponderContext extends MessageContext { + + /** + * @return Responder instance + */ + Responder getResponder(); + +} diff --git a/core/src/main/java/io/github/tcdl/msb/api/ResponderOptions.java b/core/src/main/java/io/github/tcdl/msb/api/ResponderOptions.java new file mode 100644 index 00000000..b56c31d2 --- /dev/null +++ b/core/src/main/java/io/github/tcdl/msb/api/ResponderOptions.java @@ -0,0 +1,60 @@ +package io.github.tcdl.msb.api; + +import joptsimple.internal.Strings; +import org.apache.commons.lang3.Validate; + +import javax.annotation.Nonnull; +import java.util.Collections; +import java.util.Set; + +/** + * Specifies options for {@link ResponderServer} + */ +public class ResponderOptions { + + private final Set bindingKeys; + private final MessageTemplate messageTemplate; + + public static final ResponderOptions DEFAULTS = new Builder().build(); + + protected ResponderOptions(Set bindingKeys, + MessageTemplate messageTemplate) { + + this.bindingKeys = Collections.unmodifiableSet(bindingKeys); + this.messageTemplate = messageTemplate; + } + + @Nonnull + public Set getBindingKeys() { + return bindingKeys; + } + + public MessageTemplate getMessageTemplate() { + return messageTemplate; + } + + public static class Builder { + + protected Set bindingKeys; + protected MessageTemplate messageTemplate; + + /** + * Each invocation REPLACES the old set of binding keys. Last one wins. + */ + public Builder withBindingKeys(Set bindingKeys) { + this.bindingKeys = bindingKeys; + return this; + } + + public Builder withMessageTemplate(MessageTemplate responseMessageTemplate) { + this.messageTemplate = responseMessageTemplate; + return this; + } + + public ResponderOptions build() { + return new ResponderOptions( + bindingKeys == null ? Collections.singleton(Strings.EMPTY) : bindingKeys, + messageTemplate == null ? new MessageTemplate() : messageTemplate); + } + } +} diff --git a/core/src/main/java/io/github/tcdl/msb/api/ResponderServer.java b/core/src/main/java/io/github/tcdl/msb/api/ResponderServer.java index 860bac5e..f594798d 100644 --- a/core/src/main/java/io/github/tcdl/msb/api/ResponderServer.java +++ b/core/src/main/java/io/github/tcdl/msb/api/ResponderServer.java @@ -1,5 +1,8 @@ package io.github.tcdl.msb.api; +import io.github.tcdl.msb.api.message.Message; +import io.github.tcdl.msb.api.metrics.MetricSet; + /** * {@link ResponderServer} enable user to listen on messages from the bus and executing microservice business logic. * Call to {@link #listen()} method will start listening on incoming messages from the bus. @@ -15,7 +18,17 @@ public interface ResponderServer { /** * Start listening for message on specified topic. */ - ResponderServer listen(); + ResponderServer listen(); + + /** + * Stop listening + */ + ResponderServer stop(); + + /** + Returns set of metrics related to the current responder server + */ + MetricSet getMetrics(); /** * Implementation of this interface contains business logic processed by microservice. @@ -24,10 +37,24 @@ interface RequestHandler { /** * Execute business logic and send response. * @param request request received from a bus - * @param responder object of type {@link Responder} which will be used for sending response + * @param responderContext object of type {@link ResponderContext} which will + * provide access to {@link Responder} that used for sending response and + * {@link AcknowledgementHandler} that used for explicit confirm/reject received request * @throws Exception if some problems during execution business logic or sending response were occurred */ - void process(T request, Responder responder) throws Exception; + void process(T request, ResponderContext responderContext) throws Exception; } + /** + * Implementation of this interface contains custom error handler + */ + interface ErrorHandler { + /** + * Executes user defined error handler + * + * @param exception error cause + * @param originalMessage original message + */ + void handle(Exception exception, Message originalMessage); + } } diff --git a/core/src/main/java/io/github/tcdl/msb/api/exception/AdapterCreationException.java b/core/src/main/java/io/github/tcdl/msb/api/exception/AdapterCreationException.java new file mode 100644 index 00000000..a8196da7 --- /dev/null +++ b/core/src/main/java/io/github/tcdl/msb/api/exception/AdapterCreationException.java @@ -0,0 +1,11 @@ +package io.github.tcdl.msb.api.exception; + +public class AdapterCreationException extends MsbException { + public AdapterCreationException(String message) { + super(message); + } + + public AdapterCreationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/core/src/main/java/io/github/tcdl/msb/api/exception/DuplicateCollectorException.java b/core/src/main/java/io/github/tcdl/msb/api/exception/DuplicateCollectorException.java deleted file mode 100644 index 9ebae2eb..00000000 --- a/core/src/main/java/io/github/tcdl/msb/api/exception/DuplicateCollectorException.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.github.tcdl.msb.api.exception; - -/** - * Exception is thrown in case more then one Collector is created per correlationId. - */ -public class DuplicateCollectorException extends MsbException { - public DuplicateCollectorException(String message) { - super(message); - } -} diff --git a/core/src/main/java/io/github/tcdl/msb/api/exception/MessageBuilderException.java b/core/src/main/java/io/github/tcdl/msb/api/exception/MessageBuilderException.java deleted file mode 100644 index f5b8ac3a..00000000 --- a/core/src/main/java/io/github/tcdl/msb/api/exception/MessageBuilderException.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.github.tcdl.msb.api.exception; - -/** - * Wraps any exception thrown while converting Java object to/from during building {@link io.github.tcdl.msb.api.message.Message}. - */ -public class MessageBuilderException extends MsbException { - - public MessageBuilderException(String message) { - super(message); - } -} diff --git a/core/src/main/java/io/github/tcdl/msb/api/message/Message.java b/core/src/main/java/io/github/tcdl/msb/api/message/Message.java index 191e70fd..1d8eb68e 100644 --- a/core/src/main/java/io/github/tcdl/msb/api/message/Message.java +++ b/core/src/main/java/io/github/tcdl/msb/api/message/Message.java @@ -28,9 +28,7 @@ public final class Message { private final Topics topics; @JsonInclude(ALWAYS) private final MetaMessage meta; // To be filled with createMeta() ->completeMeta() sequence - @JsonInclude(ALWAYS) private final Acknowledge ack; // To be filled on ack or response - @JsonInclude(ALWAYS) @JsonProperty("payload") private final JsonNode rawPayload; diff --git a/core/src/main/java/io/github/tcdl/msb/api/message/MetaMessage.java b/core/src/main/java/io/github/tcdl/msb/api/message/MetaMessage.java index 9a86e0f0..13da5ed7 100644 --- a/core/src/main/java/io/github/tcdl/msb/api/message/MetaMessage.java +++ b/core/src/main/java/io/github/tcdl/msb/api/message/MetaMessage.java @@ -45,7 +45,7 @@ public Builder(Integer ttl, Instant createdAt, ServiceDetails serviceDetails, Cl public MetaMessage build() { publishedAt = clock.instant(); - Long durationMs = Duration.between(publishedAt, this.createdAt).toMillis();; + Long durationMs = Duration.between(this.createdAt, publishedAt).toMillis();; return new MetaMessage(ttl, createdAt, publishedAt, durationMs, serviceDetails); } } diff --git a/core/src/main/java/io/github/tcdl/msb/api/message/Topics.java b/core/src/main/java/io/github/tcdl/msb/api/message/Topics.java index 20495d88..32b3ce00 100644 --- a/core/src/main/java/io/github/tcdl/msb/api/message/Topics.java +++ b/core/src/main/java/io/github/tcdl/msb/api/message/Topics.java @@ -1,19 +1,32 @@ package io.github.tcdl.msb.api.message; -import org.apache.commons.lang3.Validate; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.commons.lang3.Validate; public final class Topics { private final String to; private final String response; + private final String forward; + private final String routingKey; @JsonCreator - public Topics(@JsonProperty("to") String to, @JsonProperty("response") String response) { + public Topics(@JsonProperty("to") String to, + @JsonProperty("response") String response, + @JsonProperty("forward") String forward, + @JsonProperty("routingKey") String routingKey) { + Validate.notNull(to, "the 'to' must not be null"); this.to = to; this.response = response; + this.forward = forward; + this.routingKey = routingKey; + } + + public Topics(String to, String response, String forward) { + this(to, response, forward, null); } public String getTo() { @@ -24,8 +37,16 @@ public String getResponse() { return response; } + public String getForward() { + return forward; + } + + public String getRoutingKey() { + return routingKey; + } + @Override public String toString() { - return "Topics [to=" + to + ", response=" + response + "]"; + return "Topics [to=" + to + ", response=" + response + ", forward=" + forward + ", routingKey=" + routingKey + "]"; } } diff --git a/core/src/main/java/io/github/tcdl/msb/api/metrics/Gauge.java b/core/src/main/java/io/github/tcdl/msb/api/metrics/Gauge.java new file mode 100644 index 00000000..0d03639f --- /dev/null +++ b/core/src/main/java/io/github/tcdl/msb/api/metrics/Gauge.java @@ -0,0 +1,15 @@ +package io.github.tcdl.msb.api.metrics; + +/** + * A gauge metric is an instantaneous reading of a particular value + * @param the type of the metric's value + */ + +@FunctionalInterface +public interface Gauge extends Metric { + + /** + * @return the metric's current value + */ + T getValue(); +} \ No newline at end of file diff --git a/core/src/main/java/io/github/tcdl/msb/api/metrics/Metric.java b/core/src/main/java/io/github/tcdl/msb/api/metrics/Metric.java new file mode 100644 index 00000000..331350d2 --- /dev/null +++ b/core/src/main/java/io/github/tcdl/msb/api/metrics/Metric.java @@ -0,0 +1,7 @@ +package io.github.tcdl.msb.api.metrics; + +/** + * A marker interface to indicate that a class is a metric. + */ +public interface Metric { +} \ No newline at end of file diff --git a/core/src/main/java/io/github/tcdl/msb/api/metrics/MetricSet.java b/core/src/main/java/io/github/tcdl/msb/api/metrics/MetricSet.java new file mode 100644 index 00000000..8cc4e6cf --- /dev/null +++ b/core/src/main/java/io/github/tcdl/msb/api/metrics/MetricSet.java @@ -0,0 +1,34 @@ +package io.github.tcdl.msb.api.metrics; + +import java.util.Map; + +/** + * A set of named metrics. + */ +public interface MetricSet extends Metric { + + /** + * {@value #MESSAGE_COUNT_METRIC} metric key for the number available messages as {@link Gauge} of {@link Long} type + */ + String MESSAGE_COUNT_METRIC = "availableMessageCount"; + + /** + * {@value #CONSUMER_CONNECTED_METRIC} metric key for the consumer status {@link Gauge} of {@link Boolean} type + */ + String CONSUMER_CONNECTED_METRIC = "consumerConnected"; + + /** + * @return supported metric by name + */ + default Metric getMetric(String metricName) { + return getMetrics().get(metricName); + } + + /** + * A map of metric names to metrics. + * + * @return the metrics + */ + Map getMetrics(); + +} \ No newline at end of file diff --git a/core/src/main/java/io/github/tcdl/msb/api/monitor/AggregatorStats.java b/core/src/main/java/io/github/tcdl/msb/api/monitor/AggregatorStats.java deleted file mode 100644 index 3a6fa557..00000000 --- a/core/src/main/java/io/github/tcdl/msb/api/monitor/AggregatorStats.java +++ /dev/null @@ -1,40 +0,0 @@ -package io.github.tcdl.msb.api.monitor; - -import io.github.tcdl.msb.config.ServiceDetails; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Represents aggregated statistics collected from multiple instances of microservices - * (that is transmitted over the bus by from their {@link io.github.tcdl.msb.monitor.agent.ChannelMonitorAgent}s). - */ -public class AggregatorStats { - - /** - * Collects information about all topics that we know of. - *

- * Structure of the map: topics name -> stats - */ - private Map topicInfoMap = new ConcurrentHashMap<>(); - - /** - * Collects information about all instances of microservices that we know of. - *

- * Structure of the map: service instance id -> details - */ - private Map serviceDetailsById = new ConcurrentHashMap<>(); - - public Map getTopicInfoMap() { - return topicInfoMap; - } - - public Map getServiceDetailsById() { - return serviceDetailsById; - } - - @Override public String toString() { - return String.format("AggregatorStats [topicInfoMap=%s, serviceDetailsById=%s]", topicInfoMap, serviceDetailsById); - } - -} diff --git a/core/src/main/java/io/github/tcdl/msb/api/monitor/AggregatorTopicStats.java b/core/src/main/java/io/github/tcdl/msb/api/monitor/AggregatorTopicStats.java deleted file mode 100644 index e9276674..00000000 --- a/core/src/main/java/io/github/tcdl/msb/api/monitor/AggregatorTopicStats.java +++ /dev/null @@ -1,106 +0,0 @@ -package io.github.tcdl.msb.api.monitor; - -import javax.annotation.Nullable; -import java.time.Instant; -import java.util.Objects; -import java.util.Set; -import java.util.concurrent.ConcurrentSkipListSet; - -/** - * Represents statistics for the given topic - */ -public class AggregatorTopicStats { - /** - * Instance ids of services producing to this topic - */ - private Set producers = new ConcurrentSkipListSet<>(); - - /** - * Instance ids of services consuming from this topics - */ - private Set consumers = new ConcurrentSkipListSet<>(); - - /** - * Time when the last message was produced to the topic - */ - private Instant lastProducedAt; - - /** - * Time when the last message was consumed from the topic - */ - private Instant lastConsumedAt; - - public AggregatorTopicStats() { - } - - public AggregatorTopicStats(@Nullable AggregatorTopicStats aggregatorTopicStats) { - if (aggregatorTopicStats != null) { - this.producers = aggregatorTopicStats.producers; - this.consumers = aggregatorTopicStats.consumers; - this.lastProducedAt = aggregatorTopicStats.lastProducedAt; - this.lastConsumedAt = aggregatorTopicStats.lastConsumedAt; - } - } - - public AggregatorTopicStats withProducers(Set producers) { - this.producers = producers; - return this; - } - - public AggregatorTopicStats withConsumers(Set consumers) { - this.consumers = consumers; - return this; - } - - public AggregatorTopicStats withLastProducedAt(Instant lastProducedAt) { - this.lastProducedAt = lastProducedAt; - return this; - } - - public AggregatorTopicStats withLastConsumedAt(Instant lastConsumedAt) { - this.lastConsumedAt = lastConsumedAt; - return this; - } - - public Set getProducers() { - return producers; - } - - public Set getConsumers() { - return consumers; - } - - public Instant getLastProducedAt() { - return lastProducedAt; - } - - public Instant getLastConsumedAt() { - return lastConsumedAt; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - AggregatorTopicStats that = (AggregatorTopicStats) o; - return Objects.equals(producers, that.producers) && - Objects.equals(consumers, that.consumers) && - Objects.equals(lastProducedAt, that.lastProducedAt) && - Objects.equals(lastConsumedAt, that.lastConsumedAt); - } - - @Override - public int hashCode() { - return Objects.hash(producers, consumers, lastProducedAt, lastConsumedAt); - } - - @Override - public String toString() { - return String.format("AggregatorTopicStats [producers=%s, consumers=%s, lastProducedAt=%s, lastConsumedAt=%s]", producers, consumers, lastProducedAt, - lastConsumedAt); - } -} diff --git a/core/src/main/java/io/github/tcdl/msb/api/monitor/ChannelMonitorAggregator.java b/core/src/main/java/io/github/tcdl/msb/api/monitor/ChannelMonitorAggregator.java deleted file mode 100644 index 3738a8d4..00000000 --- a/core/src/main/java/io/github/tcdl/msb/api/monitor/ChannelMonitorAggregator.java +++ /dev/null @@ -1,51 +0,0 @@ -package io.github.tcdl.msb.api.monitor; - -import io.github.tcdl.msb.api.Callback; -import io.github.tcdl.msb.api.ObjectFactory; - -/** - * Gathers statistics over the bus from other running microservices that have {@link io.github.tcdl.msb.monitor.agent.ChannelMonitorAgent} activated via - * {@link io.github.tcdl.msb.api.MsbContextBuilder#enableChannelMonitorAgent(boolean)}. The statistics is taken from 2 sources: - * - * 1. By listening to {@link io.github.tcdl.msb.support.Utils#TOPIC_ANNOUNCE} - * 2. By sending periodic heartbeats to {@link io.github.tcdl.msb.support.Utils#TOPIC_HEARTBEAT} and analysing responses. This responses will be aggregated and - * then overwrite stats with most recent information to detect that some microservices went down. - * - * Typical lifecycle for this aggregator is: - * 1. Create instance via {@link ObjectFactory#createChannelMonitorAggregator(Callback)} - * 2. Activate the aggregator via {@link #start()} - * 3. The registered handler processes the stats from the bus - * 4. Deactivate the aggregator via {@link #stop()} - */ -public interface ChannelMonitorAggregator { - - /** - * See {@link #start(boolean, long, int)} - */ - long DEFAULT_HEARTBEAT_INTERVAL_MS = 10000; - - /** - * See {@link #start(boolean, long, int)} - */ - int DEFAULT_HEARTBEAT_TIMEOUT_MS = 5000; - - /** - * Convenience method that activates the aggregator with default heartbeat parameters - */ - default void start() { - start(true, DEFAULT_HEARTBEAT_INTERVAL_MS, DEFAULT_HEARTBEAT_TIMEOUT_MS); - } - - /** - * Activates the aggregator with the given parameters - * @param activateHeartbeats if equal to false then no periodic heartbeats are sent and stats is obtained only from announcement channel - * @param heartbeatIntervalMs Interval in milliseconds between heartbeat requests - * @param heartbeatTimeoutMs how long does the aggregator waits in milliseconds for responses after each heartbeat - */ - void start(boolean activateHeartbeats, long heartbeatIntervalMs, int heartbeatTimeoutMs); - - /** - * Deactivates this aggregator. After this method is invoked the object is not usable. This method can be invoked multiple times. - */ - void stop(); -} diff --git a/core/src/main/java/io/github/tcdl/msb/callback/CallbackHandler.java b/core/src/main/java/io/github/tcdl/msb/callback/CallbackHandler.java new file mode 100644 index 00000000..1710e9f4 --- /dev/null +++ b/core/src/main/java/io/github/tcdl/msb/callback/CallbackHandler.java @@ -0,0 +1,13 @@ +package io.github.tcdl.msb.callback; + +/** + * Container for {@link Runnable} callbacks. + */ +public interface CallbackHandler { + + /** + * Run all callbacks within the container, catch and log exceptions if any. + */ + void runCallbacks(); + +} diff --git a/core/src/main/java/io/github/tcdl/msb/callback/CallbackHandlerBase.java b/core/src/main/java/io/github/tcdl/msb/callback/CallbackHandlerBase.java new file mode 100644 index 00000000..e0a6bf4b --- /dev/null +++ b/core/src/main/java/io/github/tcdl/msb/callback/CallbackHandlerBase.java @@ -0,0 +1,25 @@ +package io.github.tcdl.msb.callback; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.LinkedHashSet; +import java.util.Set; + +public abstract class CallbackHandlerBase implements CallbackHandler { + + private static final Logger LOG = LoggerFactory.getLogger(CallbackHandlerBase.class); + + protected final Set callbacks = new LinkedHashSet<>(); + + public void runCallbacks() { + for(Runnable callback: callbacks) { + try { + callback.run(); + } catch (Exception ex) { + LOG.warn("Exception while trying to invoke a callback", ex); + } + } + } + +} diff --git a/core/src/main/java/io/github/tcdl/msb/callback/MutableCallbackHandler.java b/core/src/main/java/io/github/tcdl/msb/callback/MutableCallbackHandler.java new file mode 100644 index 00000000..8c8bf018 --- /dev/null +++ b/core/src/main/java/io/github/tcdl/msb/callback/MutableCallbackHandler.java @@ -0,0 +1,23 @@ +package io.github.tcdl.msb.callback; + +/** + * Mutable {@link CallbackHandler} implementation. + */ +public class MutableCallbackHandler extends CallbackHandlerBase { + + /** + * Add a callback. + * @param runnable + */ + public void add(Runnable runnable) { + callbacks.add(runnable); + } + + /** + * Remove a callback. + * @param runnable + */ + public void remove(Runnable runnable) { + callbacks.remove(runnable); + } +} diff --git a/core/src/main/java/io/github/tcdl/msb/collector/Collector.java b/core/src/main/java/io/github/tcdl/msb/collector/Collector.java index 87b6687a..64fbf1e8 100644 --- a/core/src/main/java/io/github/tcdl/msb/collector/Collector.java +++ b/core/src/main/java/io/github/tcdl/msb/collector/Collector.java @@ -3,70 +3,120 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import io.github.tcdl.msb.api.AcknowledgementHandler; import io.github.tcdl.msb.api.Callback; +import io.github.tcdl.msb.api.MessageContext; import io.github.tcdl.msb.api.RequestOptions; import io.github.tcdl.msb.api.message.Acknowledge; import io.github.tcdl.msb.api.message.Message; +import io.github.tcdl.msb.config.MsbConfig; import io.github.tcdl.msb.events.EventHandlers; +import io.github.tcdl.msb.impl.MessageContextImpl; import io.github.tcdl.msb.impl.MsbContextImpl; import io.github.tcdl.msb.support.Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.annotation.concurrent.NotThreadSafe; import java.time.Clock; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.atomic.LongAdder; +import java.util.function.BiConsumer; import static io.github.tcdl.msb.support.Utils.ifNull; +import static java.lang.Math.toIntExact; /** * {@link Collector} is a component which collects responses and acknowledgements for sent requests. */ -public class Collector { +@NotThreadSafe +public class Collector implements ConsumedMessagesAwareMessageHandler, ExecutionOptionsAwareMessageHandler { private static final Logger LOG = LoggerFactory.getLogger(Collector.class); - private static final int WAIT_FOR_RESPONSES_UNTIL_TIMEOUT = -1; + private final List ackMessages; + private final List payloadMessages; - private List ackMessages; - private List payloadMessages; + private final Map timeoutMsById; + private final Map responsesRemainingById; + private final Set handledMessagesIds; - private Map timeoutMsById; - private Map responsesRemainingById; + private final int timeoutMs; + private volatile int currentTimeoutMs; + private final Integer waitForAcksMs; + private volatile Instant waitForAcksUntil; - private int timeoutMs; - private int currentTimeoutMs; - private long waitForAcksUntil; - private int waitForResponses; - private TypeReference payloadTypeReference; - private int responsesRemaining; + private volatile int responsesRemaining; + private final boolean shouldWaitUntilResponseTimeout; - private Long startedAt; - private TimeoutManager timeoutManager; - private ObjectMapper payloadMapper; + private final TypeReference payloadTypeReference; - private Clock clock; - private Message requestMessage; + private final long startedAt; + private final TimeoutManager timeoutManager; + private final ObjectMapper payloadMapper; - private Optional> onRawResponse = Optional.empty(); - private Optional> onResponse = Optional.empty(); - private Optional> onAcknowledge = Optional.empty(); - private Optional> onEnd = Optional.empty(); + private final Clock clock; + private final Message requestMessage; + + private final Optional> onRawResponse; + private final Optional> onResponse; + private final Optional> onAcknowledge; + private final Optional> onEnd; + private final Optional> onError; private ScheduledFuture ackTimeoutFuture; private ScheduledFuture responseTimeoutFuture; - private CollectorManager collectorManager; - private boolean shouldWaitUntilResponseTimeout; + private final CollectorManager collectorManager; + private final MsbConfig msbConfig; + + /** + * Count of consumed incoming messages so {@link #handleMessage} invocation is expected in future. + * Even redelivered messages increment this counter. + */ + private final LongAdder consumedMessagesCount = new LongAdder(); + + /** + * Count of consumed incoming messages that were lost afterwards so {@link #handleMessage} invocation is + * no longer expected + */ + private final LongAdder consumedAndLostMessagesCount = new LongAdder(); + + /** + * Counter of consumed incoming messages that are already handled by {@link #handleMessage}. + */ + private final LongAdder handledMessagesHandledCount = new LongAdder(); + + /** + * Is the current instance unsubscribed from message source so new incoming messages are no longer expected. + */ + private volatile boolean isUnsubscribed = false; + + /** + * Was the "onEnd" callback invoked? Used to guarantee that "onEnd" will not be invoked more than once. + */ + private volatile boolean isOnEndInvoked = false; + + private final boolean directlyInvokable; + + public Collector(String topic, Message requestMessage, RequestOptions requestOptions, MsbContextImpl msbContext, EventHandlers eventHandlers, + TypeReference payloadTypeReference) { + this(topic, requestMessage, requestOptions, msbContext, eventHandlers, payloadTypeReference, false); + } - public Collector(String topic, Message requestMessage, RequestOptions requestOptions, MsbContextImpl msbContext, EventHandlers eventHandlers, TypeReference payloadTypeReference) { + public Collector(String topic, Message requestMessage, RequestOptions requestOptions, MsbContextImpl msbContext, EventHandlers eventHandlers, + TypeReference payloadTypeReference, boolean directlyInvokableCallbacks) { this.requestMessage = requestMessage; + this.msbConfig = msbContext.getMsbConfig(); this.clock = msbContext.getClock(); this.collectorManager = msbContext.getCollectorManagerFactory().findOrCreateCollectorManager(topic); this.timeoutManager = msbContext.getTimeoutManager(); @@ -77,116 +127,177 @@ public Collector(String topic, Message requestMessage, RequestOptions requestOpt this.payloadMessages = new LinkedList<>(); this.timeoutMsById = new HashMap<>(); this.responsesRemainingById = new HashMap<>(); + this.handledMessagesIds = new HashSet<>(); + + this.waitForAcksMs = requestOptions.getAckTimeout(); + this.waitForAcksUntil = null; this.timeoutMs = getResponseTimeoutFromConfigs(requestOptions); this.currentTimeoutMs = timeoutMs; - this.waitForAcksUntil = getWaitForAckUntilFromConfigs(requestOptions); - this.waitForResponses = requestOptions.getWaitForResponses(); - this.responsesRemaining = waitForResponses; + this.responsesRemaining = requestOptions.getWaitForResponses(); + + this.shouldWaitUntilResponseTimeout = (responsesRemaining == RequestOptions.WAIT_FOR_RESPONSES_UNTIL_TIMEOUT); + this.payloadTypeReference = payloadTypeReference; - this.shouldWaitUntilResponseTimeout = requestOptions.getWaitForResponses() == WAIT_FOR_RESPONSES_UNTIL_TIMEOUT; - if (eventHandlers != null) { - onRawResponse = Optional.ofNullable(eventHandlers.onRawResponse()); - onResponse = Optional.ofNullable(eventHandlers.onResponse()); - onAcknowledge = Optional.ofNullable(eventHandlers.onAcknowledge()); - onEnd = Optional.ofNullable(eventHandlers.onEnd()); + onRawResponse = Optional.ofNullable(eventHandlers.onRawResponse()); + onResponse = Optional.ofNullable(eventHandlers.onResponse()); + onAcknowledge = Optional.ofNullable(eventHandlers.onAcknowledge()); + onEnd = Optional.ofNullable(eventHandlers.onEnd()); + onError = Optional.ofNullable(eventHandlers.onError()); + this.directlyInvokable = directlyInvokableCallbacks; + } + + @Override + public synchronized void notifyMessageConsumed() { + consumedMessagesCount.increment(); + } + + @Override + public synchronized void notifyConsumedMessageIsLost() { + consumedAndLostMessagesCount.increment(); + if(isNoMoreMessagesHandlingPossible()) { + end(); } } boolean isAwaitingAcks() { - return waitForAcksUntil > clock.instant().toEpochMilli(); + return this.waitForAcksUntil != null && waitForAcksUntil.isAfter(Instant.now()); } - public boolean isAwaitingResponses() { - return getResponsesRemaining() > 0 || shouldWaitUntilResponseTimeout; + boolean isAwaitingResponses() { + return shouldWaitUntilResponseTimeout || getResponsesRemaining() > 0; } public void listenForResponses() { + if (this.waitForAcksMs != null && this.waitForAcksMs != 0) { + this.waitForAcksUntil = Instant.ofEpochMilli(this.startedAt).plusMillis(waitForAcksMs); + } collectorManager.registerCollector(this); } - public void handleMessage(Message incomingMessage) { - LOG.debug("Received {}", incomingMessage); + @Override + public void handleMessage(Message incomingMessage, AcknowledgementHandler acknowledgeHandler) { + LOG.debug("[correlation ids: {}-{}] Received.", + requestMessage.getCorrelationId(), incomingMessage.getCorrelationId()); + LOG.trace("Message: {}", incomingMessage); JsonNode rawPayload = incomingMessage.getRawPayload(); - if (Utils.isPayloadPresent(rawPayload)) { - LOG.debug("Received Payload {}", rawPayload); - payloadMessages.add(incomingMessage); - onRawResponse.ifPresent(handler -> handler.call(incomingMessage)); + MessageContext messageContext = createMessageContext(acknowledgeHandler, incomingMessage); + boolean isWithPayload = Utils.isPayloadPresent(rawPayload); - T payload = Utils.convert(rawPayload, payloadTypeReference, payloadMapper); - onResponse.ifPresent(handler -> handler.call(payload)); - - incResponsesRemaining(-1); + if (isWithPayload) { + LOG.debug("[correlation ids: {}-{}] Received Payload.", + requestMessage.getCorrelationId(), incomingMessage.getCorrelationId()); + LOG.trace("Payload: {}", rawPayload); + payloadMessages.add(incomingMessage); + try { + onRawResponse.ifPresent(handler -> handler.accept(incomingMessage, messageContext)); + + T payload = Utils.convert(rawPayload, payloadTypeReference, payloadMapper); + onResponse.ifPresent(handler -> handler.accept(payload, messageContext)); + } catch (Exception e) { + //do not propagate exception outside of this method in order to prevent autoRetry for responses + LOG.warn("Unexpected exception during response handler invocation", e); + onError.ifPresent(handler -> handler.accept(e, incomingMessage)); + } } else { - LOG.debug("Received {}", incomingMessage.getAck()); + LOG.debug("[correlation ids: {}-{}] Received {}", + requestMessage.getCorrelationId(), incomingMessage.getCorrelationId(), incomingMessage.getAck()); ackMessages.add(incomingMessage); - onAcknowledge.ifPresent(handler -> handler.call((incomingMessage.getAck()))); + onAcknowledge.ifPresent(handler -> handler.accept(incomingMessage.getAck(), messageContext)); } processAck(incomingMessage.getAck()); - if (isAwaitingResponses()) { - return; - } + synchronized (this) { + handledMessagesHandledCount.increment(); + updateCounters(incomingMessage, isWithPayload); + + boolean isInvokeOnEnd = false; + if (!isAwaitingResponses()) { + //set ack timer task in case we received ALL expected responses but still have to wait for ack + if (isAwaitingAcks()) { + waitForAcks(); + } else { + isInvokeOnEnd = true; + } + } - //set ack timer task in case we received ALL expected responses but still have to wait for ack - if (isAwaitingAcks()) { - waitForAcks(); - return; - } + isInvokeOnEnd = isInvokeOnEnd || isNoMoreMessagesHandlingPossible(); - end(); + if(isInvokeOnEnd) { + LOG.debug("[correlation ids: {}] All messages has been received", requestMessage.getCorrelationId()); + end(); + } + } } - protected void end() { - LOG.debug("Stop response processing"); + MessageContext createMessageContext(AcknowledgementHandler acknowledgementHandler, Message originalMessage) { + return new MessageContextImpl(acknowledgementHandler, originalMessage); + } + protected synchronized void end() { + LOG.debug("[correlation id: {}] Stop response processing ", requestMessage.getCorrelationId()); cancelAckTimeoutTask(); cancelResponseTimeoutTask(); collectorManager.unregisterCollector(this); - onEnd.ifPresent(handler -> handler.call(null)); + isUnsubscribed = true; + + if(!isOnEndInvoked && isAllConsumedMessagesHandled()) { + isOnEndInvoked = true; + LOG.debug("[correlation id: {}] triggering 'onEnd' callback", requestMessage.getCorrelationId()); + try { + onEnd.ifPresent(handler -> handler.call(null)); + } catch (Exception e) { + LOG.warn("Unexpected exception during 'onEnd' handler invocation", e); + } + } } - public void waitForResponses() { - LOG.debug("Waiting for responses until {}.", clock.instant().plus(currentTimeoutMs, ChronoUnit.MILLIS)); - this.responseTimeoutFuture = timeoutManager.enableResponseTimeout(this.currentTimeoutMs, this); + /** + * Returns true if no more {@link #handleMessage} invocations are expected. + */ + private boolean isNoMoreMessagesHandlingPossible() { + return isUnsubscribed && isAllConsumedMessagesHandled(); } - private void waitForAcks() { - if (ackTimeoutFuture == null) { - LOG.debug("Waiting for ack until {}.", Instant.ofEpochMilli(this.waitForAcksUntil)); - long ackTimeoutMs = waitForAcksUntil - clock.instant().toEpochMilli(); - ackTimeoutFuture = timeoutManager.enableAckTimeout(ackTimeoutMs, this); - } else { - LOG.debug("Ack timeout is already scheduled"); - } - } + /** + * Returns true if all incoming messages consumed at the moment were handled by {@link #handleMessage}. + * But it is possible, that some new messages will be consumed and handled afterwards. + */ + private synchronized boolean isAllConsumedMessagesHandled() { + int consumed = consumedMessagesCount.intValue(); + int handled = handledMessagesHandledCount.intValue(); + int consumedAndLost = consumedAndLostMessagesCount.intValue(); - private void processAck(Acknowledge acknowledge) { - if (acknowledge == null) - return; + LOG.debug("[correlation id: {}] Messages consumed: {}; handled: {} consumed and lost: {} ", + requestMessage.getCorrelationId(), consumed, handled, consumedAndLost); - if (acknowledge.getTimeoutMs() != null && acknowledge.getResponderId() != null) { - Integer newTimeoutMs = setTimeoutMsForResponderId(acknowledge.getResponderId(), acknowledge.getTimeoutMs()); - if (newTimeoutMs != null) { - int prevTimeoutMs = this.currentTimeoutMs; + return (consumed == consumedAndLost + handled); + } - this.currentTimeoutMs = getMaxTimeoutMs(); - if (prevTimeoutMs != currentTimeoutMs) { - cancelResponseTimeoutTask(); - this.responseTimeoutFuture = timeoutManager.enableResponseTimeout(this.currentTimeoutMs, this); - } - } + void processAck(Acknowledge acknowledge) { + if (acknowledge == null) { + return; } - if (acknowledge.getResponsesRemaining() != null) { - LOG.debug("Responses remaining for responderId [{}] is set to {}", acknowledge.getResponderId(), + LOG.debug("[correlation id: {}] Responses remaining for responderId [{}] is set to {}", + requestMessage.getCorrelationId(), acknowledge.getResponderId(), setResponsesRemainingForResponderId(acknowledge.getResponderId(), acknowledge.getResponsesRemaining())); } + + if (acknowledge.getTimeoutMs() != null) { + setTimeoutMsForResponderId(acknowledge.getResponderId(), acknowledge.getTimeoutMs()); + } + + Integer newTimeoutMs = getMaxTimeoutMs(); + if (newTimeoutMs != this.currentTimeoutMs) { + this.currentTimeoutMs = newTimeoutMs; + waitForResponses(); + } } private Integer setTimeoutMsForResponderId(String responderId, Integer timeoutMs) { @@ -198,19 +309,6 @@ private Integer setTimeoutMsForResponderId(String responderId, Integer timeoutMs return timeoutMs; } - int getResponsesRemaining() { - if (responsesRemainingById == null || responsesRemainingById.isEmpty()) { - return responsesRemaining; - } - - Integer sumOfResponsesRemaining = 0; - for (Integer responses : responsesRemainingById.values()) { - sumOfResponsesRemaining += responses; - } - - return Math.max(responsesRemaining, sumOfResponsesRemaining); - } - private int getMaxTimeoutMs() { if (timeoutMsById.isEmpty()) { return this.timeoutMs; @@ -220,29 +318,55 @@ private int getMaxTimeoutMs() { for (String responderId : timeoutMsById.keySet()) { // Use only what we're waiting for if (!responsesRemainingById.isEmpty() && responsesRemainingById.containsKey(responderId) - && responsesRemainingById.get(responderId) == 0) + && responsesRemainingById.get(responderId) == 0) { continue; + } maxTimeoutMs = Math.max(timeoutMsById.get(responderId), maxTimeoutMs); } return maxTimeoutMs; } - private Integer incResponsesRemaining(Integer inc) { + private synchronized void updateCounters(Message message, boolean isWithPayload) { + String id = message.getId(); + + /** + * Don't update remaining messages counter when a message id was already recorder so the current + * message is a redelivery of a previous one. + */ + if(!handledMessagesIds.contains(id)) { + if(isWithPayload) { + incResponsesRemaining(-1); + } + handledMessagesIds.add(message.getId()); + } + } + + private synchronized Integer incResponsesRemaining(Integer inc) { return (responsesRemaining = Math.max(responsesRemaining + inc, 0)); } - private int setResponsesRemainingForResponderId(String responderId, Integer responsesRemaining) { - boolean notChanged = (responsesRemainingById != null && responsesRemainingById.containsKey(responderId) && responsesRemainingById - .get(responderId).equals(responsesRemaining)); - if (notChanged) - return 0; + int getResponsesRemaining() { + if (responsesRemainingById.isEmpty()) { + return responsesRemaining; + } + + Integer sumOfResponsesRemaining = 0; + for (Integer responses : responsesRemainingById.values()) { + sumOfResponsesRemaining += responses; + } + + return Math.max(responsesRemaining, sumOfResponsesRemaining); + } + private Integer setResponsesRemainingForResponderId(String responderId, int responsesRemaining) { + //check for responsesRemaining < 0 seems redundant, since if config.waitForResponses == -1 we use Infinity boolean atMin = (responsesRemaining < 0 && (responsesRemainingById.isEmpty() || !responsesRemainingById .containsKey(responderId))); - if (atMin) - return 0; - + if (atMin) { + return null; + } + //when second, third, etc time same value (not equals 0) for responsesRemaining is received for corresponding responderId, it must be sum up with previous. if (responsesRemaining == 0) { responsesRemainingById.put(responderId, 0); } else { @@ -253,18 +377,30 @@ private int setResponsesRemainingForResponderId(String responderId, Integer resp return responsesRemainingById.get(responderId); } - private int getResponseTimeoutFromConfigs(RequestOptions requestOptions) { - if (requestOptions.getResponseTimeout() == null) { - return 3000; + public void waitForResponses() { + if(this.responseTimeoutFuture != null){ + this.responseTimeoutFuture.cancel(true); } - return requestOptions.getResponseTimeout(); + int newTimeoutMs = this.currentTimeoutMs - toIntExact(clock.instant().toEpochMilli() - this.startedAt); + LOG.debug("[correlation id: {}] Waiting for responses until {}.", requestMessage.getCorrelationId(), clock.instant().plus(newTimeoutMs, ChronoUnit.MILLIS)); + this.responseTimeoutFuture = timeoutManager.enableResponseTimeout(newTimeoutMs, this); } - private long getWaitForAckUntilFromConfigs(RequestOptions requestOptions) { - if (requestOptions.getAckTimeout() == null) { - return 0; + void waitForAcks() { + if (ackTimeoutFuture == null) { + LOG.debug("[correlation id: {}] Waiting for ack until {}.", requestMessage.getCorrelationId(), this.waitForAcksUntil); + long ackTimeoutMs = waitForAcksUntil.toEpochMilli() - clock.instant().toEpochMilli(); + ackTimeoutFuture = timeoutManager.enableAckTimeout(toIntExact(ackTimeoutMs), this); + } else { + LOG.debug("[correlation id: {}] Ack timeout is already scheduled", requestMessage.getCorrelationId()); + } + } + + private int getResponseTimeoutFromConfigs(RequestOptions requestOptions) { + if (requestOptions.getResponseTimeout() == null) { + return msbConfig.getDefaultResponseTimeout(); } - return this.startedAt + requestOptions.getAckTimeout(); + return requestOptions.getResponseTimeout(); } private void cancelResponseTimeoutTask() { @@ -290,4 +426,9 @@ List getPayloadMessages() { Message getRequestMessage() { return requestMessage; } + + @Override + public boolean forceDirectInvocation(){ + return directlyInvokable; + } } diff --git a/core/src/main/java/io/github/tcdl/msb/collector/CollectorManager.java b/core/src/main/java/io/github/tcdl/msb/collector/CollectorManager.java index cb3edadd..5599e5c4 100644 --- a/core/src/main/java/io/github/tcdl/msb/collector/CollectorManager.java +++ b/core/src/main/java/io/github/tcdl/msb/collector/CollectorManager.java @@ -2,25 +2,29 @@ import io.github.tcdl.msb.ChannelManager; import io.github.tcdl.msb.MessageHandler; +import io.github.tcdl.msb.MessageHandlerResolver; import io.github.tcdl.msb.api.exception.ConsumerSubscriptionException; import io.github.tcdl.msb.api.message.Message; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.util.Map; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * Manages instances of {@link Collector}s that listens to the same response topic. */ -public class CollectorManager implements MessageHandler { +public class CollectorManager implements MessageHandlerResolver { private static final Logger LOG = LoggerFactory.getLogger(CollectorManager.class); + private static final String LOGGING_NAME = "Collector manager"; - private boolean isSubscribed = false; + private volatile boolean isSubscribed = false; - private String topic; - private ChannelManager channelManager; + private final String topic; + private final ChannelManager channelManager; Map collectorsByCorrelationId = new ConcurrentHashMap<>(); public CollectorManager(String topic, ChannelManager channelManager) { @@ -29,18 +33,17 @@ public CollectorManager(String topic, ChannelManager channelManager) { } /** - * Determines correlationId from the incoming message and invokes the relevant {@link Collector} instance. + * Determines correlationId from the incoming message and resolves the relevant {@link Collector} instance. */ - @Override - public void handleMessage(Message message) { + @Override public Optional resolveMessageHandler(Message message) { String correlationId = message.getCorrelationId(); Collector collector = collectorsByCorrelationId.get(correlationId); if (collector != null) { - collector.handleMessage(message); + return Optional.of(collector); } else { LOG.warn("Message with correlationId {} is not expected to be processed by any Collectors", correlationId); + return Optional.empty(); } - } /** @@ -50,25 +53,25 @@ public void registerCollector(Collector collector) { String correlationId = collector.getRequestMessage().getCorrelationId(); collectorsByCorrelationId.putIfAbsent(correlationId, collector); - synchronized (this) { - if (!isSubscribed) { - channelManager.subscribe(topic, this); - isSubscribed = true; + if(!isSubscribed) { + synchronized (this) { + if (!isSubscribed) { + channelManager.subscribeForResponses(topic, this); + isSubscribed = true; + } } } } /** - * Remove this collector from collector's map, if it is present. If map is empty (no more collectors await on consumer topic) unsubscribe from consumer. + * Remove this collector from collector's map, if it is present. */ public void unregisterCollector(Collector collector) { collectorsByCorrelationId.remove(collector.getRequestMessage().getCorrelationId()); + } - synchronized (this) { - if (collectorsByCorrelationId.isEmpty() && isSubscribed) { - channelManager.unsubscribe(topic); - isSubscribed = false; - } - } + @Override + public String getLoggingName() { + return LOGGING_NAME; } } diff --git a/core/src/main/java/io/github/tcdl/msb/collector/ConsumedMessagesAwareMessageHandler.java b/core/src/main/java/io/github/tcdl/msb/collector/ConsumedMessagesAwareMessageHandler.java new file mode 100644 index 00000000..ed49fb8b --- /dev/null +++ b/core/src/main/java/io/github/tcdl/msb/collector/ConsumedMessagesAwareMessageHandler.java @@ -0,0 +1,23 @@ +package io.github.tcdl.msb.collector; + +import io.github.tcdl.msb.MessageHandler; +import io.github.tcdl.msb.api.AcknowledgementHandler; +import io.github.tcdl.msb.api.message.Message; + +/** + * Interface used to notify {@link io.github.tcdl.msb.MessageHandler} regarding a count + * of expected {@link io.github.tcdl.msb.MessageHandler#handleMessage(Message, AcknowledgementHandler)} invocations. + */ +public interface ConsumedMessagesAwareMessageHandler extends MessageHandler { + /** + * Should be invoked when an incoming message has been consumed so {@link io.github.tcdl.msb.MessageHandler#handleMessage(Message, AcknowledgementHandler)} + * will be invoked to process it in future. + */ + void notifyMessageConsumed(); + + /** + * Should be invoked when an incoming message that was consumed previously has been lost so {@link io.github.tcdl.msb.MessageHandler#handleMessage(Message, AcknowledgementHandler)} + * invocation is no longer expected. + */ + void notifyConsumedMessageIsLost(); +} diff --git a/core/src/main/java/io/github/tcdl/msb/collector/ExecutionOptionsAwareMessageHandler.java b/core/src/main/java/io/github/tcdl/msb/collector/ExecutionOptionsAwareMessageHandler.java new file mode 100644 index 00000000..5b97addc --- /dev/null +++ b/core/src/main/java/io/github/tcdl/msb/collector/ExecutionOptionsAwareMessageHandler.java @@ -0,0 +1,16 @@ +package io.github.tcdl.msb.collector; + +import io.github.tcdl.msb.MessageHandler; + +/** + * Created by Alexandr Zolotov + * 19.05.16 + */ +public interface ExecutionOptionsAwareMessageHandler extends MessageHandler { + + /** + * Indicates whether handler should be executed by main message handling thread (true if so) or by thread from + * consumer thread pool. + */ + boolean forceDirectInvocation(); +} \ No newline at end of file diff --git a/core/src/main/java/io/github/tcdl/msb/collector/TimeoutManager.java b/core/src/main/java/io/github/tcdl/msb/collector/TimeoutManager.java index 5e107e35..4d1921ed 100644 --- a/core/src/main/java/io/github/tcdl/msb/collector/TimeoutManager.java +++ b/core/src/main/java/io/github/tcdl/msb/collector/TimeoutManager.java @@ -23,39 +23,44 @@ public TimeoutManager(int threadPoolSize) { } protected ScheduledFuture enableResponseTimeout(int timeoutMs, Collector collector) { - LOG.debug("Enabling response timeout for {} ms", timeoutMs); + LOG.debug("[correlation id: {}] Enabling response timeout for {} ms", collector.getRequestMessage().getCorrelationId(), timeoutMs); + + if (timeoutMs <= 0) { + LOG.debug("[correlation id: {}] Unable to schedule timeout with negative delay : {}", collector.getRequestMessage().getCorrelationId(), timeoutMs); + return null; + } try { return timeoutExecutorDecorator.schedule(() -> { - LOG.debug("Response timeout expired."); + LOG.debug("[correlation id: {}] Response timeout expired.", collector.getRequestMessage().getCorrelationId()); collector.end(); }, timeoutMs, TimeUnit.MILLISECONDS); } catch (RejectedExecutionException e) { - LOG.warn("Unable to schedule task for execution", e); + LOG.warn("[correlation id: {}] Unable to schedule task for execution", collector.getRequestMessage().getCorrelationId(), e); return null; } } - protected ScheduledFuture enableAckTimeout(long timeoutMs, Collector collector) { - LOG.debug("Enabling ack timeout for {} ms", timeoutMs); + protected ScheduledFuture enableAckTimeout(int timeoutMs, Collector collector) { + LOG.debug("[correlation id: {}] Enabling ack timeout for {} ms", collector.getRequestMessage().getCorrelationId(), timeoutMs); if (timeoutMs <= 0) { - LOG.debug("Unable to schedule timeout with negative delay : {}", timeoutMs); + LOG.debug("[correlation id: {}] Unable to schedule timeout with negative delay : {}", collector.getRequestMessage().getCorrelationId(), timeoutMs); return null; } try { return timeoutExecutorDecorator.schedule(() -> { if (collector.isAwaitingResponses()) { - LOG.debug("Ack timeout expired, but waiting for responses."); + LOG.debug("[correlation id: {}] Ack timeout expired, but waiting for responses.", collector.getRequestMessage().getCorrelationId()); return; } - LOG.debug("Ack timeout expired."); + LOG.debug("[correlation id: {}] Ack timeout expired.", collector.getRequestMessage().getCorrelationId()); collector.end(); }, timeoutMs, TimeUnit.MILLISECONDS); } catch (RejectedExecutionException e) { - LOG.warn("Unable to schedule task for execution", e); + LOG.warn("[correlation id: {}] Unable to schedule task for execution", collector.getRequestMessage().getCorrelationId(), e); return null; } } diff --git a/core/src/main/java/io/github/tcdl/msb/config/MsbConfig.java b/core/src/main/java/io/github/tcdl/msb/config/MsbConfig.java index 9df9d469..12dccdbc 100644 --- a/core/src/main/java/io/github/tcdl/msb/config/MsbConfig.java +++ b/core/src/main/java/io/github/tcdl/msb/config/MsbConfig.java @@ -8,9 +8,7 @@ import java.io.IOException; -import static io.github.tcdl.msb.config.ConfigurationUtil.getBoolean; -import static io.github.tcdl.msb.config.ConfigurationUtil.getInt; -import static io.github.tcdl.msb.config.ConfigurationUtil.getString; +import static io.github.tcdl.msb.config.ConfigurationUtil.*; /** * {@link MsbConfig} class provides access to configuration properties. @@ -20,18 +18,32 @@ public class MsbConfig { public final Logger LOG = LoggerFactory.getLogger(getClass()); //Broker Adapter Factory class. Represented with brokerAdapterFactory property from config - private String brokerAdapterFactoryClass; + private final String brokerAdapterFactoryClass; //Broker specific configuration. - private Config brokerConfig; + private final Config brokerConfig; private final ServiceDetails serviceDetails; - private String schema; + private final String schema; - private boolean validateMessage; + private final boolean validateMessage; - private int timerThreadPoolSize; + private final int timerThreadPoolSize; + + private final boolean mdcLogging; + + private final String mdcLoggingKeyMessageTags; + + private final String mdcLoggingKeyCorrelationId; + + private final String mdcLoggingSplitTagsBy; + + private final int consumerThreadPoolSize; + + private final int consumerThreadPoolQueueCapacity; + + private final int defaultResponseTimeout; public MsbConfig(Config loadedConfig) { Config config = loadedConfig.getConfig("msbConfig"); @@ -40,18 +52,33 @@ public MsbConfig(Config loadedConfig) { this.serviceDetails = new ServiceDetails.Builder(serviceDetailsConfig).build(); this.schema = readJsonSchema(); this.brokerAdapterFactoryClass = getBrokerAdapterFactory(config); + this.brokerConfig = config.hasPath("brokerConfig") ? config.getConfig("brokerConfig") : ConfigFactory.empty(); this.timerThreadPoolSize = getInt(config, "timerThreadPoolSize"); this.validateMessage = getBoolean(config, "validateMessage"); - LOG.debug("MSB configuration {}", this); + this.consumerThreadPoolSize = config.getInt("threadingConfig.consumerThreadPoolSize"); + this.consumerThreadPoolQueueCapacity = config.getInt("threadingConfig.consumerThreadPoolQueueCapacity"); + + Config mdcLogging = config.getConfig("mdcLogging"); + Config mdcLoggingMessageKeys= mdcLogging.getConfig("messageKeys"); + + this.mdcLogging = getBoolean(mdcLogging, "enabled"); + this.mdcLoggingSplitTagsBy = getOptionalString(mdcLogging, "splitTagsBy").orElse(null); + this.mdcLoggingKeyMessageTags = getString(mdcLoggingMessageKeys, "messageTags"); + this.mdcLoggingKeyCorrelationId = getString(mdcLoggingMessageKeys, "correlationId"); + + Config requestOptionsConfig = config.getConfig("requestOptions"); + this.defaultResponseTimeout = getInt(requestOptionsConfig, "responseTimeout"); + + LOG.debug("Loaded {}", this); } private String readJsonSchema() { try { - return IOUtils.toString(getClass().getResourceAsStream("/schema.js")); + return IOUtils.toString(getClass().getResourceAsStream("/schema.json")); } catch (IOException e) { - LOG.error("MSB configuration failed to load Json validation schema", this); + LOG.error("Failed to load Json validation schema", this); return null; } } @@ -84,9 +111,49 @@ public int getTimerThreadPoolSize() { return timerThreadPoolSize; } - @Override - public String toString() { - return String.format("MsbConfig [serviceDetails=%s, schema=%s, validateMessage=%b, timerThreadPoolSize=%d, brokerAdapterFactory=%s, brokerConfig=%s]", serviceDetails, schema, validateMessage, timerThreadPoolSize, brokerAdapterFactoryClass, brokerConfig); + public boolean isMdcLogging() { + return mdcLogging; } + public String getMdcLoggingKeyMessageTags() { + return mdcLoggingKeyMessageTags; + } + + public String getMdcLoggingKeyCorrelationId() { + return mdcLoggingKeyCorrelationId; + } + + public String getMdcLoggingSplitTagsBy() { + return mdcLoggingSplitTagsBy; + } + + public int getDefaultResponseTimeout() { + return defaultResponseTimeout; + } + + @Override public String toString() { + //please keep custom "brokerConfig" when using auto-generation of this method + return "MsbConfig{" + + "brokerAdapterFactoryClass='" + brokerAdapterFactoryClass + '\'' + + ", serviceDetails=" + serviceDetails + + ", schema='" + schema + '\'' + + ", validateMessage=" + validateMessage + + ", timerThreadPoolSize=" + timerThreadPoolSize + + ", mdcLogging=" + mdcLogging + + ", mdcLoggingKeyMessageTags='" + mdcLoggingKeyMessageTags + '\'' + + ", mdcLoggingKeyCorrelationId='" + mdcLoggingKeyCorrelationId + '\'' + + ", mdcLoggingSplitTagsBy='" + mdcLoggingSplitTagsBy + '\'' + + ", consumerThreadPoolSize=" + consumerThreadPoolSize + + ", consumerThreadPoolQueueCapacity=" + consumerThreadPoolQueueCapacity + + ", brokerConfig='" + brokerConfig.root().render() + '\'' + + '}'; + } + + public int getConsumerThreadPoolSize() { + return consumerThreadPoolSize; + } + + public int getConsumerThreadPoolQueueCapacity() { + return consumerThreadPoolQueueCapacity; + } } diff --git a/core/src/main/java/io/github/tcdl/msb/config/ServiceDetails.java b/core/src/main/java/io/github/tcdl/msb/config/ServiceDetails.java index 96dca93d..e96ef954 100644 --- a/core/src/main/java/io/github/tcdl/msb/config/ServiceDetails.java +++ b/core/src/main/java/io/github/tcdl/msb/config/ServiceDetails.java @@ -1,20 +1,20 @@ package io.github.tcdl.msb.config; -import static io.github.tcdl.msb.config.ConfigurationUtil.getString; import static io.github.tcdl.msb.config.ConfigurationUtil.getOptionalString; +import static io.github.tcdl.msb.config.ConfigurationUtil.getString; import io.github.tcdl.msb.support.Utils; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.typesafe.config.Config; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - /** * Class contains configuration data related to service instance. */ @@ -52,11 +52,14 @@ public static class Builder { public Builder(Config config) { name = getString(config, "name"); version = getString(config, "version"); - Optional optionalInstanceId = getOptionalString(config, "instanceId"); + + Optional optionalInstanceId = getOptionalString(config, "instanceId"); instanceId = optionalInstanceId.isPresent() ? optionalInstanceId.get() : Utils.generateId(); - hostname = getHostInfo().getHostName(); - ip = getHostInfo().getHostAddress(); + Optional optionalHostInfo = getHostInfo(); + hostname = optionalHostInfo.isPresent() ? optionalHostInfo.get().getHostName() : "unknown"; + ip = optionalHostInfo.isPresent() ? optionalHostInfo.get().getHostAddress() : null; + pid = getPID(); } @@ -67,19 +70,27 @@ public ServiceDetails build() { return new ServiceDetails(name, version, instanceId, hostname, ip, pid); } - private static InetAddress getHostInfo() { - InetAddress hostInfo = null; + protected Optional getHostInfo() { + Optional optionalHostInfo; try { - hostInfo = InetAddress.getLocalHost(); + optionalHostInfo = Optional.of(InetAddress.getLocalHost()); } catch (UnknownHostException ex) { - LOG.error("Fail to retrieve host info", ex); + LOG.warn("Fail to retrieve host info", ex); + optionalHostInfo = Optional.empty(); } - return hostInfo; + return optionalHostInfo; } - private static long getPID() { + protected long getPID() { String processName = java.lang.management.ManagementFactory.getRuntimeMXBean().getName(); - return Long.parseLong(processName.split("@")[0]); + long pid; + try { + pid = Long.parseLong(processName.split("@")[0]); + } catch (NumberFormatException ex) { + LOG.warn("Fail to get Process ID", ex); + pid = 0; + } + return pid; } } @@ -110,7 +121,7 @@ public long getPid() { @Override public String toString() { return "ServiceDetails [name=" + name + ", version=" + version + ", instanceId=" + instanceId + ", hostname=" - + hostname + ", ip=" + ip + ", pid=" + pid + "]"; + + hostname + String.valueOf(ip != null ? ", ip=" + ip : "") + ", pid=" + pid + "]"; } } \ No newline at end of file diff --git a/core/src/main/java/io/github/tcdl/msb/events/EventHandlers.java b/core/src/main/java/io/github/tcdl/msb/events/EventHandlers.java index 1bd4ff7b..3d249047 100644 --- a/core/src/main/java/io/github/tcdl/msb/events/EventHandlers.java +++ b/core/src/main/java/io/github/tcdl/msb/events/EventHandlers.java @@ -1,26 +1,30 @@ package io.github.tcdl.msb.events; import io.github.tcdl.msb.api.Callback; +import io.github.tcdl.msb.api.MessageContext; import io.github.tcdl.msb.api.Requester; import io.github.tcdl.msb.api.message.Acknowledge; import io.github.tcdl.msb.api.message.Message; +import java.util.function.BiConsumer; + /** * {@link EventHandlers} is a component that allows to register custom event handlers for {@link Requester} specific events. */ public class EventHandlers { - private Callback onAcknowledge = acknowledge -> {}; - private Callback onResponse = response -> {}; - private Callback onRawResponse = response -> {}; + private BiConsumer onAcknowledge = (acknowledge, msgContext) -> {}; + private BiConsumer onResponse = (acknowledge, msgContext) -> {}; + private BiConsumer onRawResponse = (acknowledge, msgContext) -> {}; private Callback onEnd = messages -> {}; + private BiConsumer onError; /** * Return callback registered for Acknowledge event. * * @return acknowledge callback */ - public Callback onAcknowledge() { + public BiConsumer onAcknowledge() { return onAcknowledge; } @@ -28,8 +32,9 @@ public Callback onAcknowledge() { * Registered callback for Acknowledge event. * * @param onAcknowledge callback + * @return EventHandlers */ - public EventHandlers onAcknowledge(Callback onAcknowledge) { + public EventHandlers onAcknowledge(BiConsumer onAcknowledge) { this.onAcknowledge = onAcknowledge; return this; } @@ -38,8 +43,9 @@ public EventHandlers onAcknowledge(Callback onAcknowledge) { * Return callback registered for Response event. * * @return response callback + * @return EventHandlers */ - public Callback onResponse() { + public BiConsumer onResponse() { return onResponse; } @@ -47,8 +53,9 @@ public Callback onResponse() { * Registered callback for Response event. * * @param onResponse callback + * @return EventHandlers */ - public EventHandlers onResponse(Callback onResponse) { + public EventHandlers onResponse(BiConsumer onResponse) { this.onResponse = onResponse; return this; } @@ -57,8 +64,9 @@ public EventHandlers onResponse(Callback onResponse) { * Return callback registered for Response event. * * @return response callback + * @return EventHandlers */ - public Callback onRawResponse() { + public BiConsumer onRawResponse() { return onRawResponse; } @@ -66,8 +74,9 @@ public Callback onRawResponse() { * Registered callback for Response event. * * @param onRawResponse callback + * @return EventHandlers */ - public EventHandlers onRawResponse(Callback onRawResponse) { + public EventHandlers onRawResponse(BiConsumer onRawResponse) { this.onRawResponse = onRawResponse; return this; } @@ -85,9 +94,31 @@ public Callback onEnd() { * Registered callback for End event. * * @param onEnd callback + * @return EventHandlers */ public EventHandlers onEnd(Callback onEnd) { this.onEnd = onEnd; return this; } + + /** + * Registered callback for Error event. + * + * @param onError callback + * @return EventHandlers + */ + public EventHandlers onError(BiConsumer onError) { + this.onError = onError; + return this; + } + + /** + * Return callback registered for Error event. + * + * @return error callback + */ + public BiConsumer onError() { + return onError; + } + } diff --git a/core/src/main/java/io/github/tcdl/msb/impl/MessageContextImpl.java b/core/src/main/java/io/github/tcdl/msb/impl/MessageContextImpl.java new file mode 100644 index 00000000..ef58414a --- /dev/null +++ b/core/src/main/java/io/github/tcdl/msb/impl/MessageContextImpl.java @@ -0,0 +1,27 @@ +package io.github.tcdl.msb.impl; + +import io.github.tcdl.msb.api.AcknowledgementHandler; +import io.github.tcdl.msb.api.MessageContext; +import io.github.tcdl.msb.api.message.Message; + +public class MessageContextImpl implements MessageContext { + private final Message originalMessage; + private final AcknowledgementHandler acknowledgementHandler; + + public MessageContextImpl(AcknowledgementHandler acknowledgementHandler, Message originalMessage) { + super(); + this.acknowledgementHandler = acknowledgementHandler; + this.originalMessage = originalMessage; + } + + @Override + public AcknowledgementHandler getAcknowledgementHandler() { + return acknowledgementHandler; + } + + @Override + public Message getOriginalMessage() { + return originalMessage; + } + +} diff --git a/core/src/main/java/io/github/tcdl/msb/impl/MsbContextImpl.java b/core/src/main/java/io/github/tcdl/msb/impl/MsbContextImpl.java index b7c102fc..060e37a3 100644 --- a/core/src/main/java/io/github/tcdl/msb/impl/MsbContextImpl.java +++ b/core/src/main/java/io/github/tcdl/msb/impl/MsbContextImpl.java @@ -4,6 +4,7 @@ import io.github.tcdl.msb.ChannelManager; import io.github.tcdl.msb.api.MsbContext; import io.github.tcdl.msb.api.ObjectFactory; +import io.github.tcdl.msb.callback.MutableCallbackHandler; import io.github.tcdl.msb.collector.CollectorManager; import io.github.tcdl.msb.collector.CollectorManagerFactory; import io.github.tcdl.msb.collector.TimeoutManager; @@ -11,7 +12,6 @@ import io.github.tcdl.msb.message.MessageFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import java.time.Clock; /** @@ -21,20 +21,23 @@ public class MsbContextImpl implements MsbContext { private static final Logger LOG = LoggerFactory.getLogger(MsbContextImpl.class); - private MsbConfig msbConfig; - private ObjectFactory objectFactory; - private MessageFactory messageFactory; - private ChannelManager channelManager; - private Clock clock; - private TimeoutManager timeoutManager; - private ObjectMapper payloadMapper; - private CollectorManagerFactory collectorManagerFactory; + private final MsbConfig msbConfig; + private volatile ObjectFactory objectFactory; + private final MessageFactory messageFactory; + private final ChannelManager channelManager; + private final Clock clock; + private final TimeoutManager timeoutManager; + private final ObjectMapper payloadMapper; + private final CollectorManagerFactory collectorManagerFactory; + private final MutableCallbackHandler shutdownCallbackHandler; + private volatile boolean isShutdownComplete = false; public MsbContextImpl(MsbConfig msbConfig, MessageFactory messageFactory, ChannelManager channelManager, Clock clock, TimeoutManager timeoutManager, ObjectMapper payloadMapper, - CollectorManagerFactory collectorManagerFactory) { + CollectorManagerFactory collectorManagerFactory, + MutableCallbackHandler shutdownCallbackHandler) { this.msbConfig = msbConfig; this.messageFactory = messageFactory; this.channelManager = channelManager; @@ -42,18 +45,24 @@ public MsbContextImpl(MsbConfig msbConfig, MessageFactory messageFactory, Channe this.timeoutManager = timeoutManager; this.payloadMapper = payloadMapper; this.collectorManagerFactory = collectorManagerFactory; + this.shutdownCallbackHandler = shutdownCallbackHandler; } /** * {@inheritDoc} */ @Override - public void shutdown() { - LOG.info("Shutting down MSB context..."); - objectFactory.shutdown(); - timeoutManager.shutdown(); - channelManager.shutdown(); - LOG.info("MSB context has been shut down."); + public synchronized void shutdown() { + if(!isShutdownComplete) { + isShutdownComplete = true; + LOG.info("Shutting down MSB context..."); + shutdownCallbackHandler.runCallbacks(); + timeoutManager.shutdown(); + channelManager.shutdown(); + LOG.info("MSB context has been shut down."); + } else { + LOG.warn("Trying to shutdown MsbContext several times"); + } } /** @@ -117,4 +126,8 @@ public void setObjectFactory(ObjectFactory objectFactory) { this.objectFactory = objectFactory; } + @Override + public void addShutdownCallback(Runnable shutdownCallback) { + shutdownCallbackHandler.add(shutdownCallback); + } } diff --git a/core/src/main/java/io/github/tcdl/msb/impl/MsbThreadContext.java b/core/src/main/java/io/github/tcdl/msb/impl/MsbThreadContext.java new file mode 100644 index 00000000..2436f30f --- /dev/null +++ b/core/src/main/java/io/github/tcdl/msb/impl/MsbThreadContext.java @@ -0,0 +1,60 @@ +package io.github.tcdl.msb.impl; + + +import io.github.tcdl.msb.api.MessageContext; + +import java.util.HashMap; +import java.util.Map; + +/** + * Gives access to initial message without polluting client classes APIs MsbThreadContext is wrapper around {@link ThreadLocal}. Additional care has to be taken + * if any kind of multithreaded message processing takes place. + */ +public class MsbThreadContext { + + private static final ThreadLocal messageContext = new ThreadLocal<>(); + private static final ThreadLocal request = new ThreadLocal<>(); + private static final ThreadLocal> map = new ThreadLocal>(){ + @Override + protected Map initialValue() { + return new HashMap<>(); + } + }; + + public static MessageContext getMessageContext() { + return messageContext.get(); + } + + public static void setMessageContext(MessageContext messageContext) { + MsbThreadContext.messageContext.set(messageContext); + } + + public static Object getRequest() { + return request.get(); + } + + public static void setRequest(Object request) { + MsbThreadContext.request.set(request); + } + + + public static Map getMap() { + return map.get(); + } + + public static void put(String key, Object value){ + if(key == null) { + throw new IllegalArgumentException("key cannot be null"); + } + + map.get().put(key, value); + } + + + static void clear() { + messageContext.remove(); + request.remove(); + map.remove(); + } + +} diff --git a/core/src/main/java/io/github/tcdl/msb/impl/NoopResponderImpl.java b/core/src/main/java/io/github/tcdl/msb/impl/NoopResponderImpl.java index 6c283377..8eda64bc 100644 --- a/core/src/main/java/io/github/tcdl/msb/impl/NoopResponderImpl.java +++ b/core/src/main/java/io/github/tcdl/msb/impl/NoopResponderImpl.java @@ -20,18 +20,17 @@ public NoopResponderImpl(Message originalMessage) { /** {@inheritDoc} */ @Override public void sendAck(Integer timeoutMs, Integer responsesRemaining) { - LOG.error("Cannot send ack because response topic is unknown. Incoming message: {}", originalMessage); + LOG.error("[correlation id: {}, message id: {}] Cannot send ack because response topic is unknown.", + originalMessage.getCorrelationId(), originalMessage.getId()); + LOG.trace("Incoming message: {}", originalMessage); } /** {@inheritDoc} */ @Override public void send(Object responsePayload) { - LOG.error("Cannot send response because response topic is unknown. Incoming message: {}", originalMessage); + LOG.error("[correlation id: {}, message id: {}] Cannot send response because response topic is unknown.", + originalMessage.getCorrelationId(), originalMessage.getId()); + LOG.trace("Incoming message: {}", originalMessage); } - /** {@inheritDoc} */ - @Override - public Message getOriginalMessage() { - return originalMessage; - } } diff --git a/core/src/main/java/io/github/tcdl/msb/impl/ObjectFactoryImpl.java b/core/src/main/java/io/github/tcdl/msb/impl/ObjectFactoryImpl.java index d11d1f7c..dfeed4de 100644 --- a/core/src/main/java/io/github/tcdl/msb/impl/ObjectFactoryImpl.java +++ b/core/src/main/java/io/github/tcdl/msb/impl/ObjectFactoryImpl.java @@ -1,33 +1,18 @@ package io.github.tcdl.msb.impl; import com.fasterxml.jackson.core.type.TypeReference; -import io.github.tcdl.msb.api.Callback; -import io.github.tcdl.msb.api.MessageTemplate; -import io.github.tcdl.msb.api.ObjectFactory; -import io.github.tcdl.msb.api.PayloadConverter; -import io.github.tcdl.msb.api.RequestOptions; -import io.github.tcdl.msb.api.Requester; -import io.github.tcdl.msb.api.ResponderServer; -import io.github.tcdl.msb.api.monitor.AggregatorStats; -import io.github.tcdl.msb.api.monitor.ChannelMonitorAggregator; -import io.github.tcdl.msb.monitor.aggregator.DefaultChannelMonitorAggregator; -import org.apache.commons.lang3.concurrent.BasicThreadFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import io.github.tcdl.msb.api.*; +import org.apache.commons.lang3.Validate; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.ThreadFactory; +import java.lang.reflect.Type; /** * Provides methods for creation {@link Requester} and {@link ResponderServer}. */ public class ObjectFactoryImpl implements ObjectFactory { - private static Logger LOG = LoggerFactory.getLogger(ObjectFactoryImpl.class); private MsbContextImpl msbContext; private PayloadConverter payloadConverter; - private ChannelMonitorAggregator channelMonitorAggregator; public ObjectFactoryImpl(MsbContextImpl msbContext) { super(); @@ -44,47 +29,55 @@ public Requester createRequester(String namespace, RequestOptions request * {@inheritDoc} */ @Override - public ResponderServer createResponderServer(String namespace, MessageTemplate messageTemplate, - ResponderServer.RequestHandler requestHandler, TypeReference payloadTypeReference) { - return ResponderServerImpl.create(namespace, messageTemplate, msbContext, requestHandler, payloadTypeReference); + public Requester createRequesterForSingleResponse(String namespace, Class payloadClass, RequestOptions baseRequestOptions) { + Validate.notNull(baseRequestOptions); + RequestOptions singleResponseRequestOptions = baseRequestOptions + .asBuilder() + .withWaitForResponses(1) + .build(); + + return RequesterImpl.create(namespace, singleResponseRequestOptions, msbContext, toTypeReference(payloadClass)); } /** * {@inheritDoc} */ @Override - public PayloadConverter getPayloadConverter() { - return payloadConverter; + public ResponderServer createResponderServer(String namespace, ResponderOptions responderOptions, + ResponderServer.RequestHandler requestHandler, ResponderServer.ErrorHandler errorHandler, TypeReference payloadTypeReference) { + return ResponderServerImpl.create(namespace, responderOptions, msbContext, requestHandler, errorHandler, payloadTypeReference); } /** * {@inheritDoc} */ @Override - public synchronized ChannelMonitorAggregator createChannelMonitorAggregator(Callback aggregatorStatsHandler) { - ThreadFactory threadFactory = new BasicThreadFactory.Builder() - .namingPattern("monitor-aggregator-heartbeat-thread-%d") - .daemon(true) + public Requester createRequesterForFireAndForget(String namespace, RequestOptions requestOptions) { + Validate.notNull(requestOptions, "RequestOptions are mandatory"); + + RequestOptions fireAndForgetRequestOptions = requestOptions + .asBuilder() + .withAckTimeout(0) + .withWaitForResponses(0) .build(); - ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(1, threadFactory); - channelMonitorAggregator = createDefaultChannelMonitorAggregator(aggregatorStatsHandler, scheduledExecutorService); - return channelMonitorAggregator; + return RequesterImpl.create(namespace, fireAndForgetRequestOptions, msbContext, null); } /** * {@inheritDoc} */ @Override - public synchronized void shutdown() { - LOG.info("Shutting down..."); - if (channelMonitorAggregator != null) { - channelMonitorAggregator.stop(); - } - LOG.info("Shutdown complete"); + public PayloadConverter getPayloadConverter() { + return payloadConverter; } - DefaultChannelMonitorAggregator createDefaultChannelMonitorAggregator(Callback aggregatorStatsHandler, ScheduledExecutorService scheduledExecutorService) { - return new DefaultChannelMonitorAggregator(msbContext, scheduledExecutorService, aggregatorStatsHandler); + private static TypeReference toTypeReference(Class clazz) { + return new TypeReference() { + @Override + public Type getType() { + return clazz; + } + }; } } diff --git a/core/src/main/java/io/github/tcdl/msb/impl/RequesterImpl.java b/core/src/main/java/io/github/tcdl/msb/impl/RequesterImpl.java index ecb932d8..88520e63 100644 --- a/core/src/main/java/io/github/tcdl/msb/impl/RequesterImpl.java +++ b/core/src/main/java/io/github/tcdl/msb/impl/RequesterImpl.java @@ -2,20 +2,23 @@ import com.fasterxml.jackson.core.type.TypeReference; import io.github.tcdl.msb.ChannelManager; -import io.github.tcdl.msb.api.Callback; -import io.github.tcdl.msb.api.MessageTemplate; -import io.github.tcdl.msb.api.RequestOptions; -import io.github.tcdl.msb.api.Requester; +import io.github.tcdl.msb.api.*; import io.github.tcdl.msb.api.message.Acknowledge; import io.github.tcdl.msb.api.message.Message; import io.github.tcdl.msb.collector.Collector; import io.github.tcdl.msb.events.EventHandlers; import io.github.tcdl.msb.message.MessageFactory; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; +import java.util.function.BiConsumer; + /** * Implementation of {@link Requester} - * + * * Expected responses are matched by correlationId from original request. * * @see Requester @@ -61,15 +64,15 @@ private RequesterImpl(String namespace, RequestOptions requestOptions, MsbContex */ @Override public void publish(Object requestPayload) { - publish(requestPayload, null, null); + publish(requestPayload, null, ArrayUtils.EMPTY_STRING_ARRAY); } /** * {@inheritDoc} */ @Override - public void publish(Object requestPayload, String tag) { - publish(requestPayload, null, tag); + public void publish(Object requestPayload, String... tags) { + publish(requestPayload, null, tags); } /** @@ -77,43 +80,118 @@ public void publish(Object requestPayload, String tag) { */ @Override public void publish(Object requestPayload, Message originalMessage) { - publish(requestPayload, originalMessage, null); + publish(requestPayload, originalMessage, ArrayUtils.EMPTY_STRING_ARRAY); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture request(Object requestPayload) { + return request(requestPayload, null, ArrayUtils.EMPTY_STRING_ARRAY); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture request(Object requestPayload, String... tags) { + return request(requestPayload, null, tags); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture request(Object requestPayload, Message originalMessage) { + return request(requestPayload, originalMessage, ArrayUtils.EMPTY_STRING_ARRAY); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture request(Object requestPayload, Message originalMessage, String... tags) { + this.eventHandlers = new EventHandlers<>(); //discard all previously set handlers + + CompletableFuture futureResult = new CompletableFuture<>(); + + this.onResponse((response, messageContext) -> futureResult.complete(response)) + .onAcknowledge((acknowledge, messageContext) -> { + boolean noResponse = !futureResult.isDone() && acknowledge.getResponsesRemaining() < 1; + boolean tooManyResponses = acknowledge.getResponsesRemaining() > 1; + if (noResponse || tooManyResponses) { + futureResult.cancel(true); + } + }) + .onEnd(end -> { + if (!futureResult.isDone()) { + futureResult.cancel(true); + } + }) + .onError((exception, message) -> futureResult.cancel(true)); + + publish(true, requestPayload, originalMessage, tags); + return futureResult; } /** * {@inheritDoc} */ @Override - public void publish(Object requestPayload, Message originalMessage, String tag) { + public void publish(Object requestPayload, Message originalMessage, String... tags) { + publish(false, requestPayload, originalMessage, tags); + } + + private void publish(boolean invokeHandlersDirectly, Object requestPayload, Message originalMessage, String... tags) { MessageTemplate messageTemplate = MessageTemplate.copyOf(requestOptions.getMessageTemplate()); - if (tag != null) { - messageTemplate.addTag(tag); + + if (tags != null) { + Arrays.stream(tags).filter(tag -> tag != null).forEach(messageTemplate::addTag); } - Message.Builder messageBuilder = messageFactory.createRequestMessageBuilder(namespace, messageTemplate, originalMessage); + + Message.Builder messageBuilder = messageFactory.createRequestMessageBuilder( + namespace, + requestOptions.getForwardNamespace(), + requestOptions.getRoutingKey(), + messageTemplate, + originalMessage); + Message message = messageFactory.createRequestMessage(messageBuilder, requestPayload); - //use Collector instance to handle expected responses/acks - if (requestOptions.isWaitForResponses()) { - String topic = message.getTopics().getResponse(); + boolean fireAndForget = !(isWaitForAckMs() || isWaitForResponses()); + boolean forwardingRequired = StringUtils.isNotBlank(requestOptions.getForwardNamespace()); - Collector collector = createCollector(topic, message, requestOptions, context, eventHandlers); + if(forwardingRequired || fireAndForget){ + publishMessage(message); + } else { + //set up collector for responses or acks + Collector collector = createCollector(message, requestOptions, context, eventHandlers, invokeHandlersDirectly); collector.listenForResponses(); - getChannelManager().findOrCreateProducer(message.getTopics().getTo()) - .publish(message); + publishMessage(message); collector.waitForResponses(); - } else { - getChannelManager().findOrCreateProducer(message.getTopics().getTo()) - .publish(message); } } + private void publishMessage(Message message) { + getChannelManager().findOrCreateProducer(message.getTopics().getTo(), false, requestOptions).publish(message); + } + + private boolean isWaitForAckMs() { + return requestOptions.getAckTimeout() != null && requestOptions.getAckTimeout() != 0; + } + + private boolean isWaitForResponses() { + return requestOptions.getWaitForResponses() != 0; + } + /** * {@inheritDoc} */ @Override - public Requester onAcknowledge(Callback acknowledgeHandler) { + public Requester onAcknowledge(BiConsumer acknowledgeHandler) { eventHandlers.onAcknowledge(acknowledgeHandler); return this; } @@ -122,7 +200,7 @@ public Requester onAcknowledge(Callback acknowledgeHandler) { * {@inheritDoc} */ @Override - public Requester onResponse(Callback responseHandler) { + public Requester onResponse(BiConsumer responseHandler) { eventHandlers.onResponse(responseHandler); return this; } @@ -130,7 +208,7 @@ public Requester onResponse(Callback responseHandler) { /** * {@inheritDoc} */ - @Override public Requester onRawResponse(Callback responseHandler) { + @Override public Requester onRawResponse(BiConsumer responseHandler) { eventHandlers.onRawResponse(responseHandler); return this; } @@ -144,11 +222,25 @@ public Requester onEnd(Callback endHandler) { return this; } + /** + * {@inheritDoc} + */ + @Override + public Requester onError(BiConsumer errorHandler) { + eventHandlers.onError(errorHandler); + return this; + } + private ChannelManager getChannelManager() { return context.getChannelManager(); } - Collector createCollector(String topic, Message requestMessage, RequestOptions requestOptions, MsbContextImpl context, EventHandlers eventHandlers) { - return new Collector<>(topic, requestMessage, requestOptions, context, eventHandlers, payloadTypeReference); + Collector createCollector(Message requestMessage, + RequestOptions requestOptions, + MsbContextImpl context, + EventHandlers eventHandlers, + boolean invokeHandlersDirectly) { + return new Collector<>(requestMessage.getTopics().getResponse(), requestMessage, requestOptions, context, + eventHandlers, payloadTypeReference, invokeHandlersDirectly); } } diff --git a/core/src/main/java/io/github/tcdl/msb/impl/ResponderContextImpl.java b/core/src/main/java/io/github/tcdl/msb/impl/ResponderContextImpl.java new file mode 100644 index 00000000..6967fbb3 --- /dev/null +++ b/core/src/main/java/io/github/tcdl/msb/impl/ResponderContextImpl.java @@ -0,0 +1,50 @@ +package io.github.tcdl.msb.impl; + +import io.github.tcdl.msb.api.AcknowledgementHandler; +import io.github.tcdl.msb.api.Responder; +import io.github.tcdl.msb.api.ResponderContext; +import io.github.tcdl.msb.api.message.Message; + +/** + * Implementation of {@link ResponderContext} Provide access to {@link Responder} + * that used for sending response and {@link AcknowledgementHandler} that used + * for explicit confirm/reject received request + */ +public class ResponderContextImpl implements ResponderContext { + + private final Responder responder; + private final AcknowledgementHandler acknowledgementHandler; + private final Message originalMessage; + + public ResponderContextImpl(Responder responder, AcknowledgementHandler acknowledgementHandler, Message originalMessage) { + super(); + this.responder = responder; + this.acknowledgementHandler = acknowledgementHandler; + this.originalMessage = originalMessage; + } + + /** + * {@inheritDoc} + */ + @Override + public Responder getResponder() { + return responder; + } + + /** + * {@inheritDoc} + */ + @Override + public AcknowledgementHandler getAcknowledgementHandler() { + return acknowledgementHandler; + } + + /** + * {@inheritDoc} + */ + @Override + public Message getOriginalMessage() { + return originalMessage; + } + +} diff --git a/core/src/main/java/io/github/tcdl/msb/impl/ResponderImpl.java b/core/src/main/java/io/github/tcdl/msb/impl/ResponderImpl.java index e8f289b2..4f0886ff 100644 --- a/core/src/main/java/io/github/tcdl/msb/impl/ResponderImpl.java +++ b/core/src/main/java/io/github/tcdl/msb/impl/ResponderImpl.java @@ -3,7 +3,9 @@ import io.github.tcdl.msb.ChannelManager; import io.github.tcdl.msb.Producer; import io.github.tcdl.msb.api.MessageTemplate; +import io.github.tcdl.msb.api.RequestOptions; import io.github.tcdl.msb.api.Responder; +import io.github.tcdl.msb.api.message.Acknowledge.Builder; import io.github.tcdl.msb.api.message.Message; import io.github.tcdl.msb.message.MessageFactory; import io.github.tcdl.msb.support.Utils; @@ -11,22 +13,19 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static io.github.tcdl.msb.api.message.Acknowledge.Builder; - public class ResponderImpl implements Responder { private static final Logger LOG = LoggerFactory.getLogger(ResponderImpl.class); private String responderId; - private Message originalMessage; private ChannelManager channelManager; private MessageFactory messageFactory; private Message.Builder messageBuilder; - public ResponderImpl(MessageTemplate messageTemplate, Message originalMessage, MsbContextImpl msbContext) { + public ResponderImpl(MessageTemplate messageTemplate, Message originalMessage, + MsbContextImpl msbContext) { validateReceivedMessage(originalMessage); this.responderId = Utils.generateId(); - this.originalMessage = originalMessage; this.channelManager = msbContext.getChannelManager(); this.messageFactory = msbContext.getMessageFactory(); this.messageBuilder = messageFactory.createResponseMessageBuilder(messageTemplate, originalMessage); @@ -60,7 +59,7 @@ public void send(Object responsePayload) { } private void sendMessage(Message message) { - Producer producer = channelManager.findOrCreateProducer(message.getTopics().getTo()); + Producer producer = channelManager.findOrCreateProducer(message.getTopics().getTo(), true, RequestOptions.DEFAULTS); LOG.debug("Publishing message to topic : {}", message.getTopics().getTo()); producer.publish(message); } @@ -70,8 +69,4 @@ private void validateReceivedMessage(Message originalMessage) { Validate.notNull(originalMessage.getTopics(), "the 'originalMessage.topics' must not be null"); } - /** {@inheritDoc} */ - public Message getOriginalMessage() { - return this.originalMessage; - } } \ No newline at end of file diff --git a/core/src/main/java/io/github/tcdl/msb/impl/ResponderServerImpl.java b/core/src/main/java/io/github/tcdl/msb/impl/ResponderServerImpl.java index 0e454c1a..25f51ea4 100644 --- a/core/src/main/java/io/github/tcdl/msb/impl/ResponderServerImpl.java +++ b/core/src/main/java/io/github/tcdl/msb/impl/ResponderServerImpl.java @@ -2,44 +2,55 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableMap; import io.github.tcdl.msb.ChannelManager; -import io.github.tcdl.msb.api.MessageTemplate; -import io.github.tcdl.msb.api.Responder; -import io.github.tcdl.msb.api.ResponderServer; -import io.github.tcdl.msb.api.exception.JsonConversionException; +import io.github.tcdl.msb.MessageHandler; +import io.github.tcdl.msb.api.*; import io.github.tcdl.msb.api.message.Message; -import io.github.tcdl.msb.api.message.payload.RestPayload; +import io.github.tcdl.msb.api.metrics.Gauge; +import io.github.tcdl.msb.api.metrics.MetricSet; import io.github.tcdl.msb.support.Utils; import org.apache.commons.lang3.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Optional; + public class ResponderServerImpl implements ResponderServer { + private static final Logger LOG = LoggerFactory.getLogger(ResponderServerImpl.class); private String namespace; + private ResponderOptions responderOptions; private MsbContextImpl msbContext; - private MessageTemplate messageTemplate; private RequestHandler requestHandler; + private Optional errorHandler; private ObjectMapper payloadMapper; private TypeReference payloadTypeReference; - private ResponderServerImpl(String namespace, MessageTemplate messageTemplate, MsbContextImpl msbContext, RequestHandler requestHandler, TypeReference payloadTypeReference) { + private ResponderServerImpl(String namespace, + ResponderOptions responderOptions, + MsbContextImpl msbContext, + RequestHandler requestHandler, + ErrorHandler errorHandler, + TypeReference payloadTypeReference) { + Validate.notNull(responderOptions); this.namespace = namespace; - this.messageTemplate = messageTemplate; + this.responderOptions = responderOptions; this.msbContext = msbContext; this.requestHandler = requestHandler; + this.errorHandler = Optional.ofNullable(errorHandler); this.payloadMapper = msbContext.getPayloadMapper(); this.payloadTypeReference = payloadTypeReference; Validate.notNull(requestHandler, "requestHandler must not be null"); } /** - * {@link io.github.tcdl.msb.api.ObjectFactory#createResponderServer(String, MessageTemplate, RequestHandler, Class)} + * {@link io.github.tcdl.msb.api.ObjectFactory#createResponderServer(String, MessageTemplate, RequestHandler, ErrorHandler, Class)} */ - static ResponderServerImpl create(String namespace, MessageTemplate messageTemplate, MsbContextImpl msbContext, - RequestHandler requestHandler, TypeReference payloadTypeReference) { - return new ResponderServerImpl<>(namespace, messageTemplate, msbContext, requestHandler, payloadTypeReference); + static ResponderServerImpl create(String namespace, ResponderOptions responderOptions, MsbContextImpl msbContext, + RequestHandler requestHandler, ErrorHandler errorHandler, TypeReference payloadTypeReference) { + return new ResponderServerImpl<>(namespace, responderOptions, msbContext, requestHandler, errorHandler, payloadTypeReference); } /** @@ -54,35 +65,63 @@ static ResponderServerImpl create(String namespace, MessageTemplate mess public ResponderServer listen() { ChannelManager channelManager = msbContext.getChannelManager(); - channelManager.subscribe(namespace, - incomingMessage -> { - LOG.debug("[{}] Received message with id: [{}]", namespace, incomingMessage.getId()); - Responder responder = createResponder(incomingMessage); - onResponder(responder); - }); + MessageHandler messageHandler = (incomingMessage, acknowledgeHandler) -> { + LOG.debug("[{}] Received message with id: [{}]", namespace, incomingMessage.getId()); + Responder responder = createResponder(incomingMessage); + ResponderContext responderContext = createResponderContext(responder, acknowledgeHandler, incomingMessage); + onResponder(responderContext); + }; + channelManager.subscribe(namespace, responderOptions, messageHandler); + return this; + } + + @Override + public ResponderServer stop(){ + ChannelManager channelManager = msbContext.getChannelManager(); + channelManager.unsubscribe(namespace); return this; } + @Override + public MetricSet getMetrics() { + Gauge messageCountMetric = () -> msbContext.getChannelManager().getAvailableMessageCount(namespace).orElse(null); + Gauge consumerConnectedMetric = () -> msbContext.getChannelManager().isConnected(namespace).orElse(null); + return () -> ImmutableMap.of( + MetricSet.MESSAGE_COUNT_METRIC, messageCountMetric, + MetricSet.CONSUMER_CONNECTED_METRIC, consumerConnectedMetric); + } + Responder createResponder(Message incomingMessage) { if (isResponseNeeded(incomingMessage)) { - return new ResponderImpl(messageTemplate, incomingMessage, msbContext); + return new ResponderImpl(responderOptions.getMessageTemplate(), incomingMessage, msbContext); } else { return new NoopResponderImpl(incomingMessage); } } - void onResponder(Responder responder) { - Message originalMessage = responder.getOriginalMessage(); + ResponderContext createResponderContext(Responder responder, AcknowledgementHandler acknowledgeHandler, Message incomingMessage) { + return new ResponderContextImpl(responder, acknowledgeHandler, incomingMessage); + } + + void onResponder(ResponderContext responderContext) { + Message originalMessage = responderContext.getOriginalMessage(); Object rawPayload = originalMessage.getRawPayload(); try { T request = Utils.convert(rawPayload, payloadTypeReference, payloadMapper); + MsbThreadContext.setMessageContext(responderContext); + MsbThreadContext.setRequest(request); + LOG.debug("[{}] Process message with id: [{}]", namespace, originalMessage.getId()); - requestHandler.process(request, responder); - } catch (JsonConversionException conversionEx) { - errorHandler(responder, conversionEx, PAYLOAD_CONVERSION_ERROR_CODE); - } catch (Exception internalEx) { - errorHandler(responder, internalEx, INTERNAL_SERVER_ERROR_CODE); + requestHandler.process(request, responderContext); + } catch (Exception e) { + if (errorHandler.isPresent()) { + errorHandler.get().handle(e, originalMessage); + } else { + errorHandler(responderContext, e); + } + } finally { + MsbThreadContext.clear(); } } @@ -90,13 +129,11 @@ private boolean isResponseNeeded(Message incomingMessage) { return incomingMessage.getTopics().getResponse() != null; } - private void errorHandler(Responder responder, Exception exception, int errorStatusCode) { - Message originalMessage = responder.getOriginalMessage(); + private void errorHandler(ResponderContext responderContext, Exception exception) { + Message originalMessage = responderContext.getOriginalMessage(); LOG.error("[{}] Error while processing message with id: [{}]", namespace, originalMessage.getId(), exception); - RestPayload responsePayload = new RestPayload.Builder() - .withStatusCode(errorStatusCode) - .withStatusMessage(exception.getMessage()) - .build(); - responder.send(responsePayload); + responderContext.getResponder().sendAck(0, 0); + //Confirm message for prevention requeue message with incorrect structure + responderContext.getAcknowledgementHandler().confirmMessage(); } } \ No newline at end of file diff --git a/core/src/main/java/io/github/tcdl/msb/impl/SimpleMessageHandlerResolverImpl.java b/core/src/main/java/io/github/tcdl/msb/impl/SimpleMessageHandlerResolverImpl.java new file mode 100644 index 00000000..3be5c426 --- /dev/null +++ b/core/src/main/java/io/github/tcdl/msb/impl/SimpleMessageHandlerResolverImpl.java @@ -0,0 +1,33 @@ +package io.github.tcdl.msb.impl; + +import io.github.tcdl.msb.MessageHandler; +import io.github.tcdl.msb.MessageHandlerResolver; +import io.github.tcdl.msb.api.message.Message; + +import java.util.Optional; + +/** + * Trivial {@link MessageHandlerResolver} implementation that returns + * a single single {@link MessageHandler} by any incoming {@link Message}. + */ +public class SimpleMessageHandlerResolverImpl implements MessageHandlerResolver { + + private final MessageHandler messageHandler; + + private final String loggingName; + + public SimpleMessageHandlerResolverImpl(MessageHandler messageHandler, String loggingName) { + this.messageHandler = messageHandler; + this.loggingName = loggingName; + } + + @Override + public Optional resolveMessageHandler(Message message) { + return Optional.of(messageHandler); + } + + @Override + public String getLoggingName() { + return loggingName; + } +} diff --git a/core/src/main/java/io/github/tcdl/msb/message/MessageFactory.java b/core/src/main/java/io/github/tcdl/msb/message/MessageFactory.java index 52d91a1c..98b5ca1b 100644 --- a/core/src/main/java/io/github/tcdl/msb/message/MessageFactory.java +++ b/core/src/main/java/io/github/tcdl/msb/message/MessageFactory.java @@ -10,6 +10,7 @@ import io.github.tcdl.msb.api.message.Topics; import io.github.tcdl.msb.config.ServiceDetails; import io.github.tcdl.msb.support.Utils; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import java.time.Clock; @@ -49,19 +50,26 @@ public Message createBroadcastMessage(Message.Builder messageBuilder, Object pay return createRequestMessage(messageBuilder, payload); } - public Message.Builder createRequestMessageBuilder(String namespace, MessageTemplate messageTemplate, Message originalMessage) { - Topics topic = new Topics(namespace, namespace + ":response:" + - this.serviceDetails.getInstanceId()); + public Message.Builder createRequestMessageBuilder(String namespace, String forwardNamespace, String routingKey, MessageTemplate messageTemplate, Message originalMessage) { + String responseNamespace = StringUtils.isBlank(forwardNamespace) + ? namespace + ":response:" + this.serviceDetails.getInstanceId() + : null; + + Topics topic = new Topics(namespace, responseNamespace, forwardNamespace, routingKey); return createMessageBuilder(topic, messageTemplate, originalMessage, false); } + public Message.Builder createRequestMessageBuilder(String namespace, String forwardNamespace, MessageTemplate messageTemplate, Message originalMessage) { + return createRequestMessageBuilder(namespace, forwardNamespace, null, messageTemplate, originalMessage); + } + public Message.Builder createResponseMessageBuilder(MessageTemplate messageTemplate, Message originalMessage) { - Topics topic = new Topics(originalMessage.getTopics().getResponse(), null); + Topics topic = new Topics(originalMessage.getTopics().getResponse(), null, null); return createMessageBuilder(topic, messageTemplate, originalMessage, true); } public Message.Builder createBroadcastMessageBuilder(String namespace, MessageTemplate messageTemplate) { - Topics topic = new Topics(namespace, null); + Topics topic = new Topics(namespace, null, null); return createMessageBuilder(topic, messageTemplate, null, false); } diff --git a/core/src/main/java/io/github/tcdl/msb/monitor/agent/AgentTopicStats.java b/core/src/main/java/io/github/tcdl/msb/monitor/agent/AgentTopicStats.java deleted file mode 100644 index 29237b59..00000000 --- a/core/src/main/java/io/github/tcdl/msb/monitor/agent/AgentTopicStats.java +++ /dev/null @@ -1,106 +0,0 @@ -package io.github.tcdl.msb.monitor.agent; - -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.time.Instant; -import java.util.Objects; - -/** - * Effectively immutable class that contains statistics for a topic. - */ -public class AgentTopicStats { - /** Indicates whether this microservice produces to the topic */ - private boolean producers; - - /** Indicates whether this microservice consumes from the topic */ - private boolean consumers; - - /** Time when this microservice produced to the topic for the last time. */ - private Instant lastProducedAt; - - /** Time when this microservice consumed from the topic for the last time */ - private Instant lastConsumedAt; - - public AgentTopicStats() { - } - - @SuppressWarnings("unused") - public AgentTopicStats(@JsonProperty boolean producers, @JsonProperty boolean consumers, @JsonProperty Instant lastProducedAt, @JsonProperty Instant lastConsumedAt) { - this.producers = producers; - this.consumers = consumers; - this.lastProducedAt = lastProducedAt; - this.lastConsumedAt = lastConsumedAt; - } - - public AgentTopicStats(AgentTopicStats agentTopicStats) { - this.consumers = agentTopicStats.consumers; - this.producers = agentTopicStats.producers; - this.lastConsumedAt = agentTopicStats.lastConsumedAt; - this.lastProducedAt = agentTopicStats.lastProducedAt; - } - - public boolean isProducers() { - return producers; - } - - public boolean isConsumers() { - return consumers; - } - - public Instant getLastProducedAt() { - return lastProducedAt; - } - - public Instant getLastConsumedAt() { - return lastConsumedAt; - } - - public AgentTopicStats withProducers(boolean producers) { - AgentTopicStats newTopic = new AgentTopicStats(this); - newTopic.producers = producers; - return newTopic; - } - - public AgentTopicStats withConsumers(boolean consumers) { - AgentTopicStats newTopic = new AgentTopicStats(this); - newTopic.consumers = consumers; - return newTopic; - } - - public AgentTopicStats withLastProducedAt(Instant lastProducedAt) { - AgentTopicStats newTopic = new AgentTopicStats(this); - newTopic.lastProducedAt = lastProducedAt; - return newTopic; - } - - public AgentTopicStats withLastConsumedAt(Instant lastConsumedAt) { - AgentTopicStats newTopic = new AgentTopicStats(this); - newTopic.lastConsumedAt = lastConsumedAt; - return newTopic; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - AgentTopicStats that = (AgentTopicStats) o; - return Objects.equals(producers, that.producers) && - Objects.equals(consumers, that.consumers) && - Objects.equals(lastProducedAt, that.lastProducedAt) && - Objects.equals(lastConsumedAt, that.lastConsumedAt); - } - - @Override - public int hashCode() { - return Objects.hash(producers, consumers, lastProducedAt, lastConsumedAt); - } - - @Override public String toString() { - return String.format("AgentTopicStats [producers=%s, consumers=%s, lastProducedAt=%s, lastConsumedAt=%s]", producers, consumers, lastProducedAt, - lastConsumedAt); - } -} \ No newline at end of file diff --git a/core/src/main/java/io/github/tcdl/msb/monitor/agent/ChannelMonitorAgent.java b/core/src/main/java/io/github/tcdl/msb/monitor/agent/ChannelMonitorAgent.java deleted file mode 100644 index a52e1ed2..00000000 --- a/core/src/main/java/io/github/tcdl/msb/monitor/agent/ChannelMonitorAgent.java +++ /dev/null @@ -1,48 +0,0 @@ -package io.github.tcdl.msb.monitor.agent; - -import io.github.tcdl.msb.ChannelManager; - -/** - * Observer interface that allows to subscribe to different events related to - * consuming from and producing to topics. - * - * The implementation is intended to be injected into {@link ChannelManager} instance. - */ -public interface ChannelMonitorAgent { - /** - * Fired when topic producer is created. Typically this happens just before publishing of the first message - * to the given topic. - * - * @param topicName - */ - void producerTopicCreated(String topicName); - - /** - * Fired when consumer is created that listens to the messages on the given topic. - * - * @param topicName - */ - void consumerTopicCreated(String topicName); - - /** - * Fired when consumer is removed for the given topic. The consumer is not longer able to listen to any messages - * on that topic. - * - * @param topicName - */ - void consumerTopicRemoved(String topicName); - - /** - * Fired when a message is sent to the given topic. - * - * @param topicName - */ - void producerMessageSent(String topicName); - - /** - * Fired when a message is consumed from the given topic. - * - * @param topicName - */ - void consumerMessageReceived(String topicName); -} diff --git a/core/src/main/java/io/github/tcdl/msb/monitor/agent/DefaultChannelMonitorAgent.java b/core/src/main/java/io/github/tcdl/msb/monitor/agent/DefaultChannelMonitorAgent.java deleted file mode 100644 index d29e02f0..00000000 --- a/core/src/main/java/io/github/tcdl/msb/monitor/agent/DefaultChannelMonitorAgent.java +++ /dev/null @@ -1,153 +0,0 @@ -package io.github.tcdl.msb.monitor.agent; - -import io.github.tcdl.msb.ChannelManager; -import io.github.tcdl.msb.Producer; -import io.github.tcdl.msb.api.MessageTemplate; -import io.github.tcdl.msb.api.Responder; -import io.github.tcdl.msb.api.message.Message; -import io.github.tcdl.msb.api.message.payload.RestPayload; -import io.github.tcdl.msb.impl.MsbContextImpl; -import io.github.tcdl.msb.impl.ResponderImpl; -import io.github.tcdl.msb.message.MessageFactory; -import io.github.tcdl.msb.support.Utils; - -import java.time.Clock; -import java.time.Instant; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * This implementation maintains statistics over all topics. It broadcasts that statistics over the bus for special monitoring microservices. The overall - * process consists of the following steps: - * - * 1. The agent sends an announcement message each time when a new consumer or producer is created for some topic - * 2. The agent listens on special heartbeat topic for periodic heartbeat messages - * 3. The agent sends the current statistics in response to the heartbeat. - */ -public class DefaultChannelMonitorAgent implements ChannelMonitorAgent { - private MsbContextImpl msbContext; - private ChannelManager channelManager; - private MessageFactory messageFactory; - private Clock clock; - - /** - * This map contains statistics info per topic. - */ - Map topicInfoMap = new ConcurrentHashMap<>(); - - public DefaultChannelMonitorAgent(MsbContextImpl msbContext) { - this.msbContext = msbContext; - - this.channelManager = msbContext.getChannelManager(); - this.messageFactory = msbContext.getMessageFactory(); - this.clock = msbContext.getClock(); - } - - /** - * Convenience factory method that creates the agent instance and starts it. - */ - public static void start(MsbContextImpl msbContext) { - new DefaultChannelMonitorAgent(msbContext).start(); - } - - /** - * Start listening on the heartbeat topic and injects itself in the channel manager instance. - * - * @return this channel manages instance. Might be useful for chaining calls. - */ - public DefaultChannelMonitorAgent start() { - channelManager.subscribe(Utils.TOPIC_HEARTBEAT, // Launch listener for heartbeat topic - message -> { - Responder responder = new ResponderImpl(null, message, msbContext); - RestPayload payload = new RestPayload.Builder>() - .withBody(topicInfoMap) - .build(); - responder.send(payload); - }); - - channelManager.setChannelMonitorAgent(this); // Inject itself in channel manager - - return this; - } - - /** {@inheritDoc} */ - @Override - public void producerTopicCreated(String topicName) { - if (Utils.isServiceTopic(topicName)) { - return; - } - - topicInfoMap.compute(topicName, - (key, agentTopicStats) -> ensureNotNull(agentTopicStats).withProducers(true)); - - doAnnounce(); - } - - /** {@inheritDoc} */ - @Override - public void consumerTopicCreated(String topicName) { - if (Utils.isServiceTopic(topicName)) { - return; - } - - topicInfoMap.compute(topicName, - (key, agentTopicStats) -> ensureNotNull(agentTopicStats).withConsumers(true)); - - doAnnounce(); - } - - /** {@inheritDoc} */ - @Override - public void consumerTopicRemoved(String topicName) { - if (Utils.isServiceTopic(topicName)) { - return; - } - - topicInfoMap.compute(topicName, - (key, agentTopicStats) -> ensureNotNull(agentTopicStats).withConsumers(false)); - } - - /** {@inheritDoc} */ - @Override - public void producerMessageSent(String topicName) { - if (Utils.isServiceTopic(topicName)) { - return; - } - - Instant now = clock.instant(); - topicInfoMap.compute(topicName, - (key, agentTopicStats) -> ensureNotNull(agentTopicStats).withLastProducedAt(now)); - } - - /** {@inheritDoc} */ - @Override - public void consumerMessageReceived(String topicName) { - if (Utils.isServiceTopic(topicName)) { - return; - } - - Instant now = clock.instant(); - topicInfoMap.compute(topicName, - (key, agentTopicStats) -> ensureNotNull(agentTopicStats).withLastConsumedAt(now)); - } - - private AgentTopicStats ensureNotNull(AgentTopicStats topicStats) { - return (topicStats != null) ? topicStats : new AgentTopicStats(); - } - - /** - * Makes broadcast of the current statistics. - */ - private void doAnnounce() { - RestPayload payload = new RestPayload.Builder>() - .withBody(topicInfoMap) - .build(); - - Producer producer = channelManager.findOrCreateProducer(Utils.TOPIC_ANNOUNCE); - - Message.Builder messageBuilder = messageFactory.createBroadcastMessageBuilder(Utils.TOPIC_ANNOUNCE, new MessageTemplate()); - Message announcementMessage = messageFactory.createBroadcastMessage(messageBuilder, payload); - - producer.publish(announcementMessage); - } -} diff --git a/core/src/main/java/io/github/tcdl/msb/monitor/agent/NoopChannelMonitorAgent.java b/core/src/main/java/io/github/tcdl/msb/monitor/agent/NoopChannelMonitorAgent.java deleted file mode 100644 index b212a2be..00000000 --- a/core/src/main/java/io/github/tcdl/msb/monitor/agent/NoopChannelMonitorAgent.java +++ /dev/null @@ -1,31 +0,0 @@ -package io.github.tcdl.msb.monitor.agent; - -/** - * Empty implementation that does nothing for each event. - */ -public class NoopChannelMonitorAgent implements ChannelMonitorAgent { - /** {@inheritDoc} */ - @Override - public void producerTopicCreated(String topicName) { - } - - /** {@inheritDoc} */ - @Override - public void consumerTopicCreated(String topicName) { - } - - /** {@inheritDoc} */ - @Override - public void consumerTopicRemoved(String topicName) { - } - - /** {@inheritDoc} */ - @Override - public void producerMessageSent(String topicName) { - } - - /** {@inheritDoc} */ - @Override - public void consumerMessageReceived(String topicName) { - } -} diff --git a/core/src/main/java/io/github/tcdl/msb/monitor/aggregator/DefaultChannelMonitorAggregator.java b/core/src/main/java/io/github/tcdl/msb/monitor/aggregator/DefaultChannelMonitorAggregator.java deleted file mode 100644 index ba9f1973..00000000 --- a/core/src/main/java/io/github/tcdl/msb/monitor/aggregator/DefaultChannelMonitorAggregator.java +++ /dev/null @@ -1,160 +0,0 @@ -package io.github.tcdl.msb.monitor.aggregator; - -import static io.github.tcdl.msb.support.Utils.TOPIC_ANNOUNCE; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.github.tcdl.msb.ChannelManager; -import io.github.tcdl.msb.api.Callback; -import io.github.tcdl.msb.api.ObjectFactory; -import io.github.tcdl.msb.api.exception.JsonConversionException; -import io.github.tcdl.msb.api.message.Message; -import io.github.tcdl.msb.api.message.MetaMessage; -import io.github.tcdl.msb.api.message.payload.RestPayload; -import io.github.tcdl.msb.api.monitor.AggregatorStats; -import io.github.tcdl.msb.api.monitor.AggregatorTopicStats; -import io.github.tcdl.msb.api.monitor.ChannelMonitorAggregator; -import io.github.tcdl.msb.config.ServiceDetails; -import io.github.tcdl.msb.impl.MsbContextImpl; -import io.github.tcdl.msb.monitor.agent.AgentTopicStats; -import io.github.tcdl.msb.support.Utils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class DefaultChannelMonitorAggregator implements ChannelMonitorAggregator { - private static final Logger LOG = LoggerFactory.getLogger(DefaultChannelMonitorAggregator.class); - - private ChannelManager channelManager; - private ObjectFactory objectFactory; - private ObjectMapper messageMapper; - private ScheduledExecutorService scheduledExecutorService; - private Callback handler; - - AggregatorStats masterAggregatorStats = new AggregatorStats(); - - public DefaultChannelMonitorAggregator(MsbContextImpl msbContext, ScheduledExecutorService scheduledExecutorService, - Callback aggregatorStatsHandler) { - this.channelManager = msbContext.getChannelManager(); - this.objectFactory = msbContext.getObjectFactory(); - this.messageMapper = msbContext.getPayloadMapper(); - this.scheduledExecutorService = scheduledExecutorService; - this.handler = aggregatorStatsHandler; - } - - @Override - public void start(boolean activateHeartbeats, long heartbeatIntervalMs, int heartbeatTimeoutMs) { - channelManager.subscribe(TOPIC_ANNOUNCE, this::onAnnounce); - LOG.debug(String.format("Subscribed to %s", TOPIC_ANNOUNCE)); - - if (activateHeartbeats) { - Runnable heartbeatTask = new HeartbeatTask(heartbeatTimeoutMs, objectFactory, this::onHeartbeatResponses); - scheduledExecutorService.scheduleAtFixedRate(heartbeatTask, 0, heartbeatIntervalMs, TimeUnit.MILLISECONDS); - LOG.debug("Periodic heartbeats activated"); - } - - LOG.info("DefaultChannelMonitorAggregator started"); - } - - @Override - public void stop() { - channelManager.unsubscribe(TOPIC_ANNOUNCE); - scheduledExecutorService.shutdown(); - LOG.info("DefaultChannelMonitorAggregator stopped"); - } - - void onHeartbeatResponses(List heartbeatResponses) { - LOG.debug(String.format("Handling heartbeat responses %s...", heartbeatResponses)); - AggregatorStats aggregatorStats = new AggregatorStats(); - boolean successfullyAggregatedAtLeastOne = false; - for (Message msg : heartbeatResponses) { - if (aggregateInfo(aggregatorStats, msg)) { - successfullyAggregatedAtLeastOne = true; - } - } - if (successfullyAggregatedAtLeastOne) { - LOG.debug(String.format("Calling registered handler for heartbeat statistics %s...", masterAggregatorStats)); - handler.call(aggregatorStats); - masterAggregatorStats = aggregatorStats; - LOG.debug(String.format("Heartbeat responses processed")); - } - } - - void onAnnounce(Message announcementMessage) { - LOG.debug(String.format("Handling announcement message %s...", announcementMessage)); - - boolean successfullyAggregated = aggregateInfo(masterAggregatorStats, announcementMessage); - - if (successfullyAggregated) { - LOG.debug(String.format("Calling registered handler for announcement statistics %s...", masterAggregatorStats)); - handler.call(masterAggregatorStats); - LOG.debug(String.format("Announcement message processed")); - } - } - - boolean aggregateInfo(AggregatorStats aggregatorStats, Message message) { - - JsonNode rawPayload = message.getRawPayload(); - - if (!Utils.isPayloadPresent(rawPayload)) { - LOG.error("Unable to convert message. Message payload is empty."); - return false; - } - - try { - RestPayload> payload = Utils - .convert(rawPayload, new TypeReference>>() { - }, messageMapper); - MetaMessage meta = message.getMeta(); - ServiceDetails serviceDetails = meta.getServiceDetails(); - String instanceId = serviceDetails.getInstanceId(); - aggregatorStats.getServiceDetailsById().put(instanceId, serviceDetails); - - Map agentTopicStatsMap = payload.getBody(); - aggregateTopicStats(aggregatorStats, agentTopicStatsMap, instanceId); - } catch (JsonConversionException e) { - LOG.error("Unable to convert message.", e); - return false; - } - - return true; - } - - void aggregateTopicStats(AggregatorStats aggregatorStats, Map agentTopicStatsMap, String instanceId) { - for (Entry entry : agentTopicStatsMap.entrySet()) { - String topic = entry.getKey(); - AgentTopicStats agentTopicStats = entry.getValue(); - - aggregatorStats.getTopicInfoMap().compute(topic, (topic1, oldValue) -> { - AggregatorTopicStats newValue = new AggregatorTopicStats(oldValue); - - if (agentTopicStats.isConsumers()) { - newValue.getConsumers().add(instanceId); - if (newValue.getLastConsumedAt() == null) { - newValue.withLastConsumedAt(agentTopicStats.getLastConsumedAt()); - } else if (agentTopicStats.getLastConsumedAt() != null - && agentTopicStats.getLastConsumedAt().isAfter(newValue.getLastConsumedAt())) { - newValue.withLastConsumedAt(agentTopicStats.getLastConsumedAt()); - } - } - - if (agentTopicStats.isProducers()) { - newValue.getProducers().add(instanceId); - if (newValue.getLastProducedAt() == null) { - newValue.withLastProducedAt(agentTopicStats.getLastProducedAt()); - } else if (agentTopicStats.getLastProducedAt() != null - && agentTopicStats.getLastProducedAt().isAfter(newValue.getLastProducedAt())) { - newValue.withLastProducedAt(agentTopicStats.getLastProducedAt()); - } - } - - return newValue; - }); - } - } -} diff --git a/core/src/main/java/io/github/tcdl/msb/monitor/aggregator/HeartbeatTask.java b/core/src/main/java/io/github/tcdl/msb/monitor/aggregator/HeartbeatTask.java deleted file mode 100644 index acee6df1..00000000 --- a/core/src/main/java/io/github/tcdl/msb/monitor/aggregator/HeartbeatTask.java +++ /dev/null @@ -1,55 +0,0 @@ -package io.github.tcdl.msb.monitor.aggregator; - -import io.github.tcdl.msb.api.Callback; -import io.github.tcdl.msb.api.ObjectFactory; -import io.github.tcdl.msb.api.RequestOptions; -import io.github.tcdl.msb.api.message.Message; -import io.github.tcdl.msb.api.message.payload.RestPayload; -import io.github.tcdl.msb.support.Utils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; - -/** - * Sends heartbeat requests and passes the aggregated responses to the registered handler. This task is meant to be scheduled periodically. - */ -public class HeartbeatTask implements Runnable { - - private static final Logger LOG = LoggerFactory.getLogger(HeartbeatTask.class); - - private int heartbeatTimeoutMs; - private ObjectFactory objectFactory; - private Callback> heartbeatHandler; - - public HeartbeatTask(int heartbeatTimeoutMs, ObjectFactory objectFactory, Callback> heartbeatHandler) { - this.heartbeatTimeoutMs = heartbeatTimeoutMs; - this.objectFactory = objectFactory; - this.heartbeatHandler = heartbeatHandler; - } - - @Override - public void run() { - try { - LOG.debug("Sending heartbeat request..."); - RequestOptions requestOptions = new RequestOptions.Builder() - .withResponseTimeout(heartbeatTimeoutMs) - .withWaitForResponses(-1) - .build(); - - RestPayload emptyPayload = new RestPayload.Builder().build(); - - List messages = Collections.synchronizedList(new LinkedList<>()); - objectFactory.createRequester(Utils.TOPIC_HEARTBEAT, requestOptions) - .onRawResponse(messages::add) - .onEnd(end -> heartbeatHandler.call(messages)) - .publish(emptyPayload); - - LOG.debug("Heartbeat request sent"); - } catch (Exception e) { - LOG.error("Error during heartbeat invocation", e); - } - } -} diff --git a/core/src/main/java/io/github/tcdl/msb/support/JsonValidator.java b/core/src/main/java/io/github/tcdl/msb/support/JsonValidator.java index cc7cdf05..af78f1d2 100644 --- a/core/src/main/java/io/github/tcdl/msb/support/JsonValidator.java +++ b/core/src/main/java/io/github/tcdl/msb/support/JsonValidator.java @@ -8,6 +8,8 @@ import com.github.fge.jsonschema.main.JsonSchemaFactory; import io.github.tcdl.msb.api.exception.JsonSchemaValidationException; import org.apache.commons.lang3.Validate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.StringReader; @@ -18,8 +20,9 @@ * Validates JSON against JSON Schema */ public class JsonValidator { - - private Map schemaCache = new ConcurrentHashMap<>(); + private static final Logger LOG = LoggerFactory.getLogger(JsonValidator.class); + private static final String ERROR_MESSAGE_TEMPLATE = "Error while validating message using schema '%s'"; + private Map schemaCache = new ConcurrentHashMap<>(); private JsonReader jsonReader; public JsonValidator() { @@ -40,17 +43,17 @@ public void validate(String json, String schema) { try { JsonNode jsonNode = jsonReader.read(json); - JsonNode jsonSchemaNode = schemaCache.computeIfAbsent(schema, s -> { + + JsonSchema jsonSchema = schemaCache.computeIfAbsent(schema, s -> { try { - return jsonReader.read(s); - } catch (IOException e) { - throw new JsonSchemaValidationException(String.format("Failed reading schema"), e); + JsonNode jsonSchemaNode = jsonReader.read(s); + JsonSchemaFactory factory = JsonSchemaFactory.byDefault(); + return factory.getJsonSchema(jsonSchemaNode); + } catch (Exception e) { + throw new JsonSchemaValidationException("Failed reading schema", e); } }); - JsonSchemaFactory factory = JsonSchemaFactory.byDefault(); - JsonSchema jsonSchema = factory.getJsonSchema(jsonSchemaNode); - ProcessingReport validationReport = jsonSchema.validate(jsonNode); if (!validationReport.isSuccess()) { @@ -58,7 +61,9 @@ public void validate(String json, String schema) { } } catch (IOException | ProcessingException e) { - throw new JsonSchemaValidationException(String.format("Error while validating message '%s' using schema '%s'", json, schema), e); + LOG.error(ERROR_MESSAGE_TEMPLATE, schema); + LOG.trace("Message: {}", json); + throw new JsonSchemaValidationException(String.format(ERROR_MESSAGE_TEMPLATE, schema), e); } } diff --git a/core/src/main/java/io/github/tcdl/msb/support/Utils.java b/core/src/main/java/io/github/tcdl/msb/support/Utils.java index 25f4b051..f3ebf0a0 100644 --- a/core/src/main/java/io/github/tcdl/msb/support/Utils.java +++ b/core/src/main/java/io/github/tcdl/msb/support/Utils.java @@ -1,22 +1,22 @@ package io.github.tcdl.msb.support; -import java.io.IOException; -import java.util.UUID; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.regex.Pattern; - import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.NullNode; import io.github.tcdl.msb.api.exception.JsonConversionException; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; import java.lang.reflect.Type; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; /** * Created by rdro on 4/22/2015. @@ -54,7 +54,6 @@ public static String toJson(Object object, ObjectMapper objectMapper) { try { return objectMapper.writeValueAsString(object); } catch (JsonProcessingException e) { - LOG.error("Failed to parse to JSON object: [{}] ", object); throw new JsonConversionException("Failed parse to JSON", e); } } @@ -74,12 +73,11 @@ public Type getType() { } public static T fromJson(String json, TypeReference typeReference, ObjectMapper objectMapper) { - if (json == null) + if (StringUtils.isEmpty(json)) return null; try { return objectMapper.readValue(json, typeReference); } catch (IOException e) { - LOG.error("Failed to parse from JSON: [{}] to object of type: [{}]", json, typeReference); throw new JsonConversionException("Failed parse from JSON", e); } } @@ -99,7 +97,6 @@ public static T convert(Object srcObject, TypeReference typeReference, Ob try { return objectMapper.convertValue(srcObject, typeReference); } catch (Exception e) { - LOG.error("Failed to convert object [{}] to type: [{}]", srcObject, typeReference.getType()); throw new JsonConversionException(e.getMessage(), e); } } @@ -118,15 +115,15 @@ public static boolean isPayloadPresent(JsonNode rawPayload) { public static void gracefulShutdown(ExecutorService executorService, String executorServiceName) { int pollingTimeout = 10; - LOG.info(String.format("[thread pool '%s'] Shutting down...", executorServiceName)); + LOG.info("[thread pool '{}'] Shutting down...", executorServiceName); executorService.shutdown(); try { while (!executorService.awaitTermination(pollingTimeout, TimeUnit.SECONDS)) { - LOG.info(String.format("[thread pool '%s'] Still has some tasks to complete. Waiting...", executorServiceName)); + LOG.info("[thread pool '{}'] Still has some tasks to complete. Waiting...", executorServiceName); } } catch (InterruptedException e) { - LOG.warn(String.format("[thread pool '%s'] Interrupted while waiting for termination", executorServiceName), e); + LOG.warn("[thread pool '{}'] Interrupted while waiting for termination", executorServiceName, e); } - LOG.info(String.format("[thread pool '%s'] Shut down complete.", executorServiceName)); + LOG.info("[thread pool '{}'] Shut down complete.", executorServiceName); } } diff --git a/core/src/main/java/io/github/tcdl/msb/threading/ConsumerExecutorFactory.java b/core/src/main/java/io/github/tcdl/msb/threading/ConsumerExecutorFactory.java new file mode 100644 index 00000000..3c85cd7d --- /dev/null +++ b/core/src/main/java/io/github/tcdl/msb/threading/ConsumerExecutorFactory.java @@ -0,0 +1,11 @@ +package io.github.tcdl.msb.threading; + +import java.util.concurrent.ExecutorService; + +/** + * Implementations define a way to create {@link ExecutorService} instances + * used to invoke {@link io.github.tcdl.msb.MessageHandler}. + */ +public interface ConsumerExecutorFactory { + ExecutorService createConsumerThreadPool(int numberOfThreads, int queueCapacity); +} diff --git a/core/src/main/java/io/github/tcdl/msb/threading/ConsumerExecutorFactoryImpl.java b/core/src/main/java/io/github/tcdl/msb/threading/ConsumerExecutorFactoryImpl.java new file mode 100644 index 00000000..d23b6259 --- /dev/null +++ b/core/src/main/java/io/github/tcdl/msb/threading/ConsumerExecutorFactoryImpl.java @@ -0,0 +1,33 @@ +package io.github.tcdl.msb.threading; + +import org.apache.commons.lang3.concurrent.BasicThreadFactory; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public class ConsumerExecutorFactoryImpl implements ConsumerExecutorFactory { + + protected static final int QUEUE_SIZE_UNLIMITED = -1; + private final BasicThreadFactory threadFactory = new BasicThreadFactory.Builder() + .namingPattern("msb-consumer-thread-%d") + .build(); + + @Override + public ExecutorService createConsumerThreadPool(int numberOfThreads, int queueCapacity) { + BlockingQueue queue; + if (queueCapacity == QUEUE_SIZE_UNLIMITED) { + queue = new LinkedBlockingQueue<>(); + } else { + queue = new ArrayBlockingQueue<>(queueCapacity); + } + + return new ThreadPoolExecutor(numberOfThreads, numberOfThreads, + 0L, TimeUnit.MILLISECONDS, + queue, + threadFactory); + } +} diff --git a/core/src/main/java/io/github/tcdl/msb/threading/DirectInvocationCapableInvoker.java b/core/src/main/java/io/github/tcdl/msb/threading/DirectInvocationCapableInvoker.java new file mode 100644 index 00000000..fdab9733 --- /dev/null +++ b/core/src/main/java/io/github/tcdl/msb/threading/DirectInvocationCapableInvoker.java @@ -0,0 +1,46 @@ +package io.github.tcdl.msb.threading; + +import io.github.tcdl.msb.MessageHandler; +import io.github.tcdl.msb.acknowledge.AcknowledgementHandlerInternal; +import io.github.tcdl.msb.api.message.Message; +import io.github.tcdl.msb.collector.ExecutionOptionsAwareMessageHandler; +import org.apache.commons.lang3.Validate; + +/** + * Created by Alexandr Zolotov + * 23.05.16 + */ +public class DirectInvocationCapableInvoker implements MessageHandlerInvoker { + + private final MessageHandlerInvoker clientMessageHandlerInvoker; + private final MessageHandlerInvoker directMessageHandlerInvoker; + + /** + * Creates composite delegate that is guarantied to have an instance of direct {@link MessageHandlerInvoker} in its disposal. + * There is no need to instantiate it in client code. It is intended to be used only internally by the library. + */ + public DirectInvocationCapableInvoker(MessageHandlerInvoker clientMessageHandlerInvoker, MessageHandlerInvoker directMessageHandlerInvoker) { + Validate.notNull(clientMessageHandlerInvoker); + Validate.notNull(directMessageHandlerInvoker); + this.clientMessageHandlerInvoker = clientMessageHandlerInvoker; + this.directMessageHandlerInvoker = directMessageHandlerInvoker; + } + + /** + * {@inheritDoc} + */ + @Override + public void execute(MessageHandler messageHandler, Message message, AcknowledgementHandlerInternal acknowledgeHandler) { + if (messageHandler instanceof ExecutionOptionsAwareMessageHandler && ((ExecutionOptionsAwareMessageHandler) messageHandler).forceDirectInvocation()) { + directMessageHandlerInvoker.execute(messageHandler, message, acknowledgeHandler); + } else { + clientMessageHandlerInvoker.execute(messageHandler, message, acknowledgeHandler); + } + } + + @Override + public void shutdown() { + clientMessageHandlerInvoker.shutdown(); + directMessageHandlerInvoker.shutdown(); + } +} \ No newline at end of file diff --git a/core/src/main/java/io/github/tcdl/msb/threading/DirectMessageHandlerInvoker.java b/core/src/main/java/io/github/tcdl/msb/threading/DirectMessageHandlerInvoker.java new file mode 100644 index 00000000..bc11745b --- /dev/null +++ b/core/src/main/java/io/github/tcdl/msb/threading/DirectMessageHandlerInvoker.java @@ -0,0 +1,23 @@ +package io.github.tcdl.msb.threading; + +import io.github.tcdl.msb.MessageHandler; +import io.github.tcdl.msb.acknowledge.AcknowledgementHandlerInternal; +import io.github.tcdl.msb.api.message.Message; + +/** + * Trivial {@link MessageHandlerInvoker} implementation that preforms a direct {@link MessageHandler} invocation + * to process a {@link Message} received. + */ +public class DirectMessageHandlerInvoker implements MessageHandlerInvoker { + + @Override + public void execute(MessageHandler messageHandler, Message message, AcknowledgementHandlerInternal acknowledgeHandler) { + messageHandler.handleMessage(message, acknowledgeHandler); + acknowledgeHandler.autoConfirm(); + } + + @Override + public void shutdown() { + + } +} diff --git a/core/src/main/java/io/github/tcdl/msb/threading/ExecutorBasedMessageHandlerInvoker.java b/core/src/main/java/io/github/tcdl/msb/threading/ExecutorBasedMessageHandlerInvoker.java new file mode 100644 index 00000000..26775c19 --- /dev/null +++ b/core/src/main/java/io/github/tcdl/msb/threading/ExecutorBasedMessageHandlerInvoker.java @@ -0,0 +1,33 @@ +package io.github.tcdl.msb.threading; + +import io.github.tcdl.msb.MessageHandler; +import io.github.tcdl.msb.acknowledge.AcknowledgementHandlerInternal; +import io.github.tcdl.msb.api.message.Message; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Base class for {@link MessageHandlerInvoker} implementations that rely on a custom + * threading model. + */ +public abstract class ExecutorBasedMessageHandlerInvoker implements MessageHandlerInvoker { + + private static final Logger LOG = LoggerFactory.getLogger(ExecutorBasedMessageHandlerInvoker.class); + + protected final ConsumerExecutorFactory consumerExecutorFactory; + + public ExecutorBasedMessageHandlerInvoker(ConsumerExecutorFactory consumerExecutorFactory) { + this.consumerExecutorFactory = consumerExecutorFactory; + } + + @Override + public void execute(MessageHandler messageHandler, Message message, AcknowledgementHandlerInternal acknowledgeHandler) { + MessageProcessingTask task = new MessageProcessingTask(messageHandler, message, acknowledgeHandler); + doSubmitTask(task, message); + LOG.debug("[correlation id: {}] Message has been put in the processing queue.", + message.getCorrelationId()); + } + + protected abstract void doSubmitTask(MessageProcessingTask task, Message message); + +} diff --git a/core/src/main/java/io/github/tcdl/msb/threading/GroupedMessageHandlerInvoker.java b/core/src/main/java/io/github/tcdl/msb/threading/GroupedMessageHandlerInvoker.java new file mode 100644 index 00000000..0e76a0bb --- /dev/null +++ b/core/src/main/java/io/github/tcdl/msb/threading/GroupedMessageHandlerInvoker.java @@ -0,0 +1,46 @@ +package io.github.tcdl.msb.threading; + +import io.github.tcdl.msb.MessageHandler; +import io.github.tcdl.msb.acknowledge.AcknowledgementHandlerInternal; +import io.github.tcdl.msb.api.message.Message; +import org.apache.commons.lang3.RandomUtils; + +import java.util.List; + +/** + * This {@link MessageHandlerInvoker} implementation delegates execution of {@link io.github.tcdl.msb.MessageHandler} + * to one of the provided invokers. Messages with the same group (resolved by {@link MessageGroupStrategy} provided) + * will be processed by the same invoker. + * + * For example, this class can be used to process messages from the same group sequentially by providing a list of + * single-threaded invokers. + */ +public class GroupedMessageHandlerInvoker implements MessageHandlerInvoker { + + private final MessageGroupStrategy messageGroupStrategy; + private final int numberOfInvokers; + private final List invokers; + + public GroupedMessageHandlerInvoker(List invokers, MessageGroupStrategy messageGroupStrategy) { + this.messageGroupStrategy = messageGroupStrategy; + this.numberOfInvokers = invokers.size(); + this.invokers = invokers; + } + + private int getInvokerKey(Message message) { + return messageGroupStrategy.getMessageGroupId(message) + .map(integer -> Math.abs(integer % numberOfInvokers)) + .orElseGet(() -> RandomUtils.nextInt(0, numberOfInvokers)); + } + + @Override + public void execute(MessageHandler messageHandler, Message message, AcknowledgementHandlerInternal acknowledgeHandler) { + int invokerKey = getInvokerKey(message); + invokers.get(invokerKey).execute(messageHandler, message, acknowledgeHandler); + } + + @Override + public void shutdown() { + invokers.forEach(MessageHandlerInvoker::shutdown); + } +} diff --git a/core/src/main/java/io/github/tcdl/msb/threading/MessageGroupStrategy.java b/core/src/main/java/io/github/tcdl/msb/threading/MessageGroupStrategy.java new file mode 100644 index 00000000..c919715d --- /dev/null +++ b/core/src/main/java/io/github/tcdl/msb/threading/MessageGroupStrategy.java @@ -0,0 +1,23 @@ +package io.github.tcdl.msb.threading; + +import io.github.tcdl.msb.api.message.Message; + +import java.util.Optional; + +/** + * Implementations of this interface define a way to resolve a message group by a message. Messages + * with the same message group will be executed by {@link GroupedMessageHandlerInvoker} + * one after another (in a single-threaded mode) + * while messages with different message groups could be executed in parallel. + */ +@FunctionalInterface +public interface MessageGroupStrategy { + /** + * Resolve message group by a message. Message group identifier is any integer. If the message group + * can't be resolved for a particular message, {@link Optional#empty()} should be returned. In this + * case an execution thread will be selected randomly. + * @param message + * @return + */ + Optional getMessageGroupId(Message message); +} \ No newline at end of file diff --git a/core/src/main/java/io/github/tcdl/msb/threading/MessageHandlerInvoker.java b/core/src/main/java/io/github/tcdl/msb/threading/MessageHandlerInvoker.java new file mode 100644 index 00000000..8db2a088 --- /dev/null +++ b/core/src/main/java/io/github/tcdl/msb/threading/MessageHandlerInvoker.java @@ -0,0 +1,31 @@ +package io.github.tcdl.msb.threading; + +import io.github.tcdl.msb.MessageHandler; +import io.github.tcdl.msb.acknowledge.AcknowledgementHandlerInternal; +import io.github.tcdl.msb.api.AcknowledgementHandler; +import io.github.tcdl.msb.api.message.Message; + +/** + * This interface defines a way to invoke {@link MessageHandler} to process a {@link Message} received. + */ +public interface MessageHandlerInvoker { + /** + * Handle an incoming {@link Message} using {@link MessageHandler} provided. After an invocation attempt, one of + * {@link AcknowledgementHandlerInternal} methods should be invoked (depending on result - + * {@link AcknowledgementHandlerInternal#autoConfirm()} should be used the processing was successful): + * it is required to call in order to confirm the message. + * The method should always throw an exception when a message supplied can't be handled + * so there will be no {@link MessageHandler#handleMessage(Message, AcknowledgementHandler)} invocation. + * + * @param messageHandler {@link MessageHandler} instance related to a {@link Message} to be processed. + * @param message {@link Message} to be processed. + * @param acknowledgeHandler acknowledgement handler. + * @throws RuntimeException when a message can't be handled. + */ + void execute(MessageHandler messageHandler, Message message, AcknowledgementHandlerInternal acknowledgeHandler); + + /** + * Perform cleanup on shutdown if required. + */ + void shutdown(); +} diff --git a/core/src/main/java/io/github/tcdl/msb/threading/MessageHandlerInvokerFactory.java b/core/src/main/java/io/github/tcdl/msb/threading/MessageHandlerInvokerFactory.java new file mode 100644 index 00000000..6deccd7a --- /dev/null +++ b/core/src/main/java/io/github/tcdl/msb/threading/MessageHandlerInvokerFactory.java @@ -0,0 +1,22 @@ +package io.github.tcdl.msb.threading; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public interface MessageHandlerInvokerFactory { + + MessageHandlerInvoker createDirectHandlerInvoker(); + + MessageHandlerInvoker createExecutorBasedHandlerInvoker(int numberOfThreads, int queueCapacity); + + default MessageHandlerInvoker createGroupedExecutorBasedHandlerInvoker( + int numberOfThreads, int queueCapacity, MessageGroupStrategy messageGroupStrategy) { + List invokers = IntStream + .range(0, numberOfThreads) + .boxed() + .map(i -> createExecutorBasedHandlerInvoker(1, queueCapacity)) + .collect(Collectors.toList()); + return new GroupedMessageHandlerInvoker<>(invokers, messageGroupStrategy); + } +} diff --git a/core/src/main/java/io/github/tcdl/msb/threading/MessageHandlerInvokerFactoryImpl.java b/core/src/main/java/io/github/tcdl/msb/threading/MessageHandlerInvokerFactoryImpl.java new file mode 100644 index 00000000..89238edd --- /dev/null +++ b/core/src/main/java/io/github/tcdl/msb/threading/MessageHandlerInvokerFactoryImpl.java @@ -0,0 +1,20 @@ +package io.github.tcdl.msb.threading; + +public class MessageHandlerInvokerFactoryImpl implements MessageHandlerInvokerFactory { + + private final ConsumerExecutorFactory consumerExecutorFactory; + + public MessageHandlerInvokerFactoryImpl(ConsumerExecutorFactory consumerExecutorFactory) { + this.consumerExecutorFactory = consumerExecutorFactory; + } + + @Override + public MessageHandlerInvoker createDirectHandlerInvoker() { + return new DirectMessageHandlerInvoker(); + } + + @Override + public MessageHandlerInvoker createExecutorBasedHandlerInvoker(int numberOfThreads, int queueCapacity) { + return new ThreadPoolMessageHandlerInvoker(numberOfThreads, queueCapacity, consumerExecutorFactory); + } +} diff --git a/core/src/main/java/io/github/tcdl/msb/threading/MessageProcessingTask.java b/core/src/main/java/io/github/tcdl/msb/threading/MessageProcessingTask.java new file mode 100644 index 00000000..f1a38604 --- /dev/null +++ b/core/src/main/java/io/github/tcdl/msb/threading/MessageProcessingTask.java @@ -0,0 +1,69 @@ +package io.github.tcdl.msb.threading; + +import io.github.tcdl.msb.MessageHandler; +import io.github.tcdl.msb.acknowledge.AcknowledgementHandlerInternal; +import io.github.tcdl.msb.api.message.Message; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; + +import java.util.Map; + +/** + * {@link MessageProcessingTask} wraps incoming message. + */ +public class MessageProcessingTask implements Runnable { + private static final Logger LOG = LoggerFactory.getLogger(MessageProcessingTask.class); + + final Message message; + final MessageHandler messageHandler; + final AcknowledgementHandlerInternal ackHandler; + final Map mdcLogContextMap; + final boolean mdcLogCopy; + + public MessageProcessingTask( MessageHandler messageHandler, Message message, + AcknowledgementHandlerInternal ackHandler) { + this.message = message; + this.messageHandler = messageHandler; + this.ackHandler = ackHandler; + this.mdcLogContextMap = MDC.getCopyOfContextMap(); + this.mdcLogCopy = mdcLogContextMap != null && !mdcLogContextMap.isEmpty(); + } + + /** + * Passes the message to the configured handler and acknowledges it to AMQP broker. + * IMPORTANT CAVEAT: This task is meant to be run in a thread pool so it should handle all its exceptions carefully. In particular it shouldn't + * throw an exception (because it's going to be swallowed anyway and not printed) + */ + @Override + public void run() { + if(mdcLogCopy) { + MDC.setContextMap(mdcLogContextMap); + } + try { + LOG.debug("[correlation id: {}] Starting message processing", message.getCorrelationId()); + messageHandler.handleMessage(message, ackHandler); + LOG.debug("[correlation id: {}] Message has been processed", message.getCorrelationId()); + ackHandler.autoConfirm(); + } catch (Throwable e) { + LOG.error("[correlation id: {}] Failed to process message", message.getCorrelationId(), e); + ackHandler.autoRetry(); + } finally { + if(mdcLogCopy) { + MDC.clear(); + } + } + } + + public Message getMessage() { + return message; + } + + public MessageHandler getMessageHandler() { + return messageHandler; + } + + public AcknowledgementHandlerInternal getAckHandler() { + return ackHandler; + } +} diff --git a/core/src/main/java/io/github/tcdl/msb/threading/ThreadPoolMessageHandlerInvoker.java b/core/src/main/java/io/github/tcdl/msb/threading/ThreadPoolMessageHandlerInvoker.java new file mode 100644 index 00000000..03490839 --- /dev/null +++ b/core/src/main/java/io/github/tcdl/msb/threading/ThreadPoolMessageHandlerInvoker.java @@ -0,0 +1,37 @@ +package io.github.tcdl.msb.threading; + +import io.github.tcdl.msb.api.message.Message; +import io.github.tcdl.msb.support.Utils; + +import java.util.concurrent.ExecutorService; + +/** + * Concurrent {@link MessageHandlerInvoker} implementation used to invoke all {@link io.github.tcdl.msb.MessageHandler} + * in a single thread pool with a configured number of threads. This approach is effective, but may lead + * to concurrent issues when incoming messages order matters. When facing this kind of issues, + * it is possible either to configure this class to work in a single-threaded mode, + * or use {@link GroupedMessageHandlerInvoker} instead. + */ +public class ThreadPoolMessageHandlerInvoker extends ExecutorBasedMessageHandlerInvoker { + + private final ExecutorService executor; + + public ThreadPoolMessageHandlerInvoker(int numberOfThreads, int queueCapacity, ConsumerExecutorFactory consumerExecutorFactory) { + super(consumerExecutorFactory); + this.executor = consumerExecutorFactory.createConsumerThreadPool(numberOfThreads, queueCapacity); + } + + @Override + protected void doSubmitTask(MessageProcessingTask task, Message message) { + executor.submit(task); + } + + @Override + public void shutdown() { + doShutdown(executor); + } + + protected void doShutdown(ExecutorService executor) { + Utils.gracefulShutdown(executor, "consumer"); + } +} \ No newline at end of file diff --git a/core/src/main/resources/reference.conf b/core/src/main/resources/reference.conf index b41db30e..9513d8e8 100644 --- a/core/src/main/resources/reference.conf +++ b/core/src/main/resources/reference.conf @@ -11,14 +11,33 @@ msbConfig { timerThreadPoolSize = 10 # Enable/disable message validation against json schema - validateMessage = true + validateMessage = false brokerAdapterFactory = "io.github.tcdl.msb.adapters.amqp.AmqpAdapterFactory" + threadingConfig = { + consumerThreadPoolSize = 5 + # -1 means unlimited + consumerThreadPoolQueueCapacity = -1 + } + # Broker Adapter Defaults # AMQP (override values from .conf) brokerConfig = { } + # Mapped Diagnostic Context logging settings + mdcLogging = { + enabled = true + splitTagsBy = ":" + messageKeys = { # Mapped Diagnostic Context keys + messageTags = "msbTags" + correlationId = "msbCorrelationId" + } + } + + requestOptions { + responseTimeout = 5000 + } } diff --git a/core/src/main/resources/schema.js b/core/src/main/resources/schema.json similarity index 68% rename from core/src/main/resources/schema.js rename to core/src/main/resources/schema.json index 6d627bc6..700d8f25 100644 --- a/core/src/main/resources/schema.js +++ b/core/src/main/resources/schema.json @@ -5,16 +5,18 @@ "id": { "type": "string" }, "correlationId": { "type": "string" }, "tags": { - "type": "array", - "items": { - "type": "string" - } + "type": "array", + "items": { + "type": "string" + } }, "topics": { "type": ["object", "null"], "properties": { - "to": { "$ref": "#/definitions/topic" }, - "response": { "$ref": "#/definitions/topic" } + "to": { "$ref": "#/definitions/topic" }, + "response": { "$ref": "#/definitions/topic" }, + "forward": { "$ref": "#/definitions/topic" }, + "routingKey": {"type": "string"} }, "required": ["to"] }, @@ -28,9 +30,19 @@ "serviceDetails": { "$ref": "#/definitions/serviceDetails" } }, "required": ["createdAt"] - } + }, + "ack": { + "type": ["object", "null"], + "properties": { + "responderId": { "type": "string" }, + "responsesRemaining": { "type": "number"}, + "timeoutMs": { "type": ["number", "null"] } + }, + "required": ["responderId", "responsesRemaining"] + }, + "payload": {} }, - "required": ["id", "correlationId", "meta", "ack", "payload"], + "required": ["id", "correlationId", "meta"], "definitions": { "topic": { "type": ["string", "null"], diff --git a/core/src/test/java/io/github/tcdl/msb/ChannelManagerConcurrentTest.java b/core/src/test/java/io/github/tcdl/msb/ChannelManagerConcurrentTest.java index 8a6b4545..599c6cf4 100644 --- a/core/src/test/java/io/github/tcdl/msb/ChannelManagerConcurrentTest.java +++ b/core/src/test/java/io/github/tcdl/msb/ChannelManagerConcurrentTest.java @@ -1,32 +1,30 @@ package io.github.tcdl.msb; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.atLeast; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.timeout; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import java.time.Clock; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - import com.fasterxml.jackson.databind.ObjectMapper; import com.googlecode.junittoolbox.MultithreadingTester; -import io.github.tcdl.msb.api.RequesterResponderIT; -import io.github.tcdl.msb.api.message.Message; +import io.github.tcdl.msb.adapters.AdapterFactory; +import io.github.tcdl.msb.adapters.AdapterFactoryLoader; +import io.github.tcdl.msb.adapters.ConsumerAdapter; +import io.github.tcdl.msb.api.RequestOptions; +import io.github.tcdl.msb.api.ResponderOptions; import io.github.tcdl.msb.collector.CollectorManager; import io.github.tcdl.msb.config.MsbConfig; -import io.github.tcdl.msb.monitor.agent.ChannelMonitorAgent; import io.github.tcdl.msb.support.JsonValidator; import io.github.tcdl.msb.support.TestUtils; +import io.github.tcdl.msb.threading.ConsumerExecutorFactoryImpl; +import io.github.tcdl.msb.threading.MessageHandlerInvoker; +import io.github.tcdl.msb.threading.ThreadPoolMessageHandlerInvoker; import org.junit.Before; import org.junit.Test; +import java.time.Clock; + +import static org.mockito.Mockito.*; + public class ChannelManagerConcurrentTest { private ChannelManager channelManager; - private ChannelMonitorAgent mockChannelMonitorAgent; - private MessageHandler messageHandlerMock; + private AdapterFactory adapterFactory; @Before public void setUp() { @@ -34,21 +32,18 @@ public void setUp() { Clock clock = Clock.systemDefaultZone(); JsonValidator validator = new JsonValidator(); ObjectMapper messageMapper = TestUtils.createMessageMapper(); - this.channelManager = new ChannelManager(msbConfig, clock, validator, messageMapper); - - mockChannelMonitorAgent = mock(ChannelMonitorAgent.class); - channelManager.setChannelMonitorAgent(mockChannelMonitorAgent); - messageHandlerMock = mock(MessageHandler.class); + MessageHandlerInvoker messageHandlerInvoker = new ThreadPoolMessageHandlerInvoker(msbConfig.getConsumerThreadPoolSize(), msbConfig.getConsumerThreadPoolQueueCapacity(), new ConsumerExecutorFactoryImpl()); + adapterFactory = spy(new AdapterFactoryLoader(msbConfig).getAdapterFactory()); + this.channelManager = new ChannelManager(msbConfig, clock, validator, messageMapper, adapterFactory, messageHandlerInvoker); } @Test public void testProducerCachedMultithreadInteraction() { String topic = "topic:test-producer-cached-multithreaded"; - new MultithreadingTester().add(() -> { - channelManager.findOrCreateProducer(topic); - verify(mockChannelMonitorAgent).producerTopicCreated(topic); - }).run(); + new MultithreadingTester().add(() -> {channelManager.findOrCreateProducer(topic, false, RequestOptions.DEFAULTS);}).run(); + + verify(adapterFactory, times(1)).createProducerAdapter(eq(topic), eq(false), eq(RequestOptions.DEFAULTS)); } @Test @@ -56,57 +51,14 @@ public void testConsumerUnsubscribeMultithreadInteraction() { String topic = "topic:test-remove-consumer-multithreaded"; CollectorManager collectorManager = new CollectorManager(topic, channelManager); - channelManager.subscribe(topic, collectorManager); - - new MultithreadingTester().add(() -> { - channelManager.unsubscribe(topic); - verify(mockChannelMonitorAgent, timeout(400)).consumerTopicRemoved(topic); - }).run(); - } - - @Test - public void testPublishMessageInvokesAgentMultithreadInteraction() throws InterruptedException { - String topic = "topic:test-agent-publish-multithreaded"; - int numberOfThreads = 10; - int numberOfInvocationsPerThread = 20; - - Producer producer = channelManager.findOrCreateProducer(topic); - Message message = TestUtils.createSimpleRequestMessage(topic); - - CountDownLatch messagesSent = new CountDownLatch(numberOfThreads * numberOfInvocationsPerThread); - - new MultithreadingTester().numThreads(numberOfThreads).numRoundsPerThread(numberOfInvocationsPerThread).add(() -> { - producer.publish(message); - messagesSent.countDown(); - }).run(); - - assertTrue(messagesSent.await(4000, TimeUnit.MILLISECONDS)); - verify(mockChannelMonitorAgent, atLeast(numberOfThreads * numberOfInvocationsPerThread)).producerMessageSent(topic); - } - - @Test - public void testReceiveMessageInvokesAgentAndEmitsEventMultithreadInteraction() throws InterruptedException { - String topic = "topic:test-agent-consumer-multithreaded"; - int numberOfThreads = 4; - int numberOfInvocationsPerThread = 20; - Producer producer = channelManager.findOrCreateProducer(topic); - Message message = TestUtils.createSimpleRequestMessage(topic); + ConsumerAdapter consumerAdapter = mock(ConsumerAdapter.class); + when(adapterFactory.createConsumerAdapter(eq(topic), eq(true), any(ResponderOptions.class))).thenReturn(consumerAdapter); - CountDownLatch messagesReceived = new CountDownLatch(numberOfThreads * numberOfInvocationsPerThread); + channelManager.subscribeForResponses(topic, collectorManager); - channelManager.findOrCreateProducer(topic); - channelManager.subscribe(topic, - msg -> { - messagesReceived.countDown(); - }); + new MultithreadingTester().add(() -> {channelManager.unsubscribe(topic);}).run(); - new MultithreadingTester().numThreads(numberOfThreads).numRoundsPerThread(numberOfInvocationsPerThread).add(() -> { - producer.publish(message); - }).run(); - - assertTrue(messagesReceived.await(RequesterResponderIT.MESSAGE_TRANSMISSION_TIME, TimeUnit.MILLISECONDS)); - verify(mockChannelMonitorAgent, times(numberOfThreads * numberOfInvocationsPerThread)).consumerMessageReceived(topic); + verify(consumerAdapter, times(1)).unsubscribe(); } - } diff --git a/core/src/test/java/io/github/tcdl/msb/ChannelManagerTest.java b/core/src/test/java/io/github/tcdl/msb/ChannelManagerTest.java index 02274c55..dcc2dc12 100644 --- a/core/src/test/java/io/github/tcdl/msb/ChannelManagerTest.java +++ b/core/src/test/java/io/github/tcdl/msb/ChannelManagerTest.java @@ -1,31 +1,38 @@ package io.github.tcdl.msb; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import javax.xml.ws.Holder; -import java.time.Clock; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - import com.fasterxml.jackson.databind.ObjectMapper; +import io.github.tcdl.msb.adapters.AdapterFactory; +import io.github.tcdl.msb.adapters.AdapterFactoryLoader; +import io.github.tcdl.msb.api.RequestOptions; +import io.github.tcdl.msb.api.ResponderOptions; import io.github.tcdl.msb.api.exception.ConsumerSubscriptionException; import io.github.tcdl.msb.api.message.Message; import io.github.tcdl.msb.config.MsbConfig; -import io.github.tcdl.msb.monitor.agent.ChannelMonitorAgent; import io.github.tcdl.msb.support.JsonValidator; import io.github.tcdl.msb.support.TestUtils; +import io.github.tcdl.msb.threading.ConsumerExecutorFactoryImpl; +import io.github.tcdl.msb.threading.MessageHandlerInvoker; +import io.github.tcdl.msb.threading.ThreadPoolMessageHandlerInvoker; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; + +import javax.xml.ws.Holder; +import java.time.Clock; +import java.util.Collections; +import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.*; public class ChannelManagerTest { private ChannelManager channelManager; - private ChannelMonitorAgent mockChannelMonitorAgent; + + @Rule + public ExpectedException expectedException = ExpectedException.none(); @Before public void setUp() { @@ -33,89 +40,145 @@ public void setUp() { Clock clock = Clock.systemDefaultZone(); JsonValidator validator = new JsonValidator(); ObjectMapper messageMapper = TestUtils.createMessageMapper(); - this.channelManager = new ChannelManager(msbConfig, clock, validator, messageMapper); - mockChannelMonitorAgent = mock(ChannelMonitorAgent.class); - channelManager.setChannelMonitorAgent(mockChannelMonitorAgent); + MessageHandlerInvoker messageHandlerInvoker = new ThreadPoolMessageHandlerInvoker(msbConfig.getConsumerThreadPoolSize(), msbConfig.getConsumerThreadPoolQueueCapacity(), new ConsumerExecutorFactoryImpl()); + + AdapterFactory adapterFactory = new AdapterFactoryLoader(msbConfig).getAdapterFactory(); + this.channelManager = new ChannelManager(msbConfig, clock, validator, messageMapper, adapterFactory, messageHandlerInvoker); } @Test public void testProducerCached() { String topic = "topic:test-producer-cached"; - // Producer was created and monitor agent notified - Producer producer1 = channelManager.findOrCreateProducer(topic); + // Producer was created + Producer producer1 = channelManager.findOrCreateProducer(topic, false, RequestOptions.DEFAULTS); assertNotNull(producer1); - verify(mockChannelMonitorAgent).producerTopicCreated(topic); - // Cached producer was returned and monitor agent wasn't notified - Producer producer2 = channelManager.findOrCreateProducer(topic); + // Cached producer was returned + Producer producer2 = channelManager.findOrCreateProducer(topic, false, RequestOptions.DEFAULTS); assertNotNull(producer2); assertSame(producer1, producer2); - verifyNoMoreInteractions(mockChannelMonitorAgent); } - @Test(expected = ConsumerSubscriptionException.class) - public void testConsumerSubscribeMultipleSameTopic() { - String topic = "topic:test-consumer-cached"; + @Test + public void testMultipleConsumersCantSubscribeOnTheSameTopic() { + String topic = "topic:test-consumer"; - // Consumer was created and monitor agent notified - channelManager.subscribe(topic, message -> {}); - channelManager.subscribe(topic, message -> {}); + // Consumer was created + channelManager.subscribe(topic, (message, acknowledgeHandler) -> {}); + expectedException.expect(ConsumerSubscriptionException.class); + channelManager.subscribe(topic, (message, acknowledgeHandler) -> {}); } @Test - public void testPublishMessageInvokesAgent() { - String topic = "topic:test-agent-publish"; + public void testCantSubscribeOnTheSameTopic() throws Exception { + String topic = "interesting:topic"; + String bindingKey = "routing.key.one"; - Producer producer = channelManager.findOrCreateProducer(topic); - Message message = TestUtils.createSimpleRequestMessage(topic); - producer.publish(message); + ResponderOptions responderOptions1 = new ResponderOptions.Builder().withBindingKeys(Collections.singleton(bindingKey)).build(); + ResponderOptions responderOptions2 = new ResponderOptions.Builder().withBindingKeys(Collections.singleton(bindingKey)).build(); - verify(mockChannelMonitorAgent).producerMessageSent(topic); + channelManager.subscribe(topic, responderOptions1, (message, acknowledgeHandler) -> {}); + expectedException.expect(ConsumerSubscriptionException.class); + channelManager.subscribe(topic, responderOptions2, (message, acknowledgeHandler) -> {}); } @Test - public void testReceiveMessageInvokesAgentAndEmitsEvent() throws InterruptedException { + public void testReceiveMessageInvokesHandler() throws InterruptedException { String topic = "topic:test-agent-consume"; CountDownLatch awaitReceiveEvents = new CountDownLatch(1); final Holder messageEvent = new Holder<>(); Message message = TestUtils.createSimpleRequestMessage(topic); - channelManager.findOrCreateProducer(topic).publish(message); + channelManager.subscribe(topic, - msg -> { + (msg, acknowledgeHandler) -> { messageEvent.value = msg; awaitReceiveEvents.countDown(); }); + channelManager.findOrCreateProducer(topic, false, RequestOptions.DEFAULTS).publish(message); assertTrue(awaitReceiveEvents.await(4000, TimeUnit.MILLISECONDS)); - verify(mockChannelMonitorAgent).consumerMessageReceived(topic); assertNotNull(messageEvent.value); } @Test - public void testSubscribeUnsubscribe() { - String topic = "topic:test-unsubscribe-once"; + public void testAvailableMessageCountInitialized() { + String topic = "some:topic"; + + Optional result = channelManager.getAvailableMessageCount(topic); + + assertEquals(Optional.empty(), result); + } + + @Test + public void testAvailableMessageCountSubscribed() { + String topic = "some:topic"; + + ResponderOptions responderOptions = new ResponderOptions.Builder().build(); + + channelManager.subscribe(topic, responderOptions, (message, acknowledgeHandler) -> {}); + + expectedException.expect(UnsupportedOperationException.class); + channelManager.getAvailableMessageCount(topic); + } + + @Test + public void testAvailableMessageCountUnsubscribed() { + String topic = "some:topic"; + + ResponderOptions responderOptions = new ResponderOptions.Builder().build(); + + channelManager.subscribe(topic, responderOptions, (message, acknowledgeHandler) -> {}); + + expectedException.expect(UnsupportedOperationException.class); + channelManager.getAvailableMessageCount(topic); - channelManager.subscribe(topic, message -> {}); channelManager.unsubscribe(topic); - verify(mockChannelMonitorAgent).consumerTopicRemoved(topic); + Optional result = channelManager.getAvailableMessageCount(topic); + + assertEquals(Optional.empty(), result); } @Test - public void testSubscribeUnsubscribeSeparateTopics() { - String topic1 = "topic:test-unsubscribe-try-first"; - String topic2 = "topic:test-unsubscribe-try-other"; + public void testIsConsumerConnectedInitialized() { + String topic = "some:topic"; - channelManager.subscribe(topic1, message -> {}); - channelManager.subscribe(topic2, message -> {}); + Optional result = channelManager.isConnected(topic); - channelManager.unsubscribe(topic1); - verify(mockChannelMonitorAgent).consumerTopicRemoved(topic1); - verify(mockChannelMonitorAgent, never()).consumerTopicRemoved(topic2); + assertEquals(Optional.empty(), result); } + @Test + public void testConsumerCountSubscribed() { + String topic = "some:topic"; + + ResponderOptions responderOptions = new ResponderOptions.Builder().build(); + + channelManager.subscribe(topic, responderOptions, (message, acknowledgeHandler) -> {}); + + expectedException.expect(UnsupportedOperationException.class); + channelManager.isConnected(topic); + } + + @Test + public void testConsumerCountUnsubscribed() { + String topic = "some:topic"; + + ResponderOptions responderOptions = new ResponderOptions.Builder().build(); + + channelManager.subscribe(topic, responderOptions, (message, acknowledgeHandler) -> {}); + + expectedException.expect(UnsupportedOperationException.class); + channelManager.isConnected(topic); + + channelManager.unsubscribe(topic); + + Optional result = channelManager.getAvailableMessageCount(topic); + + assertEquals(Optional.empty(), result); + } } \ No newline at end of file diff --git a/core/src/test/java/io/github/tcdl/msb/ConsumerTest.java b/core/src/test/java/io/github/tcdl/msb/ConsumerTest.java index 54d15120..5b7b659e 100644 --- a/core/src/test/java/io/github/tcdl/msb/ConsumerTest.java +++ b/core/src/test/java/io/github/tcdl/msb/ConsumerTest.java @@ -2,36 +2,52 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.typesafe.config.ConfigFactory; +import io.github.tcdl.msb.acknowledge.AcknowledgementHandlerInternal; import io.github.tcdl.msb.adapters.ConsumerAdapter; import io.github.tcdl.msb.api.exception.JsonConversionException; import io.github.tcdl.msb.api.message.Message; import io.github.tcdl.msb.api.message.MetaMessage; import io.github.tcdl.msb.api.message.Topics; +import io.github.tcdl.msb.collector.ConsumedMessagesAwareMessageHandler; import io.github.tcdl.msb.config.MsbConfig; -import io.github.tcdl.msb.monitor.agent.ChannelMonitorAgent; import io.github.tcdl.msb.support.JsonValidator; import io.github.tcdl.msb.support.TestUtils; import io.github.tcdl.msb.support.Utils; +import io.github.tcdl.msb.threading.MessageHandlerInvoker; +import org.apache.commons.lang3.StringUtils; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; +import org.slf4j.MDC; import java.time.Clock; import java.time.Instant; import java.time.ZoneId; +import java.util.Map; +import java.util.Optional; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; @RunWith(MockitoJUnitRunner.class) public class ConsumerTest { private static final String TOPIC = "test:consumer"; + private static final String MDC_KEY_TAGS = "msbTags"; + + private static final String MDC_KEY_CORR_ID = "msbCorrelationId"; + + private static final String MDC_SPLIT_KEY = "tagkey"; + + private static final String MDC_SPLIT_BY = ":"; + + private static final String CORRELATION_ID = "34223432423423"; + @Mock private ConsumerAdapter adapterMock; @@ -39,10 +55,22 @@ public class ConsumerTest { private MsbConfig msbConfMock; @Mock - private ChannelMonitorAgent channelMonitorAgentMock; + private MessageHandler messageHandlerMock; @Mock - private MessageHandler messageHandlerMock; + private ConsumedMessagesAwareMessageHandler consumedMessagesAwareMessageHandlerMock; + + @Mock + private MessageHandlerResolver messageHandlerResolverMock; + + @Mock + private MessageHandlerResolver consumedMessagesAwareMessageHandlerResolverMock; + + @Mock + private MessageHandlerInvoker messageHandlerInvokerMock; + + @Mock + private AcknowledgementHandlerInternal acknowledgementHandlerMock; private Clock clock = Clock.systemDefaultZone(); @@ -50,70 +78,141 @@ public class ConsumerTest { private ObjectMapper messageMapper = TestUtils.createMessageMapper(); + @Before + public void setUp() { + when(messageHandlerResolverMock.resolveMessageHandler(any())) + .thenReturn(Optional.of(messageHandlerMock)); + + when(consumedMessagesAwareMessageHandlerResolverMock.resolveMessageHandler(any())) + .thenReturn(Optional.of(consumedMessagesAwareMessageHandlerMock)); + + when(msbConfMock.getMdcLoggingKeyCorrelationId()).thenReturn(MDC_KEY_CORR_ID); + when(msbConfMock.getMdcLoggingKeyMessageTags()).thenReturn(MDC_KEY_TAGS); + when(msbConfMock.isMdcLogging()).thenReturn(true); + when(msbConfMock.getMdcLoggingSplitTagsBy()).thenReturn(MDC_SPLIT_BY); + } + @Test(expected = NullPointerException.class) public void testCreateConsumerNullAdapter() { - new Consumer(null, TOPIC, messageHandlerMock, msbConfMock, clock, channelMonitorAgentMock, validator, messageMapper); + new Consumer(null, messageHandlerInvokerMock, TOPIC, messageHandlerResolverMock, msbConfMock, clock, validator, messageMapper); } @Test(expected = NullPointerException.class) public void testCreateConsumerNullTopic() { - new Consumer(adapterMock, null, messageHandlerMock, msbConfMock, clock, channelMonitorAgentMock, validator, messageMapper); + new Consumer(adapterMock, messageHandlerInvokerMock, null, messageHandlerResolverMock, msbConfMock, clock, validator, messageMapper); } @Test(expected = NullPointerException.class) public void testCreateConsumerNullMessageHandler() { - new Consumer(adapterMock, TOPIC, null, msbConfMock, clock, channelMonitorAgentMock, validator, messageMapper); + new Consumer(adapterMock, messageHandlerInvokerMock, TOPIC, null, msbConfMock, clock, validator, messageMapper); } @Test(expected = NullPointerException.class) public void testCreateConsumerNullMsbConf() { - new Consumer(adapterMock, TOPIC, messageHandlerMock, null, clock, channelMonitorAgentMock, validator, messageMapper); + new Consumer(adapterMock, messageHandlerInvokerMock, TOPIC, messageHandlerResolverMock, null, clock, validator, messageMapper); } @Test(expected = NullPointerException.class) public void testCreateConsumerNullClock() { - new Consumer(adapterMock, TOPIC, messageHandlerMock, msbConfMock, null, channelMonitorAgentMock, validator, messageMapper); - } - - @Test(expected = NullPointerException.class) - public void testCreateConsumerNullMonitorAgent() { - new Consumer(adapterMock, TOPIC, messageHandlerMock, msbConfMock, clock, null, validator, messageMapper); + new Consumer(adapterMock, messageHandlerInvokerMock, TOPIC, messageHandlerResolverMock, msbConfMock, null, validator, messageMapper); } @Test(expected = NullPointerException.class) public void testCreateConsumerNullValidator() { - new Consumer(adapterMock, TOPIC, messageHandlerMock, msbConfMock, clock, channelMonitorAgentMock, null, messageMapper); + new Consumer(adapterMock, messageHandlerInvokerMock, TOPIC, messageHandlerResolverMock, msbConfMock, clock, null, messageMapper); } @Test(expected = NullPointerException.class) public void testCreateConsumerNullMessageMapper() { - new Consumer(adapterMock, TOPIC, messageHandlerMock, msbConfMock, clock, channelMonitorAgentMock, validator, null); + new Consumer(adapterMock, messageHandlerInvokerMock, TOPIC, messageHandlerResolverMock, msbConfMock, clock, validator, null); } @Test - public void testSubscribeAdapterSubscribed() { - new Consumer(adapterMock, TOPIC, messageHandlerMock, msbConfMock, clock, channelMonitorAgentMock, validator, messageMapper); + public void testValidMessageProcessedBySubscriber() throws JsonConversionException { + Message originalMessage = TestUtils.createSimpleRequestMessage(TOPIC); + Consumer consumer = new Consumer(adapterMock, messageHandlerInvokerMock, TOPIC, messageHandlerResolverMock, msbConfMock, clock, validator, messageMapper); + + consumer.handleRawMessage(Utils.toJson(originalMessage, messageMapper), acknowledgementHandlerMock); - verify(adapterMock).subscribe(any(ConsumerAdapter.RawMessageHandler.class)); + verifyMessageHandled(); } @Test - public void testValidMessageProcessedBySubscriber() throws JsonConversionException { + public void testConsumedMessagesAwareMessageHandlerNotifiedWhenMessageHandled() throws JsonConversionException { + Message originalMessage = TestUtils.createSimpleRequestMessage(TOPIC); + Consumer consumer = new Consumer(adapterMock, messageHandlerInvokerMock, TOPIC, consumedMessagesAwareMessageHandlerResolverMock, msbConfMock, clock, validator, messageMapper); + + consumer.handleRawMessage(Utils.toJson(originalMessage, messageMapper), acknowledgementHandlerMock); + + verify(consumedMessagesAwareMessageHandlerMock, times(1)).notifyMessageConsumed(); + } + + @Test + public void testConsumedMessagesAwareMessageHandlerNotifiedWhenMessageLost() throws JsonConversionException { + doThrow(new RuntimeException("Something really unexpected.")).when(messageHandlerInvokerMock).execute(any(), any(), any()); + + Message originalMessage = TestUtils.createSimpleRequestMessage(TOPIC); + Consumer consumer = new Consumer(adapterMock, messageHandlerInvokerMock, TOPIC, consumedMessagesAwareMessageHandlerResolverMock, msbConfMock, clock, validator, messageMapper); + + consumer.handleRawMessage(Utils.toJson(originalMessage, messageMapper), acknowledgementHandlerMock); + + verify(consumedMessagesAwareMessageHandlerMock, times(1)).notifyMessageConsumed(); + verify(consumedMessagesAwareMessageHandlerMock, times(1)).notifyConsumedMessageIsLost(); + } + + @Test + public void testMessageHandlerCantBeResolved() throws JsonConversionException { + when(messageHandlerResolverMock.resolveMessageHandler(any())) + .thenReturn(Optional.empty()); + + Message originalMessage = TestUtils.createSimpleRequestMessage(TOPIC); + Consumer consumer = new Consumer(adapterMock, messageHandlerInvokerMock, TOPIC, messageHandlerResolverMock, msbConfMock, clock, validator, messageMapper); + + consumer.handleRawMessage(Utils.toJson(originalMessage, messageMapper), acknowledgementHandlerMock); + + verifyMessageNotHandled(); + verify(acknowledgementHandlerMock, times(1)).autoReject(); + } + + @Test + public void testMessageHandlerInvokeException() throws JsonConversionException { + doThrow(new RuntimeException("Something really unexpected.")).when(messageHandlerInvokerMock).execute(any(), any(), any()); + Message originalMessage = TestUtils.createSimpleRequestMessage(TOPIC); - Consumer consumer = new Consumer(adapterMock, TOPIC, messageHandlerMock, msbConfMock, clock, channelMonitorAgentMock, validator, messageMapper); + Consumer consumer = new Consumer(adapterMock, messageHandlerInvokerMock, TOPIC, messageHandlerResolverMock, msbConfMock, clock, validator, messageMapper); - consumer.handleRawMessage(Utils.toJson(originalMessage, messageMapper)); + consumer.handleRawMessage(Utils.toJson(originalMessage, messageMapper), acknowledgementHandlerMock); - verify(messageHandlerMock).handleMessage(any(Message.class)); + verify(acknowledgementHandlerMock, times(1)).autoRetry(); } @Test public void testExceptionWhileMessageConvertingProcessedBySubscriber() throws JsonConversionException { - Consumer consumer = new Consumer(adapterMock, TOPIC, messageHandlerMock, msbConfMock, clock, channelMonitorAgentMock, validator, messageMapper); + Consumer consumer = new Consumer(adapterMock, messageHandlerInvokerMock, TOPIC, messageHandlerResolverMock, msbConfMock, clock, validator, messageMapper); + + consumer.handleRawMessage("{\"body\":\"fake message\"}", acknowledgementHandlerMock); + + verifyMessageNotHandled(); + } - consumer.handleRawMessage("{\"body\":\"fake message\"}"); + @Test + public void testMessageCount() { + Consumer consumer = new Consumer(adapterMock, messageHandlerInvokerMock, TOPIC, messageHandlerResolverMock, msbConfMock, clock, validator, messageMapper); + Optional answer = Optional.of(42L); + when(adapterMock.messageCount()).thenReturn(answer); + Optional result = consumer.messageCount(); + verify(adapterMock, times(1)).messageCount(); + assertEquals(answer, result); + } - verify(messageHandlerMock, never()).handleMessage(any(Message.class)); + @Test + public void testIsConsumerConnected() { + Consumer consumer = new Consumer(adapterMock, messageHandlerInvokerMock, TOPIC, messageHandlerResolverMock, msbConfMock, clock, validator, messageMapper); + Optional answer = Optional.of(true); + when(adapterMock.isConnected()).thenReturn(answer); + Optional result = consumer.isConnected(); + verify(adapterMock, times(1)).isConnected(); + assertEquals(answer, result); } @Test @@ -123,53 +222,113 @@ public void testHandleRawMessageConsumeFromTopicSkipValidation() { // disable validation when(msbConf.isValidateMessage()).thenReturn(false); - Consumer consumer = new Consumer(adapterMock, TOPIC, messageHandlerMock, msbConfMock, clock, channelMonitorAgentMock, validator, messageMapper); + Consumer consumer = new Consumer(adapterMock, messageHandlerInvokerMock, TOPIC, messageHandlerResolverMock, msbConfMock, clock, validator, messageMapper); // create a message with required empty namespace Message message = TestUtils.createSimpleRequestMessage(""); - consumer.handleRawMessage(Utils.toJson(message, messageMapper)); + consumer.handleRawMessage(Utils.toJson(message, messageMapper), acknowledgementHandlerMock); // should skip validation and process it - verify(messageHandlerMock).handleMessage(any(Message.class)); + verifyMessageHandled(); } @Test public void testHandleRawMessageConsumeFromTopic() throws JsonConversionException { Message originalMessage = TestUtils.createSimpleRequestMessage(TOPIC); - Consumer consumer = new Consumer(adapterMock, TOPIC, messageHandlerMock, msbConfMock, clock, channelMonitorAgentMock, validator, messageMapper); + Consumer consumer = new Consumer(adapterMock, messageHandlerInvokerMock, TOPIC, messageHandlerResolverMock, msbConfMock, clock, validator, messageMapper); - consumer.handleRawMessage(Utils.toJson(originalMessage, messageMapper)); - verify(messageHandlerMock).handleMessage(any(Message.class)); + consumer.handleRawMessage(Utils.toJson(originalMessage, messageMapper), acknowledgementHandlerMock); + verifyMessageHandled(); } @Test public void testHandleRawMessageConsumeFromTopicValidateThrowException() { MsbConfig msbConf = TestUtils.createMsbConfigurations(); - Consumer consumer = new Consumer(adapterMock, TOPIC, messageHandlerMock, msbConf, clock, channelMonitorAgentMock, validator, messageMapper); + Consumer consumer = new Consumer(adapterMock, messageHandlerInvokerMock, TOPIC, messageHandlerResolverMock, msbConf, clock, validator, messageMapper); - consumer.handleRawMessage("{\"body\":\"fake message\"}"); - verify(messageHandlerMock, never()).handleMessage(any(Message.class)); // no processing + consumer.handleRawMessage("{\"body\":\"fake message\"}", acknowledgementHandlerMock); + verifyMessageNotHandled(); } @Test public void testHandleRawMessageConsumeFromServiceTopicValidateThrowException() { String service_topic = "_service:topic"; MsbConfig msbConf = TestUtils.createMsbConfigurations(); - Consumer consumer = new Consumer(adapterMock, service_topic, messageHandlerMock, msbConf, clock, channelMonitorAgentMock, validator, messageMapper); + Consumer consumer = new Consumer(adapterMock, messageHandlerInvokerMock, service_topic, messageHandlerResolverMock, msbConf, clock, validator, messageMapper); - consumer.handleRawMessage("{\"body\":\"fake message\"}"); - verify(messageHandlerMock, never()).handleMessage(any(Message.class)); // no processing + consumer.handleRawMessage("{\"body\":\"fake message\"}", acknowledgementHandlerMock); + verifyMessageNotHandled(); } @Test public void testHandleRawMessageConsumeFromTopicExpiredMessage() throws JsonConversionException { Message expiredMessage = createExpiredMsbRequestMessageWithTopicTo(TOPIC); - Consumer consumer = new Consumer(adapterMock, TOPIC, messageHandlerMock, msbConfMock, clock, channelMonitorAgentMock, validator, messageMapper); + Consumer consumer = new Consumer(adapterMock, messageHandlerInvokerMock, TOPIC, messageHandlerResolverMock, msbConfMock, clock, validator, messageMapper); + + consumer.handleRawMessage(Utils.toJson(expiredMessage, messageMapper), acknowledgementHandlerMock); + verifyMessageNotHandled(); + } - consumer.handleRawMessage(Utils.toJson(expiredMessage, messageMapper)); - verify(messageHandlerMock, never()).handleMessage(any(Message.class)); + @Test + public void testSaveMdcSuccessWithTagsSplit() throws JsonConversionException { + verifyMdc(true, true); + } + + @Test + public void testSaveMdcSuccessNoTagsSplit() throws JsonConversionException { + when(msbConfMock.getMdcLoggingSplitTagsBy()).thenReturn(""); + verifyMdc(true, false); + + when(msbConfMock.getMdcLoggingSplitTagsBy()).thenReturn(null); + verifyMdc(true, false); + } + + @Test + public void testSaveMdcDisabled() throws JsonConversionException { + when(msbConfMock.isMdcLogging()).thenReturn(false); + verifyMdc(false, false); + } + + private void verifyMdc(boolean isMdcExpected, boolean isSplitExpected) { + String splitTagVal = "tag2" + MDC_SPLIT_BY + "tag2!$#.$#$$#&&**"; + String splitTag = MDC_SPLIT_KEY + MDC_SPLIT_BY + splitTagVal; + Message originalMessage = TestUtils.createMsbRequestMessage( + TOPIC, null, CORRELATION_ID, TestUtils.createSimpleRequestPayload(), "tag1", splitTag, "tag3"); + + MessageHandlerInvoker testInvokeStrategy = new MessageHandlerInvoker() { + + @Override + public void execute(MessageHandler messageHandler, Message message, AcknowledgementHandlerInternal acknowledgeHandler) { + if(isMdcExpected) { + assertEquals("tag1,"+splitTag+",tag3", MDC.get(MDC_KEY_TAGS)); + assertEquals(CORRELATION_ID, MDC.get(MDC_KEY_CORR_ID)); + + } else { + assertTrue(StringUtils.isEmpty(MDC.get(MDC_KEY_TAGS))); + assertTrue(StringUtils.isEmpty(MDC.get(MDC_KEY_CORR_ID))); + + } + + if(isSplitExpected) { + assertEquals(splitTagVal, MDC.get(MDC_SPLIT_KEY)); + } else { + assertTrue(StringUtils.isEmpty(MDC.get(MDC_SPLIT_KEY))); + } + } + + @Override + public void shutdown() { + + } + }; + + Consumer consumer = new Consumer(adapterMock, testInvokeStrategy, TOPIC, messageHandlerResolverMock, msbConfMock, clock, validator, messageMapper); + + consumer.handleRawMessage(Utils.toJson(originalMessage, messageMapper), acknowledgementHandlerMock); + Map map = MDC.getCopyOfContextMap(); + assertTrue("MDC cleanup was expected but was not performed", map == null || map.isEmpty()); } private Message createExpiredMsbRequestMessageWithTopicTo(String topicTo) { @@ -178,9 +337,17 @@ private Message createExpiredMsbRequestMessageWithTopicTo(String topicTo) { MsbConfig msbConf = new MsbConfig(ConfigFactory.load()); Clock clock = Clock.fixed(MOMENT_IN_PAST, ZoneId.systemDefault()); - Topics topic = new Topics(topicTo, topicTo + ":response:" + msbConf.getServiceDetails().getInstanceId()); + Topics topic = new Topics(topicTo, topicTo + ":response:" + msbConf.getServiceDetails().getInstanceId(), null); MetaMessage.Builder metaBuilder = new MetaMessage.Builder(0, clock.instant(), msbConf.getServiceDetails(), clock); return new Message.Builder().withCorrelationId(Utils.generateId()).withId(Utils.generateId()).withTopics(topic).withMetaBuilder(metaBuilder) .build(); } + + private void verifyMessageHandled() { + verify(messageHandlerInvokerMock, times(1)).execute(eq(messageHandlerMock), any(Message.class), eq(acknowledgementHandlerMock)); + } + + private void verifyMessageNotHandled() { + verify(messageHandlerInvokerMock, never()).execute(any(), any(), any()); + } } \ No newline at end of file diff --git a/core/src/test/java/io/github/tcdl/msb/ProducerTest.java b/core/src/test/java/io/github/tcdl/msb/ProducerTest.java index 32a5a864..8c5ec682 100644 --- a/core/src/test/java/io/github/tcdl/msb/ProducerTest.java +++ b/core/src/test/java/io/github/tcdl/msb/ProducerTest.java @@ -14,6 +14,7 @@ import io.github.tcdl.msb.config.MsbConfig; import io.github.tcdl.msb.support.TestUtils; import io.github.tcdl.msb.support.Utils; +import org.apache.commons.lang3.StringUtils; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; @@ -27,6 +28,7 @@ import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -48,33 +50,25 @@ public class ProducerTest { @Test(expected = NullPointerException.class) public void testCreateConsumerProducerNullAdapter() { - new Producer(null, "testTopic", handlerMock, messageMapper); + new Producer(null, "testTopic", messageMapper); } @Test(expected = NullPointerException.class) public void testCreateProducerNullTopic() { - new Producer(adapterMock, null, handlerMock, messageMapper); - } - - @Test(expected = NullPointerException.class) - public void testCreateProducerNullHandler() { - new Producer(adapterMock, "testTopic", null, messageMapper); + new Producer(adapterMock, null, messageMapper); } @Test(expected = NullPointerException.class) public void testCreateProducerNullMapper() { - new Producer(adapterMock, "testTopic", handlerMock, null); + new Producer(adapterMock, "testTopic", null); } @Test - @SuppressWarnings("unchecked") - public void testPublishVerifyHandlerCalled() { + public void testPublishWithDefaultRoutingKey() throws Exception { Message originaMessage = TestUtils.createSimpleRequestMessage(TOPIC); - - Producer producer = new Producer(adapterMock, TOPIC, handlerMock, messageMapper); + Producer producer = new Producer(adapterMock, TOPIC, messageMapper); producer.publish(originaMessage); - - verify(handlerMock).call(any(Message.class)); + verify(adapterMock).publish(anyString(), eq(StringUtils.EMPTY)); } @Test(expected = ChannelException.class) @@ -82,9 +76,9 @@ public void testPublishVerifyHandlerCalled() { public void testPublishRawAdapterThrowChannelException() throws ChannelException { Message originaMessage = TestUtils.createSimpleRequestMessage(TOPIC); - Mockito.doThrow(ChannelException.class).when(adapterMock).publish(anyString()); + Mockito.doThrow(ChannelException.class).when(adapterMock).publish(anyString(), anyString()); - Producer producer = new Producer(adapterMock, TOPIC, handlerMock, messageMapper); + Producer producer = new Producer(adapterMock, TOPIC, messageMapper); producer.publish(originaMessage); verify(handlerMock, never()).call(any(Message.class)); @@ -96,7 +90,7 @@ public void testPublishRawAdapterThrowChannelException() throws ChannelException public void testPublishThrowExceptionVerifyCallbackNotSetNotCalled() throws ChannelException { Message brokenMessage = createBrokenRequestMessageWithAndTopicTo(TOPIC); - Producer producer = new Producer(adapterMock, TOPIC, handlerMock, messageMapper); + Producer producer = new Producer(adapterMock, TOPIC, messageMapper); producer.publish(brokenMessage); verify(adapterMock, never()).publish(anyString()); @@ -108,7 +102,7 @@ private Message createBrokenRequestMessageWithAndTopicTo(String topicTo) { Clock clock = Clock.systemDefaultZone(); ObjectMapper payloadMapper = TestUtils.createMessageMapper(); - Topics topic = new Topics(topicTo, topicTo + ":response:" + msbConf.getServiceDetails().getInstanceId()); + Topics topic = new Topics(topicTo, topicTo + ":response:" + msbConf.getServiceDetails().getInstanceId(), null); Map body = new HashMap<>(); body.put("body", "{\\\"x\\\" : 3} garbage"); RestPayload> payload = new RestPayload.Builder>() diff --git a/core/src/test/java/io/github/tcdl/msb/RunOnShutdownScheduledExecutorDecoratorTest.java b/core/src/test/java/io/github/tcdl/msb/RunOnShutdownScheduledExecutorDecoratorTest.java index 8aeed975..454b070b 100644 --- a/core/src/test/java/io/github/tcdl/msb/RunOnShutdownScheduledExecutorDecoratorTest.java +++ b/core/src/test/java/io/github/tcdl/msb/RunOnShutdownScheduledExecutorDecoratorTest.java @@ -6,15 +6,13 @@ import org.junit.Test; import org.mockito.Mockito; +import java.util.concurrent.CancellationException; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.timeout; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; public class RunOnShutdownScheduledExecutorDecoratorTest { @@ -56,18 +54,36 @@ public void testShutdownWithCancelledTask() { } @Test(timeout = 20000) - public void testShutdownWithCompletedTask() { + public void testShutdownWithCompletedTask() throws Exception { Runnable mockCompletedRunnable = mock(Runnable.class); - executorDecorator.schedule(mockCompletedRunnable, TIME_IMMEDIATE, TimeUnit.SECONDS); + ScheduledFuture scheduleCompleted = executorDecorator.schedule(mockCompletedRunnable, TIME_IMMEDIATE, TimeUnit.SECONDS); verify(mockCompletedRunnable, timeout(1000).times(1)).run(); Runnable mockRunnable = mock(Runnable.class); - executorDecorator.schedule(mockRunnable, TIME_FAR_FUTURE, TimeUnit.SECONDS); + ScheduledFuture scheduleFuture =executorDecorator.schedule(mockRunnable, TIME_FAR_FUTURE, TimeUnit.SECONDS); + + assertFalse(scheduleCompleted.isCancelled()); + assertTrue(scheduleCompleted.isDone()); + + assertFalse(scheduleFuture.isCancelled()); + assertFalse(scheduleFuture.isDone()); + + assertNull(scheduleCompleted.get()); executorDecorator.shutdown(); verify(mockRunnable, times(1)).run(); verify(mockCompletedRunnable, times(1)).run(); // verify the task is not invoked again + + assertTrue(scheduleFuture.isCancelled()); + assertTrue(scheduleFuture.isDone()); + + try { + scheduleFuture.get(); + fail("CancellationException is expected"); + } catch (CancellationException ex) { + //ok + } } @Test(timeout = 20000) diff --git a/core/src/test/java/io/github/tcdl/msb/acknowledge/AcknowledgementHandlerImplTest.java b/core/src/test/java/io/github/tcdl/msb/acknowledge/AcknowledgementHandlerImplTest.java new file mode 100644 index 00000000..1f28700f --- /dev/null +++ b/core/src/test/java/io/github/tcdl/msb/acknowledge/AcknowledgementHandlerImplTest.java @@ -0,0 +1,259 @@ +package io.github.tcdl.msb.acknowledge; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; +import java.util.stream.IntStream; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + + +@RunWith(MockitoJUnitRunner.class) +public class AcknowledgementHandlerImplTest { + + private AcknowledgementHandlerImpl handler; + + @Mock + private AcknowledgementAdapter acknowledgementAdapter; + + @Before + public void setUp() { + handler = getHandler(false); + } + + @Test + public void testMessageConfirmed() throws Exception { + handler.confirmMessage(); + verifySingleConfirm(); + } + + @Test + public void testMessageRejected() throws Exception { + handler.rejectMessage(); + verifySingleReject(); + } + + @Test + public void testMessageRequeued() throws Exception { + handler.retryMessage(); + verifySingleRetry(); + } + + @Test + public void testMessageConfirmedWhenAutoAcknowledgementDisabled() throws Exception { + handler.setAutoAcknowledgement(false); + handler.confirmMessage(); + verifySingleConfirm(); + } + + @Test + public void testMessageRejectedWhenAutoAcknowledgementDisabled() throws Exception { + handler.setAutoAcknowledgement(false); + handler.rejectMessage(); + verifySingleReject(); + } + + @Test + public void testMessageRequeuedWhenAutoAcknowledgementDisabled() throws Exception { + handler.setAutoAcknowledgement(false); + handler.retryMessage(); + verifySingleRetry(); + } + + @Test + public void testAutoAcknowledgementChanged() throws Exception { + assertTrue(handler.isAutoAcknowledgement()); + handler.setAutoAcknowledgement(false); + assertFalse(handler.isAutoAcknowledgement()); + } + + @Test + public void testRedeliveredMessageRejected() throws Exception { + handler = getHandler(true); + handler.rejectMessage(); + verifySingleReject(); + } + + @Test + public void testAutoRedeliveredMessageRejectedInsteadOfRetry() throws Exception { + handler = getHandler(true); + handler.autoRetry(); + verifySingleReject(); + } + + @Test + public void testManuallyRetriedMessageNotRejected() throws Exception { + handler = getHandler(true); + handler.retryMessage(); + verifySingleRetry(); + } + + @Test + public void testConditionallyRetriedMessageRetried() throws Exception { + handler = getHandler(false); + handler.retryMessageFirstTime(); + verifySingleRetry(); + } + + @Test + public void testConditionallyRetriedRedeliveredMessageRejected() throws Exception { + handler = getHandler(true); + handler.retryMessageFirstTime(); + verifySingleReject(); + } + + @Test + public void testRedeliveredMessageConfirmed() throws Exception { + handler = getHandler(true); + handler.confirmMessage(); + verifySingleConfirm(); + } + + @Test + public void testOnlyFirstRejectInvoked() throws Exception { + handler.rejectMessage(); + verifySingleReject(); + submitMultipleConfirmRejectRequests(); + verifySingleReject(); + } + + @Test + public void testOnlyFirstRetryInvoked() throws Exception { + handler.retryMessage(); + verifySingleRetry(); + submitMultipleConfirmRejectRequests(); + verifySingleRetry(); + } + + @Test + public void testOnlyFirstConfirmInvoked() throws Exception { + handler.confirmMessage(); + verifySingleConfirm(); + submitMultipleConfirmRejectRequests(); + verifySingleConfirm(); + } + + @Test + public void testAutoConfirmConfirmsMessageOnce() throws Exception { + handler.autoConfirm(); + verifySingleConfirm(); + submitMultipleAutoConfirmAutoRejectRequests(); + verifySingleConfirm(); + } + + @Test + public void testAutoRejectRejectsMessageOnce() throws Exception { + handler.autoReject(); + verifySingleReject(); + submitMultipleAutoConfirmAutoRejectRequests(); + verifySingleReject(); + } + + @Test + public void testAutoRetryRequeueMessageOnce() throws Exception { + handler.autoRetry(); + verifySingleRetry(); + submitMultipleAutoConfirmAutoRejectRequests(); + verifySingleRetry(); + } + + @Test + public void testAutoConfirmIgnoredWhenAutoAcknowledgementDisabled() throws Exception { + handler.setAutoAcknowledgement(false); + handler.autoConfirm(); + verifyNoMoreInteractions(acknowledgementAdapter); + } + + @Test + public void testAutoRejectIgnoredWhenAutoAcknowledgementDisabled() throws Exception { + handler.setAutoAcknowledgement(false); + handler.autoReject(); + verifyNoMoreInteractions(acknowledgementAdapter); + } + + @Test + public void testAutoRetryIgnoredWhenAutoAcknowledgementDisabled() throws Exception { + handler.setAutoAcknowledgement(false); + handler.autoRetry(); + verifyNoMoreInteractions(acknowledgementAdapter); + } + + @Test + public void testAutoRetryRejectRedeliveredMessageOnce() throws Exception { + handler = getHandler(true); + handler.autoRetry(); + verifySingleReject(); + submitMultipleAutoConfirmAutoRejectRequests(); + verifySingleReject(); + } + + @Test + public void testAutoConfirmIgnoredWhenConfirmedByClient() throws Exception { + handler.confirmMessage(); + verifySingleConfirm(); + handler.autoConfirm(); + verifySingleConfirm(); + } + + @Test + public void testAutoRejectIgnoredWhenConfirmedByClient() throws Exception { + handler.confirmMessage(); + verifySingleConfirm(); + handler.autoReject(); + verifySingleConfirm(); + } + + @Test + public void testAutoConfirmIgnoredWhenRetryByClient() throws Exception { + handler.retryMessage(); + verifySingleRetry(); + handler.autoConfirm(); + verifySingleRetry(); + } + + @Test + public void testAutoRejectIgnoredWhenRejectedByClient() throws Exception { + handler.rejectMessage(); + verifySingleReject(); + handler.autoReject(); + verifySingleReject(); + } + + private void verifySingleConfirm() throws Exception { + verify(acknowledgementAdapter, times(1)).confirm(); + verifyNoMoreInteractions(acknowledgementAdapter); + } + + private void verifySingleRetry() throws Exception { + verify(acknowledgementAdapter, times(1)).retry(); + verifyNoMoreInteractions(acknowledgementAdapter); + } + + private void verifySingleReject() throws Exception { + verify(acknowledgementAdapter, times(1)).reject(); + verifyNoMoreInteractions(acknowledgementAdapter); + } + + private void submitMultipleConfirmRejectRequests() { + IntStream.range(0, 5).forEach((i) -> { + handler.confirmMessage(); + handler.retryMessage(); + handler.rejectMessage(); + }); + } + + private void submitMultipleAutoConfirmAutoRejectRequests() { + IntStream.range(0, 5).forEach((i) -> { + handler.autoReject(); + handler.autoRetry(); + handler.autoConfirm(); + }); + } + + private AcknowledgementHandlerImpl getHandler(boolean isMessageRedelivered) { + return new AcknowledgementHandlerImpl(acknowledgementAdapter, isMessageRedelivered, "id = 123"); + } + +} diff --git a/core/src/test/java/io/github/tcdl/msb/adapters/AdapterFactoryLoaderTest.java b/core/src/test/java/io/github/tcdl/msb/adapters/AdapterFactoryLoaderTest.java index 81d17bc2..f6a315fa 100644 --- a/core/src/test/java/io/github/tcdl/msb/adapters/AdapterFactoryLoaderTest.java +++ b/core/src/test/java/io/github/tcdl/msb/adapters/AdapterFactoryLoaderTest.java @@ -4,37 +4,46 @@ import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import io.github.tcdl.msb.adapters.mock.MockAdapterFactory; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + import io.github.tcdl.msb.config.MsbConfig; +import io.github.tcdl.msb.mock.adapterfactory.TestMsbAdapterFactory; +import org.junit.Before; import org.junit.Test; import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; +@RunWith(MockitoJUnitRunner.class) public class AdapterFactoryLoaderTest { - private String basicConfigWithoutAdapterFactory = "msbConfig { %s timerThreadPoolSize = 1, validateMessage = true, " - + " serviceDetails = {name = \"test_msb\", version = \"1.0.1\", instanceId = \"msbd06a-ed59-4a39-9f95-811c5fb6ab87\"} }"; - + private static final Config CONFIG = ConfigFactory.load("reference.conf"); + + private MsbConfig msbConfigSpy; + + @Before + public void setUp() { + msbConfigSpy = spy(new MsbConfig(CONFIG)); + } + @Test public void testCreatedMockAdapterByFactoryClassName(){ - String configStr = String.format(basicConfigWithoutAdapterFactory, "brokerAdapterFactory = \"io.github.tcdl.msb.adapters.mock.MockAdapterFactory\","); - Config config = ConfigFactory.parseString(configStr); - MsbConfig msbConfig = new MsbConfig(config); - AdapterFactoryLoader loader = new AdapterFactoryLoader(msbConfig); + when(msbConfigSpy.getBrokerAdapterFactory()).thenReturn("io.github.tcdl.msb.mock.adapterfactory.TestMsbAdapterFactory"); + AdapterFactoryLoader loader = new AdapterFactoryLoader(msbConfigSpy); AdapterFactory adapterFactory = loader.getAdapterFactory(); - assertThat(adapterFactory, instanceOf(MockAdapterFactory.class)); + assertThat(adapterFactory, instanceOf(TestMsbAdapterFactory.class)); } @Test public void testThrowExceptionByNonexistentFactoryClassName(){ //Define Nonexistent AdapterFactory class name String nonexistentAdapterFactoryClassName = "io.github.tcdl.msb.adapters.NonexistentAdapterFactory"; - String configStr = String.format(basicConfigWithoutAdapterFactory, "brokerAdapterFactory = \"" + nonexistentAdapterFactoryClassName + "\", "); - Config config = ConfigFactory.parseString(configStr); - MsbConfig msbConfig = new MsbConfig(config); - AdapterFactoryLoader loader = new AdapterFactoryLoader(msbConfig); + when(msbConfigSpy.getBrokerAdapterFactory()).thenReturn(nonexistentAdapterFactoryClassName); + AdapterFactoryLoader loader = new AdapterFactoryLoader(msbConfigSpy); try { loader.getAdapterFactory(); fail("Created an AdapterFactory by nonexistent class!"); @@ -48,10 +57,8 @@ public void testThrowExceptionByNonexistentFactoryClassName(){ public void testThrowExceptionByIncorrectAdapterFactoryConstructor(){ //Define AdapterFactory class name with a class without default constructor String adapterFactoryClassNameWithoutDefaultConstructor = "java.lang.Integer"; - String configStr = String.format(basicConfigWithoutAdapterFactory, "brokerAdapterFactory = \"" + adapterFactoryClassNameWithoutDefaultConstructor + "\", "); - Config config = ConfigFactory.parseString(configStr); - MsbConfig msbConfig = new MsbConfig(config); - AdapterFactoryLoader loader = new AdapterFactoryLoader(msbConfig); + when(msbConfigSpy.getBrokerAdapterFactory()).thenReturn(adapterFactoryClassNameWithoutDefaultConstructor); + AdapterFactoryLoader loader = new AdapterFactoryLoader(msbConfigSpy); try { loader.getAdapterFactory(); fail("Created an AdapterFactory by class without default constructor!"); @@ -65,10 +72,8 @@ public void testThrowExceptionByIncorrectAdapterFactoryConstructor(){ public void testThrowExceptionByIncorrectAdapterFactoryInterfaceImplementation(){ //Define AdapterFactory class name with a class that doesn't implement AdapterFactory interface String incorrectAdapterFactoryImplementationClassName = "java.lang.StringBuilder"; - String configStr = String.format(basicConfigWithoutAdapterFactory, "brokerAdapterFactory = \"" + incorrectAdapterFactoryImplementationClassName + "\", "); - Config config = ConfigFactory.parseString(configStr); - MsbConfig msbConfig = new MsbConfig(config); - AdapterFactoryLoader loader = new AdapterFactoryLoader(msbConfig); + when(msbConfigSpy.getBrokerAdapterFactory()).thenReturn(incorrectAdapterFactoryImplementationClassName); + AdapterFactoryLoader loader = new AdapterFactoryLoader(msbConfigSpy); try { loader.getAdapterFactory(); fail("Created an AdapterFactory by class that doesn't implement AdapterFactory interface!"); diff --git a/core/src/test/java/io/github/tcdl/msb/adapters/mock/MockAdapterFactoryTest.java b/core/src/test/java/io/github/tcdl/msb/adapters/mock/MockAdapterFactoryTest.java deleted file mode 100644 index 3ba24d87..00000000 --- a/core/src/test/java/io/github/tcdl/msb/adapters/mock/MockAdapterFactoryTest.java +++ /dev/null @@ -1,44 +0,0 @@ -package io.github.tcdl.msb.adapters.mock; - -import static org.hamcrest.core.IsInstanceOf.instanceOf; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; - -import io.github.tcdl.msb.adapters.AdapterFactory; -import io.github.tcdl.msb.adapters.ConsumerAdapter; -import io.github.tcdl.msb.adapters.ProducerAdapter; -import org.junit.Test; - -/** - * MockAdapterFactory is an implementation of {@link AdapterFactory} - * for {@link MockAdapterTest} - */ -public class MockAdapterFactoryTest { - - @Test - public void testCreateConsumerAdapter() { - MockAdapterFactory mockAdapterFactory = new MockAdapterFactory(); - ConsumerAdapter consumer = mockAdapterFactory.createConsumerAdapter(""); - assertThat(consumer, instanceOf(MockAdapter.class)); - assertTrue(mockAdapterFactory.consumerExecutors.size() == 1); - } - - @Test - public void testCreateProducerAdapter() { - MockAdapterFactory mockAdapterFactory = new MockAdapterFactory(); - ProducerAdapter producer = mockAdapterFactory.createProducerAdapter(""); - assertThat(producer, instanceOf(MockAdapter.class)); - assertTrue(mockAdapterFactory.consumerExecutors.size() == 0); - } - - @Test - public void testShutdown() { - MockAdapterFactory mockAdapterFactory = new MockAdapterFactory(); - mockAdapterFactory.createConsumerAdapter(""); - assertTrue(mockAdapterFactory.consumerExecutors.size() == 1); - mockAdapterFactory.shutdown(); - assertTrue(mockAdapterFactory.consumerExecutors.size() == 0); - - } - -} diff --git a/core/src/test/java/io/github/tcdl/msb/adapters/mock/MockAdapterTest.java b/core/src/test/java/io/github/tcdl/msb/adapters/mock/MockAdapterTest.java deleted file mode 100644 index 39f3542e..00000000 --- a/core/src/test/java/io/github/tcdl/msb/adapters/mock/MockAdapterTest.java +++ /dev/null @@ -1,76 +0,0 @@ -package io.github.tcdl.msb.adapters.mock; - -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.timeout; -import static org.mockito.Mockito.verify; -import java.util.LinkedList; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.ExecutorService; - -import com.fasterxml.jackson.databind.ObjectMapper; -import io.github.tcdl.msb.adapters.ConsumerAdapter; -import io.github.tcdl.msb.adapters.ProducerAdapter; -import io.github.tcdl.msb.api.exception.ChannelException; -import io.github.tcdl.msb.api.exception.JsonConversionException; -import io.github.tcdl.msb.api.message.Message; -import io.github.tcdl.msb.support.TestUtils; -import io.github.tcdl.msb.support.Utils; -import org.junit.Test; - -/** - * MockAdapter class represents implementation of {@link ProducerAdapter} and {@link ConsumerAdapter} - * for test purposes. - * - */ -public class MockAdapterTest { - - private ObjectMapper messageMapper = TestUtils.createMessageMapper(); - - @Test - public void testPublishAddMessageToMap() throws ChannelException, JsonConversionException { - String topic = "test:mock-adapter-publish"; - Message message = TestUtils.createSimpleRequestMessage(topic); - MockAdapter mockAdapter = new MockAdapter(topic); - - mockAdapter.publish(Utils.toJson(message, messageMapper)); - - assertNotNull(mockAdapter.messageMap.get(topic).poll()); - } - - @Test - public void testSubscribeCallMessageHandler() throws ChannelException, JsonConversionException { - String topic = "test:mock-adapter-subscribe"; - String message = Utils.toJson(TestUtils.createSimpleRequestMessage(topic), messageMapper); - Queue activeConsumerExecutors = new LinkedList<>(); - MockAdapter mockAdapter = new MockAdapter(topic, activeConsumerExecutors); - Queue messages = new ConcurrentLinkedQueue<>(); - messages.add(message); - mockAdapter.messageMap.put(topic, messages); - ConsumerAdapter.RawMessageHandler mockHandler = mock(ConsumerAdapter.RawMessageHandler.class); - - mockAdapter.subscribe(mockHandler); - - assertTrue(activeConsumerExecutors.size() == 1); - verify(mockHandler, timeout(500)).onMessage(eq(message)); - } - - @Test - public void testUnsubscribe() throws ChannelException, JsonConversionException { - String topic = "test:mock-adapter-unsubscribe"; - Queue activeConsumerExecutors = new LinkedList<>(); - MockAdapter mockAdapter = new MockAdapter(topic, activeConsumerExecutors); - - ConsumerAdapter.RawMessageHandler mockHandler = mock(ConsumerAdapter.RawMessageHandler.class); - mockAdapter.subscribe(mockHandler); - - assertTrue(activeConsumerExecutors.size() == 1); - mockAdapter.unsubscribe(); - assertTrue(activeConsumerExecutors.size() == 0); - - } - -} diff --git a/core/src/test/java/io/github/tcdl/msb/api/ChannelMonitorIT.java b/core/src/test/java/io/github/tcdl/msb/api/ChannelMonitorIT.java deleted file mode 100644 index 804e2ac4..00000000 --- a/core/src/test/java/io/github/tcdl/msb/api/ChannelMonitorIT.java +++ /dev/null @@ -1,181 +0,0 @@ -package io.github.tcdl.msb.api; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import java.time.Instant; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -import io.github.tcdl.msb.adapters.mock.MockAdapter; -import io.github.tcdl.msb.api.message.Message; -import io.github.tcdl.msb.api.message.payload.RestPayload; -import io.github.tcdl.msb.api.monitor.AggregatorStats; -import io.github.tcdl.msb.api.monitor.ChannelMonitorAggregator; -import io.github.tcdl.msb.impl.MsbContextImpl; -import io.github.tcdl.msb.monitor.agent.AgentTopicStats; -import io.github.tcdl.msb.monitor.agent.ChannelMonitorAgent; -import io.github.tcdl.msb.monitor.agent.DefaultChannelMonitorAgent; -import io.github.tcdl.msb.support.TestUtils; -import io.github.tcdl.msb.support.Utils; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -public class ChannelMonitorIT { - - private static final Instant LAST_PRODUCED_TIME = Instant.parse("2007-12-03T15:15:30.00Z"); - private static final Instant LAST_CONSUMED_TIME = Instant.parse("2007-12-03T17:15:30.00Z"); - - private static final int HEARTBEAT_TIMEOUT_MS = 2000; - - MsbContextImpl msbContext; - ChannelMonitorAggregator channelMonitorAggregator; - - @Before - public void setUp() { - msbContext = TestUtils.createSimpleMsbContext(); - } - - @After - public void tearDown() { - channelMonitorAggregator.stop(); - } - - @Test - public void testAnnouncement() throws InterruptedException { - String TOPIC_NAME = "topic1"; - CountDownLatch announcementReceived = monitorPrepareAwaitOnAnnouncement(TOPIC_NAME); - - ChannelMonitorAgent channelMonitorAgent = new DefaultChannelMonitorAgent(msbContext); - channelMonitorAgent.producerTopicCreated(TOPIC_NAME); - - assertTrue("Announcement was not received", announcementReceived.await(RequesterResponderIT.MESSAGE_TRANSMISSION_TIME, TimeUnit.MILLISECONDS)); - } - - @Test - public void testAnnouncementUnexpectedMessage() throws InterruptedException { - String TOPIC_NAME = "topic2"; - CountDownLatch announcementReceived = monitorPrepareAwaitOnAnnouncement(TOPIC_NAME); - - //simulate broken announcement in broker - MockAdapter.pushRequestMessage(Utils.TOPIC_ANNOUNCE, - Utils.toJson(TestUtils.createSimpleRequestMessage(Utils.TOPIC_ANNOUNCE), msbContext.getPayloadMapper())); - - assertFalse("Broken announcement was handled", announcementReceived.await(RequesterResponderIT.MESSAGE_TRANSMISSION_TIME / 2, TimeUnit.MILLISECONDS)); - - //verify next correct announcement was handled - ChannelMonitorAgent channelMonitorAgent = new DefaultChannelMonitorAgent(msbContext); - channelMonitorAgent.producerTopicCreated(TOPIC_NAME); - - assertTrue("Announcement was not received", announcementReceived.await(RequesterResponderIT.MESSAGE_TRANSMISSION_TIME, TimeUnit.MILLISECONDS)); - } - - private CountDownLatch monitorPrepareAwaitOnAnnouncement(String topicName) throws InterruptedException { - CountDownLatch announcementReceived = new CountDownLatch(1); - Callback handler = stats -> { - assertTrue(stats.getTopicInfoMap().containsKey(topicName)); - assertEquals(1, stats.getTopicInfoMap().get(topicName).getProducers().size()); - announcementReceived.countDown(); - }; - - channelMonitorAggregator = msbContext.getObjectFactory().createChannelMonitorAggregator(handler); - channelMonitorAggregator.start(false, ChannelMonitorAggregator.DEFAULT_HEARTBEAT_INTERVAL_MS, HEARTBEAT_TIMEOUT_MS); - - return announcementReceived; - } - - @Test - public void testHeartbeatMessage() throws InterruptedException { - String TOPIC_NAME = "topic3"; - - Map topicInfoMap = new HashMap<>(); - topicInfoMap.put(TOPIC_NAME, new AgentTopicStats(true, false, LAST_PRODUCED_TIME, LAST_CONSUMED_TIME)); - - RestPayload payload = new RestPayload.Builder>() - .withBody(topicInfoMap) - .build(); - - CountDownLatch heartBeatResponseReceived = new CountDownLatch(1); - Callback handler = stats -> { - assertTrue(stats.getTopicInfoMap().containsKey(TOPIC_NAME)); - assertEquals(1, stats.getTopicInfoMap().get(TOPIC_NAME).getProducers().size()); - heartBeatResponseReceived.countDown(); - }; - - channelMonitorAggregator = msbContext.getObjectFactory().createChannelMonitorAggregator(handler); - channelMonitorAggregator.start(true, ChannelMonitorAggregator.DEFAULT_HEARTBEAT_INTERVAL_MS, HEARTBEAT_TIMEOUT_MS); - - //need to await for original request for heartbeat to be send to simulate response with same correlationId - Message requestMessage = awaitHeartBeatRequestSent(); - - Message responseMessage = TestUtils.createMsbRequestMessageWithCorrelationId(requestMessage.getTopics().getResponse(), - requestMessage.getCorrelationId(), - payload); - MockAdapter.pushRequestMessage(requestMessage.getTopics().getResponse(), Utils.toJson(responseMessage, msbContext.getPayloadMapper())); - - assertTrue("Heartbeat response was not received", - heartBeatResponseReceived.await(HEARTBEAT_TIMEOUT_MS * 2, TimeUnit.MILLISECONDS)); - } - - @Test - public void testHeartbeatUnexpectedMessage() throws InterruptedException { - String TOPIC_NAME = "topic4"; - - Map topicInfoMap = new HashMap<>(); - topicInfoMap.put(TOPIC_NAME, new AgentTopicStats(true, false, LAST_PRODUCED_TIME, LAST_CONSUMED_TIME)); - - RestPayload payload = new RestPayload.Builder>() - .withBody(topicInfoMap) - .build(); - - CountDownLatch heartBeatResponseReceived = new CountDownLatch(1); - Callback handler = stats -> { - assertEquals(1, stats.getTopicInfoMap().size()); - heartBeatResponseReceived.countDown(); - }; - - channelMonitorAggregator = msbContext.getObjectFactory().createChannelMonitorAggregator(handler); - channelMonitorAggregator.start(true, ChannelMonitorAggregator.DEFAULT_HEARTBEAT_INTERVAL_MS, HEARTBEAT_TIMEOUT_MS); - - //need to await for original request for heartbeat to be send to simulate response with same correlationId - Message requestMessage = awaitHeartBeatRequestSent(); - - Message brokenResponseMessage = TestUtils.createMsbRequestMessageWithCorrelationId(requestMessage.getTopics().getResponse(), - requestMessage.getCorrelationId(), - " unexpected statistics format received"); - Message responseMessage = TestUtils - .createMsbRequestMessageWithCorrelationId(requestMessage.getTopics().getResponse(), requestMessage.getCorrelationId(), - payload); - //simulate 3 heartbeatResponses: 1 valid and 2 broken - MockAdapter.pushRequestMessage(requestMessage.getTopics().getResponse(), Utils.toJson(brokenResponseMessage, msbContext.getPayloadMapper())); - MockAdapter.pushRequestMessage(requestMessage.getTopics().getResponse(), Utils.toJson(responseMessage, msbContext.getPayloadMapper())); - MockAdapter.pushRequestMessage(requestMessage.getTopics().getResponse(), Utils.toJson(brokenResponseMessage, msbContext.getPayloadMapper())); - - assertTrue("Heartbeat response was not received", - heartBeatResponseReceived.await(HEARTBEAT_TIMEOUT_MS * 2, TimeUnit.MILLISECONDS)); - } - - private Message awaitHeartBeatRequestSent() throws InterruptedException { - //need to await for original request for heartbeat to be send to simulate response with same correlationId - CountDownLatch awaitRequestMessage = new CountDownLatch(1); - List outgoingRequestMessages = new LinkedList<>(); - msbContext.getChannelManager().subscribe(Utils.TOPIC_HEARTBEAT, message -> { - outgoingRequestMessages.add(message); - awaitRequestMessage.countDown(); - }); - - //fail the test if not able to get original heartbeat request - assertTrue("Heartbeat original request not captured", - awaitRequestMessage.await(HEARTBEAT_TIMEOUT_MS, TimeUnit.MILLISECONDS)); - - //unsubscribe or else will consume messages from previous run - msbContext.getChannelManager().unsubscribe(Utils.TOPIC_HEARTBEAT); - return outgoingRequestMessages.get(0); - } - -} diff --git a/core/src/test/java/io/github/tcdl/msb/api/RequestOptionsTest.java b/core/src/test/java/io/github/tcdl/msb/api/RequestOptionsTest.java index 61e4a894..4cf76511 100644 --- a/core/src/test/java/io/github/tcdl/msb/api/RequestOptionsTest.java +++ b/core/src/test/java/io/github/tcdl/msb/api/RequestOptionsTest.java @@ -1,6 +1,7 @@ package io.github.tcdl.msb.api; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; import org.junit.Test; @@ -12,7 +13,7 @@ public void testGetWaitForResponsesConfigsNull() { .withWaitForResponses(null) .build(); - assertEquals("expect 0 if MessageOptions.waitForResponses is null", Integer.valueOf(0), requestOptions.getWaitForResponses()); + assertEquals( RequestOptions.WAIT_FOR_RESPONSES_UNTIL_TIMEOUT, requestOptions.getWaitForResponses()); } @Test @@ -21,16 +22,57 @@ public void testGetWaitForResponsesConfigsMinusOne() { .withWaitForResponses(-1) .build(); - assertEquals("expect -1 if MessageOptions.waitForResponses is -1", Integer.valueOf(-1), requestOptions.getWaitForResponses()); + assertEquals(RequestOptions.WAIT_FOR_RESPONSES_UNTIL_TIMEOUT, requestOptions.getWaitForResponses()); } @Test public void testGetWaitForResponsesConfigsPositive() { + int responsesRemaining = 100; RequestOptions requestOptions = new RequestOptions.Builder() - .withWaitForResponses(100) + .withWaitForResponses(responsesRemaining) .build(); - assertEquals("expect 100 if MessageOptions.waitForResponses is 100", Integer.valueOf(100), requestOptions.getWaitForResponses()); + assertEquals(responsesRemaining, requestOptions.getWaitForResponses()); } -} + @Test + public void testForwardNamespace() { + String forwardNamespace = "test:forward"; + RequestOptions requestOptions = new RequestOptions.Builder() + .withForwardNamespace(forwardNamespace) + .build(); + + assertEquals(forwardNamespace, requestOptions.getForwardNamespace()); + } + + @Test + public void testBuilderFromExistingRequestOptions() throws Exception { + + MessageTemplate sourceMessageTemplate = new MessageTemplate(); + + Integer ackTimeout = 1; + Integer responseTimeout = 2; + String forwardNamespace = "forward:namespace"; + int waitForResponses = 3; + String routingKey = "routing.key"; + + RequestOptions source = new RequestOptions.Builder() + .withAckTimeout(ackTimeout) + .withResponseTimeout(responseTimeout) + .withForwardNamespace(forwardNamespace) + .withWaitForResponses(waitForResponses) + .withMessageTemplate(sourceMessageTemplate) + .withRoutingKey(routingKey) + .build(); + + RequestOptions.Builder builder = new RequestOptions.Builder().from(source); + RequestOptions result = builder.build(); + + assertEquals(ackTimeout, result.getAckTimeout()); + assertEquals(responseTimeout, result.getResponseTimeout()); + assertEquals(waitForResponses, result.getWaitForResponses()); + assertEquals(forwardNamespace, result.getForwardNamespace()); + assertSame(sourceMessageTemplate, result.getMessageTemplate()); + assertEquals(routingKey, result.getRoutingKey()); + } +} \ No newline at end of file diff --git a/core/src/test/java/io/github/tcdl/msb/api/RequesterIT.java b/core/src/test/java/io/github/tcdl/msb/api/RequesterIT.java index 2587bf2a..61692111 100644 --- a/core/src/test/java/io/github/tcdl/msb/api/RequesterIT.java +++ b/core/src/test/java/io/github/tcdl/msb/api/RequesterIT.java @@ -3,11 +3,12 @@ import com.fasterxml.jackson.databind.JsonNode; import java.util.Base64; -import io.github.tcdl.msb.adapters.mock.MockAdapter; import io.github.tcdl.msb.api.message.Message; import io.github.tcdl.msb.api.message.payload.RestPayload; import io.github.tcdl.msb.impl.MsbContextImpl; import io.github.tcdl.msb.support.TestUtils; +import io.github.tcdl.msb.mock.adapterfactory.TestMsbStorageForAdapterFactory; +import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -21,11 +22,13 @@ public class RequesterIT { private RequestOptions requestOptions; private MsbContextImpl msbContext; + private TestMsbStorageForAdapterFactory storage; @Before public void setUp() throws Exception { this.requestOptions = TestUtils.createSimpleRequestOptionsWithTags(STATIC_TAG); this.msbContext = TestUtils.createSimpleMsbContext(); + storage = TestMsbStorageForAdapterFactory.extract(msbContext); } @Test @@ -34,7 +37,7 @@ public void testRequestMessage() throws Exception { Requester requester = msbContext.getObjectFactory().createRequester(NAMESPACE, requestOptions); requester.publish(requestPayload); - String adapterJsonMessage = MockAdapter.pollJsonMessageForTopic(NAMESPACE); + String adapterJsonMessage = storage.getOutgoingMessage(NAMESPACE); TestUtils.assertRequestMessagePayload(adapterJsonMessage, requestPayload, NAMESPACE); } @@ -45,7 +48,7 @@ public void testRequestMessageWithBodyBufferBase64Encoded() throws Exception { Requester requester = msbContext.getObjectFactory().createRequester(NAMESPACE, requestOptions); requester.publish(requestPayload); - String adapterJsonMessage = MockAdapter.pollJsonMessageForTopic(NAMESPACE); + String adapterJsonMessage = storage.getOutgoingMessage(NAMESPACE); JsonNode jsonObject = msbContext.getPayloadMapper().readTree(adapterJsonMessage); String base64Encoded = Base64.getEncoder().encodeToString(bytesToSend); @@ -59,7 +62,7 @@ public void testRequestMessageWithDynamicTag() throws Exception { Requester requester = msbContext.getObjectFactory().createRequester(NAMESPACE, requestOptions); requester.publish(requestPayload, dynamicTag); - String adapterJsonMessage = MockAdapter.pollJsonMessageForTopic(NAMESPACE); + String adapterJsonMessage = storage.getOutgoingMessage(NAMESPACE); TestUtils.assertRequestMessagePayload(adapterJsonMessage, requestPayload, NAMESPACE); TestUtils.assertMessageTags(adapterJsonMessage, STATIC_TAG, dynamicTag); } @@ -73,7 +76,7 @@ public void testRequestMessageWithDynamicTagAndOriginalMessage() throws Exceptio Requester requester = msbContext.getObjectFactory().createRequester(NAMESPACE, requestOptions); requester.publish(requestPayload, originalMessage, dynamicTag); - String adapterJsonMessage = MockAdapter.pollJsonMessageForTopic(NAMESPACE); + String adapterJsonMessage = storage.getOutgoingMessage(NAMESPACE); TestUtils.assertRequestMessagePayload(adapterJsonMessage, requestPayload, NAMESPACE); TestUtils.assertMessageTags(adapterJsonMessage, dynamicTagOriginal, STATIC_TAG, dynamicTag); } @@ -87,7 +90,7 @@ public void testRequestMessageWithDuplicateTagInOriginalMessage() throws Excepti Requester requester = msbContext.getObjectFactory().createRequester(NAMESPACE, requestOptions); requester.publish(requestPayload, originalMessage, dynamicTag); - String adapterJsonMessage = MockAdapter.pollJsonMessageForTopic(NAMESPACE); + String adapterJsonMessage = storage.getOutgoingMessage(NAMESPACE); TestUtils.assertRequestMessagePayload(adapterJsonMessage, requestPayload, NAMESPACE); TestUtils.assertMessageTags(adapterJsonMessage, dynamicTagOriginal, STATIC_TAG, dynamicTag); } diff --git a/core/src/test/java/io/github/tcdl/msb/api/RequesterResponderIT.java b/core/src/test/java/io/github/tcdl/msb/api/RequesterResponderIT.java index 3b154678..afc7aaa7 100644 --- a/core/src/test/java/io/github/tcdl/msb/api/RequesterResponderIT.java +++ b/core/src/test/java/io/github/tcdl/msb/api/RequesterResponderIT.java @@ -1,27 +1,28 @@ package io.github.tcdl.msb.api; -import com.fasterxml.jackson.databind.JsonNode; -import io.github.tcdl.msb.adapters.mock.MockAdapter; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import com.fasterxml.jackson.core.type.TypeReference; import io.github.tcdl.msb.api.message.Acknowledge; import io.github.tcdl.msb.api.message.payload.RestPayload; import io.github.tcdl.msb.impl.MsbContextImpl; import io.github.tcdl.msb.support.TestUtils; import io.github.tcdl.msb.support.Utils; -import org.junit.Before; -import org.junit.Test; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Random; -import java.util.Set; +import java.util.*; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import io.github.tcdl.msb.mock.adapterfactory.TestMsbStorageForAdapterFactory; +import org.apache.commons.lang3.StringUtils; +import org.junit.Before; +import org.junit.Test; + +import com.fasterxml.jackson.databind.JsonNode; public class RequesterResponderIT { @@ -34,10 +35,12 @@ public class RequesterResponderIT { public static final int MESSAGE_ROUNDTRIP_TRANSMISSION_TIME = MESSAGE_TRANSMISSION_TIME * 2; private MsbContextImpl msbContext; + private TestMsbStorageForAdapterFactory storage; @Before public void setUp() throws Exception { - this.msbContext = TestUtils.createSimpleMsbContext(); + msbContext = TestUtils.createSimpleMsbContext(); + storage = TestMsbStorageForAdapterFactory.extract(msbContext); } @Test @@ -61,6 +64,25 @@ public void testResponderServerReceiveCustomPayloadMessageSendByRequester() thro assertTrue("Message was not received", requestReceived.await(MESSAGE_TRANSMISSION_TIME, TimeUnit.MILLISECONDS)); } + @Test + public void testResponderServerReceiveNullPayloadMessageSendByRequester() throws Exception { + String namespace = "test:requester-responder-test-null-request-received"; + RequestOptions requestOptions = TestUtils.createSimpleRequestOptions(); + CountDownLatch requestReceived = new CountDownLatch(1); + + //Create and send request message + Requester requester = msbContext.getObjectFactory().createRequester(namespace, requestOptions); + + msbContext.getObjectFactory().createResponderServer(namespace, requestOptions.getMessageTemplate(), (request, response) -> { + requestReceived.countDown(); + assertNull(request); + }, String.class).listen(); + + requester.publish(null); + + assertTrue("Message was not received", requestReceived.await(MESSAGE_TRANSMISSION_TIME, TimeUnit.MILLISECONDS)); + } + @Test public void testResponderAnswerWithAckRequesterReceiveAck() throws Exception { String namespace = "test:requester-responder-test-get-ack"; @@ -75,23 +97,25 @@ public void testResponderAnswerWithAckRequesterReceiveAck() throws Exception { List receivedResponseAcks = new LinkedList<>(); + //listen for message and send ack + MsbContextImpl serverMsbContext = TestUtils.createSimpleMsbContext(); + storage.connect(serverMsbContext); + + serverMsbContext.getObjectFactory().createResponderServer(namespace, messageTemplate, (request, responderContext) -> { + responderContext.getResponder().sendAck(100, 2); + ackSend.countDown(); + }) + .listen(); + //Create and send request message directly to broker, wait for ack RestPayload requestPayload = TestUtils.createSimpleRequestPayload(); msbContext.getObjectFactory().createRequester(namespace, requestOptions). - onAcknowledge((Acknowledge ack) -> { - receivedResponseAcks.add(ack); + onAcknowledge((ackMessage, ackHandler) -> { + receivedResponseAcks.add(ackMessage); ackResponseReceived.countDown(); }) .publish(requestPayload); - //listen for message and send ack - MsbContextImpl serverMsbContext = TestUtils.createSimpleMsbContext(); - serverMsbContext.getObjectFactory().createResponderServer(namespace, messageTemplate, (request, response) -> { - response.sendAck(100, 2); - ackSend.countDown(); - }) - .listen(); - assertTrue("Message ack was not send", ackSend.await(MESSAGE_TRANSMISSION_TIME, TimeUnit.MILLISECONDS)); assertTrue("Message ack response not received", ackResponseReceived.await(MESSAGE_ROUNDTRIP_TRANSMISSION_TIME, TimeUnit.MILLISECONDS)); assertTrue("Expected one ack", receivedResponseAcks.size() == 1); @@ -113,23 +137,25 @@ public void testResponderAnswerWithResponseRequesterReceiveCustomPayloadResponse String requestPayload = "request payload"; String responsePayload = "response payload"; + + //listen for message and send response + MsbContextImpl serverMsbContext = TestUtils.createSimpleMsbContext(); + storage.connect(serverMsbContext); + + serverMsbContext.getObjectFactory().createResponderServer(namespace, messageTemplate, (request, responderContext) -> { + responderContext.getResponder().send(responsePayload); + respSent.countDown(); + }, String.class).listen(); + //Create and send request message directly to broker, wait for response msbContext.getObjectFactory().createRequester(namespace, requestOptions, String.class) - .onResponse(payload -> { + .onResponse((payload, ackHandler) -> { receivedResponses.add(payload); respReceived.countDown(); assertEquals(responsePayload, payload); }) .publish(requestPayload); - //listen for message and send response - MsbContextImpl serverMsbContext = TestUtils.createSimpleMsbContext(); - - serverMsbContext.getObjectFactory().createResponderServer(namespace, messageTemplate, (request, response) -> { - response.send(responsePayload); - respSent.countDown(); - }, String.class).listen(); - assertTrue("Message response was not send", respSent.await(MESSAGE_TRANSMISSION_TIME, TimeUnit.MILLISECONDS)); assertTrue("Message response not received", respReceived.await(MESSAGE_ROUNDTRIP_TRANSMISSION_TIME, TimeUnit.MILLISECONDS)); assertTrue("Expected one response", receivedResponses.size() == 1); @@ -151,24 +177,27 @@ public void testResponderCommunicationWithAck() throws Exception { CountDownLatch ackReceived = new CountDownLatch(1); MsbContextImpl serverOneMsbContext = TestUtils.createSimpleMsbContext(); + storage.connect(serverOneMsbContext); serverOneMsbContext.getObjectFactory().createResponderServer(namespace1, responderServerOneMessageOptions, (request, response) -> { //Create and send request message, wait for ack Requester requester = msbContext.getObjectFactory().createRequester(namespace2, requestAwaitAckMessageOptions); RestPayload requestPayload = TestUtils.createSimpleRequestPayload(); - requester.onAcknowledge((Acknowledge a) -> ackReceived.countDown()); + requester.onAcknowledge((ackMessage, achHandler) -> ackReceived.countDown()); requester.publish(requestPayload); }) .listen(); MsbContextImpl serverTwoMsbContext = TestUtils.createSimpleMsbContext(); - serverTwoMsbContext.getObjectFactory().createResponderServer(namespace2, responderServerTwoMessageOptions, (request, response) -> { - response.sendAck(100, 2); - ackSent.countDown(); - }) + storage.connect(serverTwoMsbContext); + serverTwoMsbContext.getObjectFactory().createResponderServer(namespace2, responderServerTwoMessageOptions, + (request, responderContext) -> { + responderContext.getResponder().sendAck(100, 2); + ackSent.countDown(); + }) .listen(); - MockAdapter.pushRequestMessage(namespace1, + storage.publishIncomingMessage(namespace1, StringUtils.EMPTY, Utils.toJson(TestUtils.createSimpleRequestMessage(namespace1), msbContext.getPayloadMapper())); assertTrue("Message ack was not send", ackSent.await(MESSAGE_TRANSMISSION_TIME, TimeUnit.MILLISECONDS)); @@ -199,7 +228,7 @@ public void testMultipleRequesterListenForAcks() throws Exception { while (messagesToSend.get() > 0) { msbContext.getObjectFactory().createRequester(namespace, requestOptions). - onAcknowledge((Acknowledge ack) -> { + onAcknowledge((ack, ackHanlder) -> { receivedResponseAcks.add(ack); ackResponseReceived.countDown(); }) @@ -208,19 +237,22 @@ public void testMultipleRequesterListenForAcks() throws Exception { } }); - publishingThread.setDaemon(true); - publishingThread.start(); //listen for message and send ack MsbContextImpl serverMsbContext = TestUtils.createSimpleMsbContext(); + storage.connect(serverMsbContext); + Random randomAckValue = new Random(); - randomAckValue.ints(); - serverMsbContext.getObjectFactory().createResponderServer(namespace, requestOptions.getMessageTemplate(), (request, response) -> { - response.sendAck(randomAckValue.nextInt(), randomAckValue.nextInt()); - ackSend.countDown(); - }) + + serverMsbContext.getObjectFactory().createResponderServer(namespace, requestOptions.getMessageTemplate(), + (request, responderContext) -> { + responderContext.getResponder().sendAck(randomAckValue.nextInt(), randomAckValue.nextInt()); + ackSend.countDown(); + }) .listen(); + publishingThread.start(); + assertTrue("Message ack was not send", ackSend.await(MESSAGE_TRANSMISSION_TIME, TimeUnit.MILLISECONDS)); assertTrue("Message ack response not received", ackResponseReceived.await(MESSAGE_ROUNDTRIP_TRANSMISSION_TIME, TimeUnit.MILLISECONDS)); assertTrue("Expected one ack", receivedResponseAcks.size() == requestsToSendDuringTest); @@ -235,15 +267,21 @@ public void testRequestMessageCollectorUnsubscribeAfterResponsesAndSubscribeAgai .withWaitForResponses(1) .build(); + final CountDownLatch daemonListens = new CountDownLatch(1); + Thread serverListenThread = new Thread(() -> { msbContext.getObjectFactory().createResponderServer(namespace, requestOptionsWaitResponse.getMessageTemplate(), - (request, response) -> response.send("payload from test : testRequestMessageCollectorUnsubscribeAfterResponsesAndSubscribeAgain") + (request, responderContext) -> + responderContext.getResponder().send("payload from test : testRequestMessageCollectorUnsubscribeAfterResponsesAndSubscribeAgain") ) .listen(); + daemonListens.countDown(); }); serverListenThread.setDaemon(true); serverListenThread.start(); + daemonListens.await(5000, TimeUnit.MILLISECONDS); + CountDownLatch endConversation1 = new CountDownLatch(1); CountDownLatch endConversation2 = new CountDownLatch(1); diff --git a/core/src/test/java/io/github/tcdl/msb/api/ResponderIT.java b/core/src/test/java/io/github/tcdl/msb/api/ResponderIT.java index 219a4162..13214acd 100644 --- a/core/src/test/java/io/github/tcdl/msb/api/ResponderIT.java +++ b/core/src/test/java/io/github/tcdl/msb/api/ResponderIT.java @@ -1,8 +1,10 @@ package io.github.tcdl.msb.api; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.github.tcdl.msb.adapters.mock.MockAdapter; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import io.github.tcdl.msb.api.exception.JsonSchemaValidationException; import io.github.tcdl.msb.api.message.Message; import io.github.tcdl.msb.api.message.payload.RestPayload; @@ -10,18 +12,18 @@ import io.github.tcdl.msb.impl.ResponderImpl; import io.github.tcdl.msb.support.JsonValidator; import io.github.tcdl.msb.support.TestUtils; + +import java.io.IOException; + +import io.github.tcdl.msb.mock.adapterfactory.TestMsbStorageForAdapterFactory; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; public class ResponderIT { @@ -32,14 +34,18 @@ public class ResponderIT { private MsbContextImpl msbContext; private JsonValidator validator; private ObjectMapper payloadMapper; + private TestMsbStorageForAdapterFactory storage; @Before public void setUp() throws Exception { msbContext = (MsbContextImpl) new MsbContextBuilder().build(); + storage = TestMsbStorageForAdapterFactory.extract(msbContext); validator = new JsonValidator(); payloadMapper = msbContext.getPayloadMapper(); } + + @Test public void testCreateAckMessage() throws Exception { String namespace = "test:responder-ack"; @@ -52,7 +58,7 @@ public void testCreateAckMessage() throws Exception { responder.sendAck(ackTimeout, responsesRemaining); - String adapterJsonMessage = MockAdapter.pollJsonMessageForTopic(originalMessage.getTopics().getResponse()); + String adapterJsonMessage = storage.getOutgoingMessage(originalMessage.getTopics().getResponse()); assertNotNull("Ack message shouldn't be null", adapterJsonMessage); assertAckMessage(adapterJsonMessage, ackTimeout, responsesRemaining, originalMessage.getTopics().getResponse()); @@ -93,7 +99,7 @@ public void testCreateResponseMessage() throws Exception { RestPayload responsePayload = TestUtils.createSimpleResponsePayload(); responder.send(responsePayload); - String adapterJsonMessage = MockAdapter.pollJsonMessageForTopic(originalMessage.getTopics().getResponse()); + String adapterJsonMessage = storage.getOutgoingMessage(originalMessage.getTopics().getResponse()); assertNotNull("Response message shouldn't be null", adapterJsonMessage); TestUtils.assertResponseMessagePayload(adapterJsonMessage, responsePayload, originalMessage.getTopics().getResponse()); @@ -110,7 +116,7 @@ public void testCreateResponseMessageWithTags() throws Exception { RestPayload responsePayload = TestUtils.createSimpleResponsePayload(); responder.send(responsePayload); - String adapterJsonMessage = MockAdapter.pollJsonMessageForTopic(originalMessage.getTopics().getResponse()); + String adapterJsonMessage = storage.getOutgoingMessage(originalMessage.getTopics().getResponse()); assertNotNull("Response message shouldn't be null", adapterJsonMessage); TestUtils.assertResponseMessagePayload(adapterJsonMessage, responsePayload, originalMessage.getTopics().getResponse()); diff --git a/core/src/test/java/io/github/tcdl/msb/api/message/MetaMessageTest.java b/core/src/test/java/io/github/tcdl/msb/api/message/MetaMessageTest.java new file mode 100644 index 00000000..5c6f4fa5 --- /dev/null +++ b/core/src/test/java/io/github/tcdl/msb/api/message/MetaMessageTest.java @@ -0,0 +1,23 @@ +package io.github.tcdl.msb.api.message; + +import static org.junit.Assert.assertTrue; +import java.time.Clock; + +import com.typesafe.config.ConfigFactory; +import io.github.tcdl.msb.config.MsbConfig; +import org.junit.Test; + +/** + * Created by ruslan on 17.12.15. + */ +public class MetaMessageTest { + + private Clock clock = Clock.systemDefaultZone(); + + @Test + public void testDurationIsPositivValue() { + MsbConfig msbConf = new MsbConfig(ConfigFactory.load()); + MetaMessage metaMessage = new MetaMessage.Builder(0, clock.instant().minusMillis(1), msbConf.getServiceDetails(), clock).build(); + assertTrue(metaMessage.getDurationMs() > 0); + } +} diff --git a/core/src/test/java/io/github/tcdl/msb/callback/MutableCallbackHandlerTest.java b/core/src/test/java/io/github/tcdl/msb/callback/MutableCallbackHandlerTest.java new file mode 100644 index 00000000..ea2a0853 --- /dev/null +++ b/core/src/test/java/io/github/tcdl/msb/callback/MutableCallbackHandlerTest.java @@ -0,0 +1,57 @@ +package io.github.tcdl.msb.callback; + +import org.assertj.core.api.exception.RuntimeIOException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@RunWith(MockitoJUnitRunner.class) +public class MutableCallbackHandlerTest { + + @Mock + private Runnable callback1; + + @Mock + private Runnable callback2; + + @Mock + private Runnable callback3; + + @Mock + private Runnable callback4; + + @InjectMocks + private MutableCallbackHandler handler; + + @Test + public void testSuccess() throws Exception { + + doThrow(RuntimeIOException.class).when(callback1).run(); + + handler.add(callback1); + handler.add(callback2); + handler.add(callback3); + + for(int i=0; i < 11; i++) { + handler.add(callback4); + } + + handler.runCallbacks(); + + handler.remove(callback2); + + handler.runCallbacks(); + + verify(callback1, times(2)).run(); + verify(callback2, times(1)).run(); + verify(callback3, times(2)).run(); + verify(callback4, times(2)).run(); + } + +} \ No newline at end of file diff --git a/core/src/test/java/io/github/tcdl/msb/collector/CollectorManagerTest.java b/core/src/test/java/io/github/tcdl/msb/collector/CollectorManagerTest.java index 90763758..4445a726 100644 --- a/core/src/test/java/io/github/tcdl/msb/collector/CollectorManagerTest.java +++ b/core/src/test/java/io/github/tcdl/msb/collector/CollectorManagerTest.java @@ -1,21 +1,25 @@ package io.github.tcdl.msb.collector; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import io.github.tcdl.msb.ChannelManager; +import io.github.tcdl.msb.MessageHandler; import io.github.tcdl.msb.api.message.Message; import io.github.tcdl.msb.support.TestUtils; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; -import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import java.util.Optional; @RunWith(MockitoJUnitRunner.class) public class CollectorManagerTest { @@ -39,9 +43,9 @@ public void testHandleMessageRegisteredCollectorForTopic() { when(collectorMock.getRequestMessage()).thenReturn(originalAndReceivedMessage); CollectorManager collectorManager = new CollectorManager(TOPIC, channelManagerMock); collectorManager.registerCollector(collectorMock); - collectorManager.handleMessage(originalAndReceivedMessage); + Optional resolved = collectorManager.resolveMessageHandler(originalAndReceivedMessage); - verify(collectorMock).handleMessage(originalAndReceivedMessage); + assertEquals(resolved.get(), collectorMock); } @Test @@ -49,9 +53,9 @@ public void testHandleMessageRegisteredCollectorForTopicUnexpectedCorrelationId( Message receivedMessage = TestUtils.createSimpleRequestMessage(TOPIC); CollectorManager collectorManager = new CollectorManager(TOPIC, channelManagerMock); collectorManager.registerCollector(collectorMock); - collectorManager.handleMessage(receivedMessage); + Optional resolved = collectorManager.resolveMessageHandler(receivedMessage); - verify(collectorMock, never()).handleMessage(receivedMessage); + assertFalse(resolved.isPresent()); } @Test @@ -61,9 +65,9 @@ public void testHandleMessageUnregisteredProperCollectorForTopic() { CollectorManager collectorManager = new CollectorManager("some-other-topic", channelManagerMock); collectorManager.registerCollector(collectorMock); - collectorManager.handleMessage(receivedMessage); + Optional resolved = collectorManager.resolveMessageHandler(receivedMessage); - verify(collectorMock, never()).handleMessage(receivedMessage); + assertFalse(resolved.isPresent()); } @Test @@ -72,7 +76,7 @@ public void testRegisterCollector() { collectorManager.registerCollector(collectorMock); assertEquals(1, collectorManager.collectorsByCorrelationId.size()); - verify(channelManagerMock, times(1)).subscribe(TOPIC, collectorManager); + verify(channelManagerMock, times(1)).subscribeForResponses(TOPIC, collectorManager); } @Test @@ -85,7 +89,7 @@ public void testRegisterMultipleCollectors() { collectorManager.registerCollector(secondCollectorMock); assertEquals(2, collectorManager.collectorsByCorrelationId.size()); - verify(channelManagerMock, times(1)).subscribe(TOPIC, collectorManager); + verify(channelManagerMock, times(1)).subscribeForResponses(TOPIC, collectorManager); } @Test @@ -93,7 +97,7 @@ public void testRegisterTheSameCollectorMultiplyTimes() { CollectorManager collectorManager = new CollectorManager(TOPIC, channelManagerMock); collectorManager.registerCollector(collectorMock); - verify(channelManagerMock, times(1)).subscribe(TOPIC, collectorManager); + verify(channelManagerMock, times(1)).subscribeForResponses(TOPIC, collectorManager); reset(channelManagerMock); collectorManager.registerCollector(collectorMock); @@ -112,7 +116,7 @@ public void testUnregisterMoreCollectorsExist() { verify(channelManagerMock, never()).unsubscribe(TOPIC); collectorManager.unregisterCollector(secondCollectorMock); - verify(channelManagerMock).unsubscribe(TOPIC); + verify(channelManagerMock, never()).unsubscribe(TOPIC); } @Test @@ -127,7 +131,7 @@ public void testUnregisterLastCollector() { collectorManager.unregisterCollector(collectorMock); collectorManager.unregisterCollector(secondCollectorMock); - verify(channelManagerMock).unsubscribe(TOPIC); + verify(channelManagerMock, never()).unsubscribe(TOPIC); } @Test @@ -136,9 +140,6 @@ public void testUnregisterTheSameCollectorsMultiplyTimes() { collectorManager.registerCollector(collectorMock); collectorManager.unregisterCollector(collectorMock); - verify(channelManagerMock, times(1)).unsubscribe(TOPIC); - - reset(channelManagerMock); collectorManager.unregisterCollector(collectorMock); verify(channelManagerMock, never()).unsubscribe(TOPIC); } @@ -149,11 +150,11 @@ public void testRegisterCollectorAfterUnregisterLast() { collectorManager.registerCollector(collectorMock); collectorManager.unregisterCollector(collectorMock); - verify(channelManagerMock, times(1)).unsubscribe(TOPIC); + verify(channelManagerMock, never()).unsubscribe(TOPIC); reset(channelManagerMock); collectorManager.registerCollector(collectorMock); - verify(channelManagerMock, times(1)).subscribe(TOPIC, collectorManager); + verify(channelManagerMock, never()).subscribeForResponses(TOPIC, collectorManager); } } diff --git a/core/src/test/java/io/github/tcdl/msb/collector/CollectorTest.java b/core/src/test/java/io/github/tcdl/msb/collector/CollectorTest.java index 51be49a7..f28a0f50 100644 --- a/core/src/test/java/io/github/tcdl/msb/collector/CollectorTest.java +++ b/core/src/test/java/io/github/tcdl/msb/collector/CollectorTest.java @@ -1,10 +1,19 @@ package io.github.tcdl.msb.collector; -import com.fasterxml.jackson.core.type.TypeReference; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.junit.Assert.*; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.*; + import io.github.tcdl.msb.ChannelManager; +import io.github.tcdl.msb.api.AcknowledgementHandler; import io.github.tcdl.msb.api.Callback; +import io.github.tcdl.msb.api.MessageContext; import io.github.tcdl.msb.api.RequestOptions; -import io.github.tcdl.msb.api.exception.JsonConversionException; import io.github.tcdl.msb.api.message.Acknowledge; import io.github.tcdl.msb.api.message.Message; import io.github.tcdl.msb.api.message.payload.RestPayload; @@ -14,30 +23,27 @@ import io.github.tcdl.msb.message.MessageFactory; import io.github.tcdl.msb.support.TestUtils; import io.github.tcdl.msb.support.Utils; + +import java.io.IOException; +import java.time.Clock; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.LongAdder; +import java.util.function.BiConsumer; +import java.util.stream.IntStream; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; -import java.io.IOException; -import java.time.Clock; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyInt; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -/** - * Created by rdro on 4/27/2015. - */ @RunWith(MockitoJUnitRunner.class) public class CollectorTest { @@ -70,12 +76,15 @@ public class CollectorTest { @Mock private CollectorManager collectorManagerMock; + + @Mock + private MessageContext messageContextMock; private MsbContextImpl msbContext; @Before public void setUp() throws IOException { - msbContext = TestUtils.createMsbContextBuilder() + this.msbContext = TestUtils.createMsbContextBuilder() .withMsbConfigurations(msbConfigurationsMock) .withMessageFactory(messageFactoryMock) .withChannelManager(channelManagerMock) @@ -85,110 +94,143 @@ public void setUp() throws IOException { .build(); when(collectorManagerFactoryMock.findOrCreateCollectorManager(TOPIC)).thenReturn(collectorManagerMock); + } @Test - public void testGetWaitForResponsesConfigsReturnFalse() { - when(requestOptionsMock.getWaitForResponses()).thenReturn(0); - Collector collector = new Collector<>(TOPIC, originalMessage, requestOptionsMock, msbContext, eventHandlers, new TypeReference() {}); - assertFalse("expect false if MessageOptions.waitForResponses equals 0", collector.isAwaitingResponses()); + public void testIsAwaitingResponsesConfigsReturnMinusOne() { + when(requestOptionsMock.getWaitForResponses()).thenReturn(-1); + Collector collector = createCollector(); + + assertTrue("expect true if MessageOptions.waitForResponses equals -1", collector.isAwaitingResponses()); } @Test - public void testGetWaitForResponsesConfigsReturnFalseMinusCase() { - when(requestOptionsMock.getWaitForResponses()).thenReturn(-10); - Collector collector = new Collector<>(TOPIC, originalMessage, requestOptionsMock, msbContext, eventHandlers, new TypeReference() {}); - assertFalse("expect false if MessageOptions.waitForResponses equals -10", collector.isAwaitingResponses()); + public void testIsAwaitingResponsesConfigsReturnPositive() { + when(requestOptionsMock.getWaitForResponses()).thenReturn(1000); + Collector collector = createCollector(); + + assertTrue("expect true if MessageOptions.waitForResponses is positive number", collector.isAwaitingResponses()); } @Test - public void testGetWaitForResponsesConfigsReturnTrue() { - when(requestOptionsMock.getWaitForResponses()).thenReturn(100); - Collector collector = new Collector<>(TOPIC, originalMessage, requestOptionsMock, msbContext, eventHandlers, new TypeReference() {}); - assertTrue("expect true if MessageOptions.waitForResponses equals 100", collector.isAwaitingResponses()); + public void testIsAwaitingResponsesConfigsReturnZero() { + when(requestOptionsMock.getWaitForResponses()).thenReturn(0); + Collector collector = createCollector(); + + assertFalse("expect false if MessageOptions.waitForResponses equals 0", collector.isAwaitingResponses()); } @Test - public void testGetWaitForResponsesConfigsReturnTrueMinusOne() { - when(requestOptionsMock.getWaitForResponses()).thenReturn(-1); - Collector collector = new Collector<>(TOPIC, originalMessage, requestOptionsMock, msbContext, eventHandlers, new TypeReference() {}); - assertTrue("expect true if MessageOptions.waitForResponses equals -1", collector.isAwaitingResponses()); + public void testIsAwaitingAcksConfigsReturnPositiveValue() { + when(requestOptionsMock.getAckTimeout()).thenReturn(1000); + Collector collector = createCollector(); + collector.listenForResponses(); + + assertTrue("expect true if MessageOptions.ackTimeout is positive number", collector.isAwaitingAcks()); } @Test - public void testIsAwaitingAcksConfigsNotSetAckTimeoutReturnFalse() { + public void testIsAwaitingAcksConfigsReturnNull() { when(requestOptionsMock.getAckTimeout()).thenReturn(null); - Collector collector = new Collector<>(TOPIC, originalMessage, requestOptionsMock, msbContext, eventHandlers, new TypeReference() {}); + Collector collector = createCollector(); + collector.listenForResponses(); + assertFalse("expect false if MessageOptions.ackTimeout null", collector.isAwaitingAcks()); } @Test - public void testIsAwaitingAcksReturnTrue() { - when(requestOptionsMock.getAckTimeout()).thenReturn(200); - Collector collector = new Collector<>(TOPIC, originalMessage, requestOptionsMock, msbContext, eventHandlers, new TypeReference() {}); - assertTrue("expect true if MessageOptions.ackTimeout equals 200", collector.isAwaitingAcks()); + public void testIsAwaitingAcksConfigsReturnZero() { + when(requestOptionsMock.getAckTimeout()).thenReturn(0); + Collector collector = createCollector(); + collector.listenForResponses(); + + assertFalse("expect false if MessageOptions.ackTimeout=0", collector.isAwaitingAcks()); } @Test public void testHandleResponse() { - String bodyText = "some body"; + String bodyText = "some body"; Message responseMessage = TestUtils.createMsbRequestMessage(TOPIC, bodyText); @SuppressWarnings("unchecked") - Callback onResponse = mock(Callback.class); + BiConsumer onResponse = mock(BiConsumer.class); @SuppressWarnings("unchecked") - Callback onRawResponse = mock(Callback.class); + BiConsumer onRawResponse = mock(BiConsumer.class); when(eventHandlers.onResponse()).thenReturn(onResponse); when(eventHandlers.onRawResponse()).thenReturn(onRawResponse); - Collector collector = new Collector<>(TOPIC, originalMessage, requestOptionsMock, msbContext, eventHandlers, new TypeReference() {}); - + + AcknowledgementHandler acknowledgeHandler = mock(AcknowledgementHandler.class); + MessageContext messageContext = mock(MessageContext.class); + + Collector collector = new Collector(TOPIC, originalMessage, requestOptionsMock, msbContext, eventHandlers, + new TypeReference() { + }) { + + MessageContext createMessageContext(AcknowledgementHandler acknowledgementHandler, Message originalMessage) { + return messageContext; + } + }; + // method under test - collector.handleMessage(responseMessage); + notifyMessagesConsumed(collector, 1); + collector.handleMessage(responseMessage, acknowledgeHandler); RestPayload expectedPayload = new RestPayload.Builder() .withBody(bodyText) .build(); - verify(onRawResponse).call(responseMessage); - verify(onResponse).call(expectedPayload); + + verify(onRawResponse).accept(responseMessage, messageContext); + verify(onResponse).accept(expectedPayload, messageContext); verify(collectorManagerMock).unregisterCollector(collector); assertTrue(collector.getPayloadMessages().stream().anyMatch(message -> message.getId().equals(responseMessage.getId()))); assertFalse(collector.getAckMessages().contains(responseMessage)); } - @Test(expected = JsonConversionException.class) + @Test public void testHandleResponseConversionFailed() { String bodyText = "some body"; Message responseMessage = TestUtils.createMsbRequestMessage(TOPIC, bodyText); @SuppressWarnings("unchecked") - Callback> onResponse = mock(Callback.class); + BiConsumer, MessageContext> onResponse = mock(BiConsumer.class); @SuppressWarnings("unchecked") - Callback onRawResponse = mock(Callback.class); + BiConsumer onRawResponse = mock(BiConsumer.class); @SuppressWarnings("unchecked") EventHandlers> eventHandlers = mock(EventHandlers.class); when(eventHandlers.onResponse()).thenReturn(onResponse); when(eventHandlers.onRawResponse()).thenReturn(onRawResponse); - TypeReference> payloadTypeReference = new TypeReference>() {}; - Collector> collector = new Collector<>(TOPIC, originalMessage, requestOptionsMock, msbContext, eventHandlers, payloadTypeReference); + TypeReference> payloadTypeReference = new TypeReference>() { + }; + + AcknowledgementHandler ackHandler = mock(AcknowledgementHandler.class); + MessageContext messageContext = mock(MessageContext.class); + + Collector> collector = new Collector>(TOPIC, originalMessage, requestOptionsMock, msbContext, eventHandlers, + payloadTypeReference) { + MessageContext createMessageContext(AcknowledgementHandler acknowledgementHandler, Message originalMessage) { + return messageContext; + } + }; // make sure that onRawResponse is called even if conversion of payload to custom type fails - try { - collector.handleMessage(responseMessage); - } finally { - verify(onRawResponse).call(responseMessage); - verify(onResponse, never()).call(any()); - } + + collector.handleMessage(responseMessage, ackHandler); + + verify(onRawResponse).accept(responseMessage, messageContext); + verify(onResponse, never()).accept(any(), any()); } @Test @SuppressWarnings({ "rawtypes", "unchecked" }) public void testHandleResponseReceivedAck() { - Callback onAck = mock(Callback.class); + BiConsumer onAck = mock(BiConsumer.class); when(eventHandlers.onAcknowledge()).thenReturn(onAck); - Collector collector = new Collector(TOPIC, originalMessage, requestOptionsMock, msbContext, eventHandlers, new TypeReference() {}); - - collector.handleMessage(responseMessageWithAck); + Collector collector = createCollector(); + + AcknowledgementHandler ackHandler = mock(AcknowledgementHandler.class); + collector.handleMessage(responseMessageWithAck, ackHandler); - verify(onAck).call(responseMessageWithAck.getAck()); + verify(onAck).accept(responseMessageWithAck.getAck(), messageContextMock); assertTrue(collector.getAckMessages().contains(responseMessageWithAck)); assertFalse(collector.getPayloadMessages().contains(responseMessageWithAck)); } @@ -198,9 +240,11 @@ public void testHandleResponseReceivedAck() { public void testHandleResponseEndEventNoResponsesRemaining() { Callback onEnd = mock(Callback.class); when(eventHandlers.onEnd()).thenReturn(onEnd); - Collector collector = new Collector(TOPIC, originalMessage, requestOptionsMock, msbContext, eventHandlers, new TypeReference() {}); + Collector collector = createCollector(); - collector.handleMessage(responseMessageWithAck); + notifyMessagesConsumed(collector, 1); + AcknowledgementHandler ackHandler = mock(AcknowledgementHandler.class); + collector.handleMessage(responseMessageWithAck, ackHandler); verify(timeoutManagerMock, never()).enableResponseTimeout(anyInt(), any(Collector.class)); verify(timeoutManagerMock, never()).enableAckTimeout(anyInt(), any(Collector.class)); @@ -220,18 +264,21 @@ public void testHandleResponseLastResponse() { when(requestOptionsMock.getResponseTimeout()).thenReturn(responseTimeout); when(requestOptionsMock.getWaitForResponses()).thenReturn(1); - Callback onResponse = mock(Callback.class); + BiConsumer onResponse = mock(BiConsumer.class); when(eventHandlers.onResponse()).thenReturn(onResponse); Callback onEnd = mock(Callback.class); when(eventHandlers.onEnd()).thenReturn(onEnd); - Collector collector = new Collector(TOPIC, originalMessage, requestOptionsMock, msbContext, eventHandlers, new TypeReference() {}); - collector.handleMessage(responseMessage); + Collector collector = createCollector(); + + notifyMessagesConsumed(collector, 1); + AcknowledgementHandler ackHandler = mock(AcknowledgementHandler.class); + collector.handleMessage(responseMessage, ackHandler); RestPayload expectedPayload = new RestPayload.Builder() .withBody(bodyText) .build(); - verify(onResponse).call(expectedPayload); + verify(onResponse).accept(expectedPayload, messageContextMock); verify(timeoutManagerMock, never()).enableResponseTimeout(eq(responseTimeout), eq(collector)); verify(timeoutManagerMock, never()).enableAckTimeout(eq(0), eq(collector)); verify(onEnd).call(any()); @@ -241,8 +288,8 @@ public void testHandleResponseLastResponse() { @SuppressWarnings({ "rawtypes", "unchecked" }) public void testHandleResponseWaitForOneMoreResponse() { String bodyText = "some body"; - Message responseMessage = TestUtils.createMsbRequestMessage(TOPIC, bodyText); - + Message responseMessage1 = TestUtils.createMsbRequestMessage(TOPIC, bodyText); + Message responseMessage2 = TestUtils.createMsbRequestMessage(TOPIC, bodyText); /*ackTimeout = 0, responseTimeout=200; waitForResponses = 2 */ int responseTimeout = 200; @@ -250,31 +297,81 @@ public void testHandleResponseWaitForOneMoreResponse() { when(requestOptionsMock.getResponseTimeout()).thenReturn(responseTimeout); when(requestOptionsMock.getWaitForResponses()).thenReturn(2); - Callback onResponse = mock(Callback.class); + BiConsumer onResponse = mock(BiConsumer.class); when(eventHandlers.onResponse()).thenReturn(onResponse); Callback onEnd = mock(Callback.class); when(eventHandlers.onEnd()).thenReturn(onEnd); - Collector collector = new Collector(TOPIC, originalMessage, requestOptionsMock, msbContext, eventHandlers, new TypeReference() {}); + Collector collector = createCollector(); collector.listenForResponses(); RestPayload expectedPayload = new RestPayload.Builder() .withBody(bodyText) .build(); + AcknowledgementHandler ackHandler = mock(AcknowledgementHandler.class); + //send first response - collector.handleMessage(responseMessage); - verify(onResponse).call(expectedPayload); + notifyMessagesConsumed(collector, 1); + collector.handleMessage(responseMessage1, ackHandler); + verify(onResponse).accept(expectedPayload, messageContextMock); verify(onEnd, never()).call(any()); //send last response - collector.handleMessage(responseMessage); - verify(onResponse, times(2)).call(expectedPayload); + notifyMessagesConsumed(collector, 1); + collector.handleMessage(responseMessage2, ackHandler); + verify(onResponse, times(2)).accept(expectedPayload, messageContextMock); verify(timeoutManagerMock, never()).enableResponseTimeout(eq(responseTimeout), eq(collector)); verify(timeoutManagerMock, never()).enableAckTimeout(eq(0), eq(collector)); verify(onEnd).call(any()); } + @Test + @SuppressWarnings({ "rawtypes", "unchecked" }) + public void testHandleResponseLastResponseLostAfterTimeout() { + String bodyText = "some body"; + Message responseMessage1 = TestUtils.createMsbRequestMessage(TOPIC, bodyText); + Message responseMessage2 = TestUtils.createMsbRequestMessage(TOPIC, bodyText); + /*ackTimeout = 0, responseTimeout=200; waitForResponses = 2 + */ + int responseTimeout = 200; + when(requestOptionsMock.getAckTimeout()).thenReturn(0); + when(requestOptionsMock.getResponseTimeout()).thenReturn(responseTimeout); + when(requestOptionsMock.getWaitForResponses()).thenReturn(2); + + BiConsumer onResponse = mock(BiConsumer.class); + when(eventHandlers.onResponse()).thenReturn(onResponse); + Callback onEnd = mock(Callback.class); + when(eventHandlers.onEnd()).thenReturn(onEnd); + + Collector collector = createCollector(); + collector.listenForResponses(); + + RestPayload expectedPayload = new RestPayload.Builder() + .withBody(bodyText) + .build(); + + AcknowledgementHandler ackHandler = mock(AcknowledgementHandler.class); + + //send first response + notifyMessagesConsumed(collector, 3); + collector.handleMessage(responseMessage1, ackHandler); + verify(onResponse).accept(expectedPayload, messageContextMock); + verify(onEnd, never()).call(any()); + + //timeout + collector.end(); + verify(onEnd, never()).call(any()); + + //second response lost + notifyMessagesLost(collector, 1); + verify(onEnd, never()).call(any()); + + //third response lost + notifyMessagesLost(collector, 1); + verify(onEnd).call(any()); + } + @Test @SuppressWarnings({ "rawtypes", "unchecked" }) public void testHandleResponseNoResponsesRemainingButAwaitAck() { @@ -288,16 +385,18 @@ public void testHandleResponseNoResponsesRemainingButAwaitAck() { when(requestOptionsMock.getResponseTimeout()).thenReturn(0); when(requestOptionsMock.getWaitForResponses()).thenReturn(0); - Callback onAck = mock(Callback.class); + BiConsumer onAck = mock(BiConsumer.class); + when(eventHandlers.onAcknowledge()).thenReturn(onAck); Callback onEnd = mock(Callback.class); when(eventHandlers.onEnd()).thenReturn(onEnd); - Collector collector = new Collector(TOPIC, originalMessage, requestOptionsMock, msbContext, eventHandlers, new TypeReference() {}); + Collector collector = createCollector(); collector.listenForResponses(); + AcknowledgementHandler ackHandler = mock(AcknowledgementHandler.class); //send payload response - collector.handleMessage(responseMessage); + collector.handleMessage(responseMessage, ackHandler); verify(timeoutManagerMock, never()).enableResponseTimeout(anyInt(), eq(collector)); verify(timeoutManagerMock).enableAckTimeout(anyInt(), eq(collector)); verify(onEnd, never()).call(any()); @@ -316,16 +415,17 @@ public void testHandleResponseReceivedPayloadButAwaitAck() { when(requestOptionsMock.getResponseTimeout()).thenReturn(0); when(requestOptionsMock.getWaitForResponses()).thenReturn(1); - Callback onAck = mock(Callback.class); + BiConsumer onAck = mock(BiConsumer.class); when(eventHandlers.onAcknowledge()).thenReturn(onAck); Callback onEnd = mock(Callback.class); when(eventHandlers.onEnd()).thenReturn(onEnd); - Collector collector = new Collector(TOPIC, originalMessage, requestOptionsMock, msbContext, eventHandlers, new TypeReference() {}); + Collector collector = createCollector(); collector.listenForResponses(); + AcknowledgementHandler ackHandler = mock(AcknowledgementHandler.class); //send payload response - collector.handleMessage(responseMessage); + collector.handleMessage(responseMessage, ackHandler); verify(timeoutManagerMock, never()).enableResponseTimeout(eq(0), eq(collector)); verify(timeoutManagerMock).enableAckTimeout(anyInt(), eq(collector)); verify(onEnd, never()).call(any()); @@ -344,16 +444,18 @@ public void testHandleResponseNoResponsesRemainingAndWaitUntilAckBeforeNow() { when(requestOptionsMock.getResponseTimeout()).thenReturn(0); when(requestOptionsMock.getWaitForResponses()).thenReturn(0); - Callback onAck = mock(Callback.class); + BiConsumer onAck = mock(BiConsumer.class); when(eventHandlers.onAcknowledge()).thenReturn(onAck); Callback onEnd = mock(Callback.class); when(eventHandlers.onEnd()).thenReturn(onEnd); - Collector collector = new Collector(TOPIC, originalMessage, requestOptionsMock, msbContext, eventHandlers, new TypeReference() {}); + Collector collector = createCollector(); collector.listenForResponses(); + AcknowledgementHandler ackHandler = mock(AcknowledgementHandler.class); //send payload response - collector.handleMessage(responseMessage); + notifyMessagesConsumed(collector, 1); + collector.handleMessage(responseMessage, ackHandler); verify(timeoutManagerMock, never()).enableResponseTimeout(eq(0), eq(collector)); verify(timeoutManagerMock, never()).enableAckTimeout(eq(ackTimeoutMs), eq(collector)); verify(onEnd).call(any()); @@ -372,13 +474,15 @@ public void testHandleResponseReceivedAckWithSameTimeoutValue() { Callback onEnd = mock(Callback.class); when(eventHandlers.onEnd()).thenReturn(onEnd); - Collector collector = new Collector(TOPIC, originalMessage, requestOptionsMock, msbContext, eventHandlers, new TypeReference() {}); + Collector collector = createCollector(); collector.listenForResponses(); Acknowledge ack = new Acknowledge.Builder().withResponderId(Utils.generateId()).withResponsesRemaining(0).withTimeoutMs(timeoutMs).build(); - Message responseMessageWithAck = TestUtils.createMsbResponseMessageWithAckNoPayload(ack, TOPIC, originalMessage.getCorrelationId()); + Message responseMessageWithAck = TestUtils.createMsbResponseMessageWithAckNoPayload(ack, TOPIC_RESPONSE, originalMessage.getCorrelationId()); - collector.handleMessage(responseMessageWithAck); + notifyMessagesConsumed(collector, 1); + AcknowledgementHandler ackHandler = mock(AcknowledgementHandler.class); + collector.handleMessage(responseMessageWithAck, ackHandler); verify(timeoutManagerMock, never()).enableResponseTimeout(eq(timeoutMs), eq(collector)); verify(onEnd).call(any()); @@ -387,7 +491,7 @@ public void testHandleResponseReceivedAckWithSameTimeoutValue() { @Test @SuppressWarnings({ "rawtypes", "unchecked" }) public void testHandleResponseReceivedAckWithUpdatedTimeoutAndNoResponsesRemaining() { - /*ackTimeout = 0, responseTimeout= 50; waitForResponses = 0 + /*ackTimeout = 0, responseTimeout= 100; waitForResponses = 0 */ int timeoutMs = 50; int timeoutMsInAck = 100; @@ -398,44 +502,50 @@ public void testHandleResponseReceivedAckWithUpdatedTimeoutAndNoResponsesRemaini Callback onEnd = mock(Callback.class); when(eventHandlers.onEnd()).thenReturn(onEnd); - Collector collector = new Collector(TOPIC, originalMessage, requestOptionsMock, msbContext, eventHandlers, new TypeReference() {}); + Collector collector = createCollector(); collector.listenForResponses(); Acknowledge ack = new Acknowledge.Builder().withResponderId(Utils.generateId()).withResponsesRemaining(0).withTimeoutMs(timeoutMsInAck) .build(); - Message responseMessageWithAck = TestUtils.createMsbResponseMessageWithAckNoPayload(ack, TOPIC, originalMessage.getCorrelationId()); + Message responseMessageWithAck = TestUtils.createMsbResponseMessageWithAckNoPayload(ack, TOPIC_RESPONSE, originalMessage.getCorrelationId()); - collector.handleMessage(responseMessageWithAck); + notifyMessagesConsumed(collector, 1); + AcknowledgementHandler ackHandler = mock(AcknowledgementHandler.class); + collector.handleMessage(responseMessageWithAck, ackHandler); - verify(timeoutManagerMock).enableResponseTimeout(eq(timeoutMsInAck), eq(collector)); + verify(timeoutManagerMock, never()).enableResponseTimeout(eq(timeoutMsInAck), eq(collector)); verify(onEnd).call(any()); } @Test @SuppressWarnings({ "rawtypes", "unchecked" }) public void testHandleResponseReceivedAckWithUpdatedTimeoutAndResponsesRemaining() { - /*ackTimeout = 0, responseTimeout= 50; waitForResponses = 2 + /*ackTimeout = 0, responseTimeout= 100; waitForResponses = 2 */ - int timeoutMs = 50; - int timeoutMsInAck = 100; + int timeoutMs = 100; + int timeoutMsInAck = 200; int responsesRemaining = 2; when(requestOptionsMock.getAckTimeout()).thenReturn(0); when(requestOptionsMock.getResponseTimeout()).thenReturn(timeoutMs); when(requestOptionsMock.getWaitForResponses()).thenReturn(0); + ArgumentCaptor timeoutCaptor = ArgumentCaptor.forClass(Integer.class); Callback onEnd = mock(Callback.class); when(eventHandlers.onEnd()).thenReturn(onEnd); - Collector collector = new Collector(TOPIC, originalMessage, requestOptionsMock, msbContext, eventHandlers, new TypeReference() {}); + Collector collector = createCollector(); collector.listenForResponses(); Acknowledge ack = new Acknowledge.Builder().withResponderId(Utils.generateId()).withResponsesRemaining(responsesRemaining) .withTimeoutMs(timeoutMsInAck).build(); - Message responseMessageWithAck = TestUtils.createMsbResponseMessageWithAckNoPayload(ack, TOPIC, originalMessage.getCorrelationId()); + Message responseMessageWithAck = TestUtils.createMsbResponseMessageWithAckNoPayload(ack, TOPIC_RESPONSE, originalMessage.getCorrelationId()); + + + collector.handleMessage(responseMessageWithAck, null); - collector.handleMessage(responseMessageWithAck); + verify(timeoutManagerMock).enableResponseTimeout(timeoutCaptor.capture(), any()); - verify(timeoutManagerMock).enableResponseTimeout(eq(timeoutMsInAck), eq(collector)); + assertThat(timeoutCaptor.getValue()).isBetween(1, timeoutMsInAck); verify(onEnd, never()).call(any()); } @@ -445,9 +555,9 @@ public void testHandleResponseReceivedAcksWithUpdatedTimeoutAndResponsesRemainin /*ackTimeout = 0, responseTimeout= 50; waitForResponses = 2 */ int timeoutMs = 50; - int timeoutMsInAckResponderOne = 100; + int timeoutMsInAckResponderOne = 2000; int responsesRemainingResponderOne = 5; - int timeoutMsInAckResponderTwo = 222; + int timeoutMsInAckResponderTwo = 5000; int responsesRemainingResponderTwo = 7; when(requestOptionsMock.getAckTimeout()).thenReturn(0); when(requestOptionsMock.getResponseTimeout()).thenReturn(timeoutMs); @@ -455,28 +565,31 @@ public void testHandleResponseReceivedAcksWithUpdatedTimeoutAndResponsesRemainin Callback onEnd = mock(Callback.class); when(eventHandlers.onEnd()).thenReturn(onEnd); + ArgumentCaptor timeoutCaptor = ArgumentCaptor.forClass(Integer.class); - Collector collector = new Collector(TOPIC, originalMessage, requestOptionsMock, msbContext, eventHandlers, new TypeReference() {}); + Collector collector = createCollector(); collector.listenForResponses(); Acknowledge ackRespOne = new Acknowledge.Builder().withResponderId(Utils.generateId()).withResponsesRemaining(responsesRemainingResponderOne) .withTimeoutMs(timeoutMsInAckResponderOne).build(); Message messageWithAckOne = TestUtils - .createMsbResponseMessageWithAckNoPayload(ackRespOne, TOPIC, originalMessage.getCorrelationId()); + .createMsbResponseMessageWithAckNoPayload(ackRespOne, TOPIC_RESPONSE, originalMessage.getCorrelationId()); Acknowledge ackRespTwo = new Acknowledge.Builder().withResponderId(Utils.generateId()).withResponsesRemaining(responsesRemainingResponderTwo) .withTimeoutMs(timeoutMsInAckResponderTwo).build(); Message messageWithAckTwo = TestUtils - .createMsbResponseMessageWithAckNoPayload(ackRespTwo, TOPIC, originalMessage.getCorrelationId()); + .createMsbResponseMessageWithAckNoPayload(ackRespTwo, TOPIC_RESPONSE, originalMessage.getCorrelationId()); - collector.handleMessage(messageWithAckOne); - verify(timeoutManagerMock).enableResponseTimeout(eq(timeoutMsInAckResponderOne), eq(collector)); + collector.handleMessage(messageWithAckOne, null); + verify(timeoutManagerMock).enableResponseTimeout(timeoutCaptor.capture(), any()); assertEquals(responsesRemainingResponderOne, collector.getResponsesRemaining()); + assertThat(timeoutCaptor.getValue()).isBetween(1, timeoutMsInAckResponderOne); verify(onEnd, never()).call(any()); - collector.handleMessage(messageWithAckTwo); - verify(timeoutManagerMock, times(1)).enableResponseTimeout(eq(timeoutMsInAckResponderTwo), eq(collector)); + collector.handleMessage(messageWithAckTwo, null); + verify(timeoutManagerMock, times(2)).enableResponseTimeout(timeoutCaptor.capture(), any()); assertEquals(responsesRemainingResponderOne + responsesRemainingResponderTwo, collector.getResponsesRemaining()); + assertThat(timeoutCaptor.getValue()).isBetween(1, timeoutMsInAckResponderTwo - 1); verify(onEnd, never()).call(any()); } @@ -484,8 +597,44 @@ public void testHandleResponseReceivedAcksWithUpdatedTimeoutAndResponsesRemainin @SuppressWarnings({ "rawtypes", "unchecked" }) public void testHandleResponseEnsureResponsesRemainingIsDecreased() { String bodyText = "some body"; - Message responseMessage = TestUtils.createMsbRequestMessage(TOPIC, bodyText); + Message responseMessage1 = TestUtils.createMsbRequestMessage(TOPIC, bodyText); + Message responseMessage2 = TestUtils.createMsbRequestMessage(TOPIC, bodyText); + /*ackTimeout = 0, responseTimeout=200; waitForResponses = 2 + */ + int responseTimeout = 200; + int responsesRemaining = 2; + when(requestOptionsMock.getAckTimeout()).thenReturn(0); + when(requestOptionsMock.getResponseTimeout()).thenReturn(responseTimeout); + when(requestOptionsMock.getWaitForResponses()).thenReturn(responsesRemaining); + + BiConsumer onResponse = mock(BiConsumer.class); + when(eventHandlers.onResponse()).thenReturn(onResponse); + Callback onEnd = mock(Callback.class); + when(eventHandlers.onEnd()).thenReturn(onEnd); + Collector collector = createCollector(); + collector.listenForResponses(); + + assertEquals(responsesRemaining, collector.getResponsesRemaining()); + + notifyMessagesConsumed(collector, 2); + //send first response + collector.handleMessage(responseMessage1, null); + assertEquals(1, collector.getResponsesRemaining()); + verify(onEnd, never()).call(any()); + + //send last response + collector.handleMessage(responseMessage2, null); + assertEquals(0, collector.getResponsesRemaining()); + verify(onEnd).call(any()); + } + + @Test + @SuppressWarnings({ "rawtypes", "unchecked" }) + public void testHandleResponseRedelivery() { + String bodyText = "some body"; + Message responseMessage1 = TestUtils.createMsbRequestMessage(TOPIC, bodyText); + Message responseMessage2 = TestUtils.createMsbRequestMessage(TOPIC, bodyText); /*ackTimeout = 0, responseTimeout=200; waitForResponses = 2 */ int responseTimeout = 200; @@ -494,32 +643,604 @@ public void testHandleResponseEnsureResponsesRemainingIsDecreased() { when(requestOptionsMock.getResponseTimeout()).thenReturn(responseTimeout); when(requestOptionsMock.getWaitForResponses()).thenReturn(responsesRemaining); - Callback onResponse = mock(Callback.class); + BiConsumer onResponse = mock(BiConsumer.class); when(eventHandlers.onResponse()).thenReturn(onResponse); Callback onEnd = mock(Callback.class); when(eventHandlers.onEnd()).thenReturn(onEnd); - Collector collector = new Collector(TOPIC, originalMessage, requestOptionsMock, msbContext, eventHandlers, new TypeReference() {}); + Collector collector = createCollector(); collector.listenForResponses(); assertEquals(responsesRemaining, collector.getResponsesRemaining()); + notifyMessagesConsumed(collector, 2); //send first response - collector.handleMessage(responseMessage); + collector.handleMessage(responseMessage1, null); + assertEquals(1, collector.getResponsesRemaining()); + verify(onEnd, never()).call(any()); + + //redeliver first response + collector.handleMessage(responseMessage1, null); assertEquals(1, collector.getResponsesRemaining()); verify(onEnd, never()).call(any()); + notifyMessagesConsumed(collector, 1); //send last response - collector.handleMessage(responseMessage); + collector.handleMessage(responseMessage2, null); assertEquals(0, collector.getResponsesRemaining()); verify(onEnd).call(any()); } + @Test + @SuppressWarnings({ "rawtypes", "unchecked" }) + public void testEndInvokedWhenOnResponseCallbacksThrowExceptions() { + String bodyText = "some body"; + Message responseMessage1 = TestUtils.createMsbRequestMessage(TOPIC, bodyText); + Message responseMessage2 = TestUtils.createMsbRequestMessage(TOPIC, bodyText); + /*ackTimeout = 0, responseTimeout=200; waitForResponses = 2 + */ + int responseTimeout = 200; + int responsesRemaining = 2; + when(requestOptionsMock.getAckTimeout()).thenReturn(0); + when(requestOptionsMock.getResponseTimeout()).thenReturn(responseTimeout); + when(requestOptionsMock.getWaitForResponses()).thenReturn(responsesRemaining); + + BiConsumer onResponse = mock(BiConsumer.class); + when(eventHandlers.onResponse()).thenReturn(onResponse); + doThrow(new RuntimeException("Unexpected error in a callback!")).when(onResponse).accept(any(), any()); + + Callback onEnd = mock(Callback.class); + when(eventHandlers.onEnd()).thenReturn(onEnd); + + Collector collector = createCollector(); + collector.listenForResponses(); + + assertEquals(responsesRemaining, collector.getResponsesRemaining()); + + notifyMessagesConsumed(collector, 2); + + //send first response + collector.handleMessage(responseMessage1, null); + verify(onEnd, never()).call(any()); + + collector.handleMessage(responseMessage2, null); + + verify(onEnd, times(1)).call(any()); + } + + @Test + @SuppressWarnings({ "rawtypes", "unchecked" }) + public void testEndThrowsExceptions() { + String bodyText = "some body"; + Message responseMessage1 = TestUtils.createMsbRequestMessage(TOPIC, bodyText); + + /*ackTimeout = 0, responseTimeout=200; waitForResponses = 2 + */ + int responseTimeout = 200; + int responsesRemaining = 1; + when(requestOptionsMock.getAckTimeout()).thenReturn(0); + when(requestOptionsMock.getResponseTimeout()).thenReturn(responseTimeout); + when(requestOptionsMock.getWaitForResponses()).thenReturn(responsesRemaining); + + BiConsumer onResponse = mock(BiConsumer.class); + when(eventHandlers.onResponse()).thenReturn(onResponse); + + Callback onEnd = mock(Callback.class); + when(eventHandlers.onEnd()).thenReturn(onEnd); + + doThrow(new RuntimeException("Unexpected error in a callback!")).when(onEnd).call(any()); + + Collector collector = createCollector(); + collector.listenForResponses(); + + assertEquals(responsesRemaining, collector.getResponsesRemaining()); + + notifyMessagesConsumed(collector, 1); + + //send first response + collector.handleMessage(responseMessage1, null); + verify(onEnd, times(1)).call(any()); + } + + + @Test + @SuppressWarnings({ "rawtypes", "unchecked" }) + public void testHandleResponseReceivedAckWithUpdatedTimeoutAndOneResponseRemaining() throws InterruptedException, IOException { + int timeoutMs = 200; + int timeoutMsInAck = 5000; + String responderId = "b"; + + when(requestOptionsMock.getAckTimeout()).thenReturn(0); + when(requestOptionsMock.getResponseTimeout()).thenReturn(timeoutMs); + when(requestOptionsMock.getWaitForResponses()).thenReturn(0); + + this.msbContext = TestUtils.createMsbContextBuilder() + .withMsbConfigurations(msbConfigurationsMock) + .withMessageFactory(messageFactoryMock) + .withChannelManager(channelManagerMock) + .withClock(Clock.systemDefaultZone()) + .withTimeoutManager(new TimeoutManager(1)) + .withCollectorManagerFactory(collectorManagerFactoryMock) + .build(); + + Callback onEnd = mock(Callback.class); + when(eventHandlers.onEnd()).thenReturn(onEnd); + + Collector collector = createCollector(); + collector.listenForResponses(); + + notifyMessagesConsumed(collector, 1); + Acknowledge ack = new Acknowledge.Builder().withResponderId(responderId).withResponsesRemaining(1).withTimeoutMs(timeoutMsInAck) + .build(); + Message responseMessageWithAck = TestUtils.createMsbResponseMessageWithAckNoPayload(ack, TOPIC_RESPONSE, originalMessage.getCorrelationId()); + collector.handleMessage(responseMessageWithAck, null); + + //simulate responder response + notifyMessagesConsumed(collector, 1); + Acknowledge responseAck = new Acknowledge.Builder().withResponderId(responderId).withResponsesRemaining(-1).build(); + ObjectMapper payloadMapper = TestUtils.createMessageMapper(); + JsonNode payloadNode = payloadMapper.readValue(String.format("{\"body\": \"%s\" }", "test response payload body"), JsonNode.class); + + Message responderMessage = TestUtils.createMsbResponseMessage(responseAck, payloadNode, TOPIC_RESPONSE, "someCorrelationId"); + + //send message + collector.handleMessage(responderMessage, null); + + verify(onEnd, after(1500).times(1)).call(any()); + } + + @Test + @SuppressWarnings({ "rawtypes", "unchecked" }) + public void testTimeoutFiredWhileMessagesExpected() throws InterruptedException, IOException { + int timeoutMs = 300; + int timeoutMsInAck = 600; + String responderId = "b"; + + when(requestOptionsMock.getAckTimeout()).thenReturn(0); + when(requestOptionsMock.getResponseTimeout()).thenReturn(timeoutMs); + when(requestOptionsMock.getWaitForResponses()).thenReturn(0); + + this.msbContext = TestUtils.createMsbContextBuilder() + .withMsbConfigurations(msbConfigurationsMock) + .withMessageFactory(messageFactoryMock) + .withChannelManager(channelManagerMock) + .withClock(Clock.systemDefaultZone()) + .withTimeoutManager(new TimeoutManager(1)) + .withCollectorManagerFactory(collectorManagerFactoryMock) + .build(); + + Callback onEnd = mock(Callback.class); + when(eventHandlers.onEnd()).thenReturn(onEnd); + + @SuppressWarnings("unchecked") + BiConsumer onResponse = (restPayload, messageContext) -> { + try { + Thread.sleep(900); + } catch (InterruptedException e) { + fail("Interrupted"); + } + }; + + when(eventHandlers.onResponse()).thenReturn(onResponse); + + Collector collector = createCollector(); + collector.listenForResponses(); + + Acknowledge ack = new Acknowledge.Builder().withResponderId(responderId).withResponsesRemaining(99).withTimeoutMs(timeoutMsInAck) + .build(); + Message responseMessageWithAck = TestUtils.createMsbResponseMessageWithAckNoPayload(ack, TOPIC_RESPONSE, originalMessage.getCorrelationId()); + notifyMessagesConsumed(collector, 1); + collector.handleMessage(responseMessageWithAck, null); + + //simulate responder response + Acknowledge responseAck = new Acknowledge.Builder().withResponderId(responderId).withResponsesRemaining(-1).build(); + ObjectMapper payloadMapper = TestUtils.createMessageMapper(); + JsonNode payloadNode = payloadMapper.readValue(String.format("{\"body\": \"%s\" }", "test response payload body"), JsonNode.class); + + Message responderMessage = TestUtils.createMsbResponseMessage(responseAck, payloadNode, TOPIC_RESPONSE, "someCorrelationId"); + + //send message + notifyMessagesConsumed(collector, 1); + collector.handleMessage(responderMessage, null); + + verify(onEnd, after(1500).times(1)).call(any()); + } + + @Test + @SuppressWarnings({ "rawtypes", "unchecked" }) + public void testTimeoutFiredDuringMessageHandling() throws Exception { + int timeoutMs = 300; + int timeoutMsInAck = 600; + String responderId = "b"; + + when(requestOptionsMock.getAckTimeout()).thenReturn(0); + when(requestOptionsMock.getResponseTimeout()).thenReturn(timeoutMs); + when(requestOptionsMock.getWaitForResponses()).thenReturn(0); + + this.msbContext = TestUtils.createMsbContextBuilder() + .withMsbConfigurations(msbConfigurationsMock) + .withMessageFactory(messageFactoryMock) + .withChannelManager(channelManagerMock) + .withClock(Clock.systemDefaultZone()) + .withTimeoutManager(new TimeoutManager(1)) + .withCollectorManagerFactory(collectorManagerFactoryMock) + .build(); + + CompletableFuture timeOnEnd = new CompletableFuture<>(); + CompletableFuture timeAfterHandler = new CompletableFuture<>(); + LongAdder onEndCounter = new LongAdder(); + + Callback onEnd = (Void in) -> { + System.out.println("onEnd invoked"); + onEndCounter.increment(); + timeOnEnd.complete(System.currentTimeMillis()); + }; + + when(eventHandlers.onEnd()).thenReturn(onEnd); + + @SuppressWarnings("unchecked") + BiConsumer onResponse = (restPayload, messageContext) -> { + try { + System.out.println("slow onResponse start"); + Thread.sleep(1500); + timeAfterHandler.complete(System.currentTimeMillis()); + System.out.println("slow onResponse finish"); + } catch (InterruptedException e) { + fail("Interrupted"); + } + }; + + when(eventHandlers.onResponse()).thenReturn(onResponse); + + Collector collector = createCollector(); + collector.listenForResponses(); + + Acknowledge ack = new Acknowledge.Builder().withResponderId(responderId).withResponsesRemaining(1).withTimeoutMs(timeoutMsInAck) + .build(); + Message responseMessageWithAck = TestUtils.createMsbResponseMessageWithAckNoPayload(ack, TOPIC_RESPONSE, originalMessage.getCorrelationId()); + notifyMessagesConsumed(collector, 1); + collector.handleMessage(responseMessageWithAck, null); + + //simulate responder response + Acknowledge responseAck = new Acknowledge.Builder().withResponderId(responderId).withResponsesRemaining(-1).build(); + ObjectMapper payloadMapper = TestUtils.createMessageMapper(); + JsonNode payloadNode = payloadMapper.readValue(String.format("{\"body\": \"%s\" }", "test response payload body"), JsonNode.class); + + Message responderMessage = TestUtils.createMsbResponseMessage(responseAck, payloadNode, TOPIC_RESPONSE, "someCorrelationId"); + + //send message + notifyMessagesConsumed(collector, 1); + collector.handleMessage(responderMessage, null); + long timeAfterHandlerValue = timeAfterHandler.get(2000, TimeUnit.MILLISECONDS); + long timeOnEndValue = timeOnEnd.get(2000, TimeUnit.MILLISECONDS); + + assertTrue("onEnd should not be invoked by timer: it should be invoked after the last message handling instead: " + + "handler complete:" + timeAfterHandlerValue + ", onEnd invoked:" + timeOnEndValue, + timeAfterHandlerValue <= timeOnEndValue); + assertEquals(1, onEndCounter.intValue()); + } + + @Test + @SuppressWarnings({ "rawtypes", "unchecked" }) + public void testHandleResponseReceivedAckWithUpdatedTimeoutAndOneResponseLost() throws InterruptedException, IOException { + int timeoutMs = 200; + int timeoutMsInAck = 500; + String responderId = "b"; + + when(requestOptionsMock.getAckTimeout()).thenReturn(0); + when(requestOptionsMock.getResponseTimeout()).thenReturn(timeoutMs); + when(requestOptionsMock.getWaitForResponses()).thenReturn(0); + + this.msbContext = TestUtils.createMsbContextBuilder() + .withMsbConfigurations(msbConfigurationsMock) + .withMessageFactory(messageFactoryMock) + .withChannelManager(channelManagerMock) + .withClock(Clock.systemDefaultZone()) + .withTimeoutManager(new TimeoutManager(1)) + .withCollectorManagerFactory(collectorManagerFactoryMock) + .build(); + + Callback onEnd = mock(Callback.class); + when(eventHandlers.onEnd()).thenReturn(onEnd); + + Collector collector = createCollector(); + collector.listenForResponses(); + + Acknowledge ack = new Acknowledge.Builder().withResponderId(responderId).withResponsesRemaining(3).withTimeoutMs(timeoutMsInAck) + .build(); + Message responseMessageWithAck = TestUtils.createMsbResponseMessageWithAckNoPayload(ack, TOPIC_RESPONSE, originalMessage.getCorrelationId()); + notifyMessagesConsumed(collector, 1); + collector.handleMessage(responseMessageWithAck, null); + + //simulate responder response + Acknowledge responseAck = new Acknowledge.Builder().withResponderId(responderId).withResponsesRemaining(-1).build(); + ObjectMapper payloadMapper = TestUtils.createMessageMapper(); + JsonNode payloadNode = payloadMapper.readValue(String.format("{\"body\": \"%s\" }", "test response payload body"), JsonNode.class); + + Message responderMessage = TestUtils.createMsbResponseMessage(responseAck, payloadNode, TOPIC_RESPONSE, "someCorrelationId"); + notifyMessagesConsumed(collector, 1); + notifyMessagesLost(collector, 1); + verify(onEnd, never()).call(any()); + + //send message + notifyMessagesConsumed(collector, 1); + collector.handleMessage(responderMessage, null); + + verify(onEnd, after(1500).times(1)).call(any()); + } + + @Test + @SuppressWarnings({ "rawtypes", "unchecked" }) + public void testHandleResponseReceivedAckWithUpdatedTimeoutAndTwoResponsesRemaining() throws InterruptedException, IOException { + int timeoutMs = 200; + int timeoutMsInAck = 5000; + String responderId = "b"; + + when(requestOptionsMock.getAckTimeout()).thenReturn(0); + when(requestOptionsMock.getResponseTimeout()).thenReturn(timeoutMs); + when(requestOptionsMock.getWaitForResponses()).thenReturn(0); + + this.msbContext = TestUtils.createMsbContextBuilder() + .withMsbConfigurations(msbConfigurationsMock) + .withMessageFactory(messageFactoryMock) + .withChannelManager(channelManagerMock) + .withClock(Clock.systemDefaultZone()) + .withTimeoutManager(new TimeoutManager(1)) + .withCollectorManagerFactory(collectorManagerFactoryMock) + .build(); + + Callback onEnd = mock(Callback.class); + when(eventHandlers.onEnd()).thenReturn(onEnd); + + Collector collector = createCollector(); + collector.listenForResponses(); + + Acknowledge ack = new Acknowledge.Builder().withResponderId(responderId).withResponsesRemaining(2).withTimeoutMs(timeoutMsInAck) + .build(); + Message responseMessageWithAck = TestUtils.createMsbResponseMessageWithAckNoPayload(ack, TOPIC_RESPONSE, originalMessage.getCorrelationId()); + notifyMessagesConsumed(collector, 1); + collector.handleMessage(responseMessageWithAck, null); + + //simulate responder response + Acknowledge responseAck = new Acknowledge.Builder().withResponderId(responderId).withResponsesRemaining(-1).build(); + ObjectMapper payloadMapper = TestUtils.createMessageMapper(); + JsonNode payloadNode = payloadMapper.readValue(String.format("{\"body\": \"%s\" }", "test response payload body"), JsonNode.class); + + Message responderMessage1 = TestUtils.createMsbResponseMessage(responseAck, payloadNode, TOPIC_RESPONSE, "someCorrelationId"); + Message responderMessage2 = TestUtils.createMsbResponseMessage(responseAck, payloadNode, TOPIC_RESPONSE, "someCorrelationId"); + + notifyMessagesConsumed(collector, 2); + + //send first message + collector.handleMessage(responderMessage1, null); + + //send second message after initial response + Thread.sleep(200); + collector.handleMessage(responderMessage2, null); + + verify(onEnd, timeout(1500)).call(any()); + } + + @Test + public void testEndUnregisterCollector() { + Collector collector = createCollector(); + + collector.end(); + + verify(collectorManagerMock).unregisterCollector(collector); + } + + @Test + public void testEndHandlerEndCalled() { + Callback onEnd = mock(Callback.class); + when(eventHandlers.onEnd()).thenReturn(onEnd); + Collector collector = createCollector(); + + collector.end(); + + verify(onEnd).call(any()); + } + + @Test + public void testProcessAckNull() { + Collector collector = createCollector(); + + collector.processAck(null); + + verify(timeoutManagerMock, never()).enableResponseTimeout(anyInt(), any()); + } + + @Test + public void testProcessAckPerResponder() { + int timeoutMs = 5000; + ArgumentCaptor timeoutCaptor = ArgumentCaptor.forClass(Integer.class); + Collector collector = createCollector(); + + collector.processAck(new Acknowledge.Builder().withResponderId("a").withTimeoutMs(timeoutMs).build()); + + verify(timeoutManagerMock).enableResponseTimeout(timeoutCaptor.capture(), any()); + assertThat(timeoutCaptor.getValue()).isBetween(1, timeoutMs); + } + + @Test + public void testProcessAckWillTakeDefaultTimeoutIsMaxAndNotCallTimerAgain() { + int timeoutMs = 1500; + when(msbConfigurationsMock.getDefaultResponseTimeout()).thenReturn(3000); + when(requestOptionsMock.getResponseTimeout()).thenReturn(null); + Collector collector = createCollector(); + + collector.processAck(new Acknowledge.Builder().withResponderId("a").withTimeoutMs(timeoutMs).build()); + + verify(timeoutManagerMock, never()).enableResponseTimeout(anyInt(), any()); + } + + @Test + public void testProcessAckExtendsTimeout() throws Exception { + int initialTimeout = 50; + int timeoutFromAck = 500; + + ScheduledFuture timeoutTask1 = mock(ScheduledFuture.class); + ScheduledFuture timeoutTask2= mock(ScheduledFuture.class); + + when(requestOptionsMock.getResponseTimeout()).thenReturn(initialTimeout); + Collector collector = createCollector(); + + doReturn(timeoutTask1) + .doReturn(timeoutTask2) + .when(timeoutManagerMock).enableResponseTimeout(anyInt(), same(collector)); + + collector.waitForResponses(); + collector.processAck(new Acknowledge.Builder().withResponderId("irrelevant").withTimeoutMs(timeoutFromAck).build()); + + ArgumentCaptor intCaptor = ArgumentCaptor.forClass(Integer.class); + + verify(timeoutTask1).cancel(anyBoolean()); + verify(timeoutTask2, never()).cancel(anyBoolean()); + verify(timeoutManagerMock, times(2)).enableResponseTimeout(intCaptor.capture(), same(collector)); + } + + @Test + public void testEndHandlerTimersStopped() { + ScheduledFuture ackTimerMock = mock(ScheduledFuture.class); + ScheduledFuture timeoutTimerMock = mock(ScheduledFuture.class); + when(timeoutManagerMock.enableAckTimeout(anyInt(), any())).thenReturn(ackTimerMock); + when(timeoutManagerMock.enableResponseTimeout(anyInt(), any())).thenReturn(timeoutTimerMock); + when(requestOptionsMock.getAckTimeout()).thenReturn(100); + when(requestOptionsMock.getResponseTimeout()).thenReturn(100); + + Collector collector = createCollector(); + collector.listenForResponses(); + collector.waitForAcks(); + collector.waitForResponses(); + + verify(timeoutManagerMock).enableAckTimeout(anyInt(), any()); + verify(timeoutManagerMock).enableResponseTimeout(anyInt(), any()); + + collector.end(); + + verify(ackTimerMock).cancel(anyBoolean()); + verify(timeoutTimerMock).cancel(anyBoolean()); + } + @Test public void testListenForResponses() { - Collector collector = new Collector<>(TOPIC, originalMessage, requestOptionsMock, msbContext, eventHandlers, new TypeReference() {}); + Collector collector = createCollector(); collector.listenForResponses(); verify(collectorManagerMock).registerCollector(collector); } + + @Test + public void testWaitForResponses() throws InterruptedException { + int timeoutMs = 1000; + int initCollectorAfter = 50; + ArgumentCaptor timeoutCaptor = ArgumentCaptor.forClass(Integer.class); + when(requestOptionsMock.getResponseTimeout()).thenReturn(timeoutMs); + Collector collector = createCollector(); + Thread.sleep(initCollectorAfter); + collector.waitForResponses(); + + verify(timeoutManagerMock).enableResponseTimeout(timeoutCaptor.capture(), any()); + int timeoutLeftToWait = timeoutMs - initCollectorAfter; + assertThat(timeoutCaptor.getValue()).isBetween(1, timeoutLeftToWait); + } + + @Test + public void testWaitForResponsesReceivedGreaterTimeout() throws InterruptedException { + int timeoutMs = 1000; + int updatedTimeoutMs = 2000; + int receivedAckAfter = 50; + ArgumentCaptor timeoutCaptor = ArgumentCaptor.forClass(Integer.class); + when(requestOptionsMock.getResponseTimeout()).thenReturn(timeoutMs); + Collector collector = createCollector(); + Thread.sleep(receivedAckAfter); + collector.processAck(new Acknowledge.Builder().withResponderId("a").withTimeoutMs(updatedTimeoutMs).build()); + collector.waitForResponses(); + + verify(timeoutManagerMock, times(2)).enableResponseTimeout(timeoutCaptor.capture(), any()); + int timeoutLeftToWait = updatedTimeoutMs - receivedAckAfter; + assertThat(timeoutCaptor.getValue()).isBetween(1, timeoutLeftToWait); + } + + @Test + public void testMultipleAckTimeouts() throws InterruptedException { + Collector collector = createCollector(); + + int numAckSequenceRepeats = 5; + int numAckTimeouts = 4; + int numAskTimeoutRepeats = 3; + + IntStream.range(0, numAckSequenceRepeats).forEach((i)->{ + IntStream.range(0, numAckTimeouts).forEach((j)->{ + Acknowledge ack = new Acknowledge + .Builder() + .withResponderId("tc") + .withTimeoutMs(1000 + j * 700) + .build(); + IntStream.range(0, numAskTimeoutRepeats).forEach((k)->{ + collector.processAck(ack); + }); + }); + }); + + collector.waitForResponses(); + // call enableResponseTimeout() on waitForResponses() call and on each timeout change + verify(timeoutManagerMock, times(numAckTimeouts * numAckSequenceRepeats + 1)).enableResponseTimeout(anyInt(), any()); + } + + @Test + public void testWaitForAcks() { + int timeoutMs = 1000; + ArgumentCaptor timeoutCaptor = ArgumentCaptor.forClass(Integer.class); + when(requestOptionsMock.getAckTimeout()).thenReturn(timeoutMs); + Collector collector = createCollector(); + + //set waitForAcksUntil value + collector.listenForResponses(); + collector.waitForAcks(); + + verify(timeoutManagerMock).enableAckTimeout(timeoutCaptor.capture(), any()); + assertThat(timeoutCaptor.getValue()).isBetween(500, timeoutMs); + } + + @Test + public void testWaitForAcksMultipleInvocations() { + int timeoutMs = 1000; + + when(requestOptionsMock.getAckTimeout()).thenReturn(timeoutMs); + Collector collector = createCollector(); + + when(timeoutManagerMock.enableAckTimeout(anyInt(), any())).thenReturn(mock(ScheduledFuture.class)); + + collector.listenForResponses(); + collector.waitForAcks(); + collector.waitForAcks(); + collector.waitForAcks(); + + verify(timeoutManagerMock, times(1)).enableAckTimeout(anyInt(), any()); + } + + private Collector createCollector() { + return new Collector(TOPIC, originalMessage, requestOptionsMock, msbContext, eventHandlers, new TypeReference() { + }) { + MessageContext createMessageContext(AcknowledgementHandler acknowledgementHandler, Message originalMessage) { + return messageContextMock; + } + + }; + } + + private void notifyMessagesConsumed(Collector collector, int messagesCount) { + for(int i = 0; i < messagesCount; i++) { + collector.notifyMessageConsumed(); + } + } + + private void notifyMessagesLost(Collector collector, int messagesCount) { + for(int i = 0; i < messagesCount; i++) { + collector.notifyConsumedMessageIsLost(); + } + } + + } \ No newline at end of file diff --git a/core/src/test/java/io/github/tcdl/msb/collector/TimeoutManagerTest.java b/core/src/test/java/io/github/tcdl/msb/collector/TimeoutManagerTest.java index ccbba0dc..5d7c5412 100644 --- a/core/src/test/java/io/github/tcdl/msb/collector/TimeoutManagerTest.java +++ b/core/src/test/java/io/github/tcdl/msb/collector/TimeoutManagerTest.java @@ -4,7 +4,8 @@ import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; - +import io.github.tcdl.msb.support.TestUtils; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -14,7 +15,12 @@ public class TimeoutManagerTest { @Mock - Collector mockCollector; + private Collector mockCollector; + + @Before + public void setUp() { + when(mockCollector.getRequestMessage()).thenReturn(TestUtils.createSimpleRequestMessage("123")); + } @Test public void testEnableResponseTimeout() { @@ -23,6 +29,14 @@ public void testEnableResponseTimeout() { verify(mockCollector, timeout(50)).end(); } + @Test + public void testEnableResponseTimeoutRejected() { + TimeoutManager timeoutManager = new TimeoutManager(1); + timeoutManager.shutdown(); + timeoutManager.enableResponseTimeout(10, mockCollector); + verify(mockCollector, never()).end(); + } + @Test public void testEnableResponseTimeoutMultipleTimesLastWin() { TimeoutManager timeoutManager = new TimeoutManager(2); @@ -40,6 +54,14 @@ public void testEnableAckTimeoutPositive() { verify(mockCollector, timeout(50)).end(); } + @Test + public void testEnableAckTimeoutRejected() { + TimeoutManager timeoutManager = new TimeoutManager(1); + timeoutManager.shutdown(); + timeoutManager.enableAckTimeout(10, mockCollector); + verify(mockCollector, never()).end(); + } + @Test public void testEnableAckTimeoutNegative() { TimeoutManager timeoutManager = new TimeoutManager(1); diff --git a/core/src/test/java/io/github/tcdl/msb/config/ServiceDetailsTest.java b/core/src/test/java/io/github/tcdl/msb/config/ServiceDetailsTest.java index cb10ebb1..1041f7dc 100644 --- a/core/src/test/java/io/github/tcdl/msb/config/ServiceDetailsTest.java +++ b/core/src/test/java/io/github/tcdl/msb/config/ServiceDetailsTest.java @@ -3,7 +3,18 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import io.github.tcdl.msb.api.exception.ConfigurationException; +import io.github.tcdl.msb.api.message.Message; +import io.github.tcdl.msb.api.message.MetaMessage; +import io.github.tcdl.msb.api.message.Topics; +import io.github.tcdl.msb.support.TestUtils; +import io.github.tcdl.msb.support.Utils; + +import java.net.InetAddress; +import java.time.Clock; +import java.util.Optional; import org.junit.Test; @@ -30,6 +41,47 @@ public void testServiceDetailsAll() { assertNotNull("expect Pid is not null", serviceDetails.getPid()); } + @Test + public void testServiceDetailsWithUnresolvedHostInfo() { + + String serviceDetailsConfigStr = String.format("serviceDetails = {name = \"%s\", version = \"%s\", instanceId = \"%s\"}", name, version, instanceId); + Config config = ConfigFactory.parseString(serviceDetailsConfigStr); + ServiceDetails serviceDetails = new ServiceDetails.Builder(config.getConfig("serviceDetails")) { + + @Override + protected Optional getHostInfo() { + return Optional.empty(); + } + + }.build(); + + //Verify object methods + assertTrue("expect Hostname is \"unknown\"", "unknown".equals(serviceDetails.getHostname())); + assertNull("expect Ip is null", serviceDetails.getIp()); + + //Verify toString() + assertTrue("expect to find \"hostname=unknown\" substring", + serviceDetails.toString().indexOf("hostname=unknown") > 0); + assertTrue("expect not to find \"ip=\" substring", + serviceDetails.toString().indexOf("ip=") < 0); + + //Verify Json serialization + Clock clock = Clock.systemDefaultZone(); + Message message = new Message.Builder() + .withMetaBuilder(new MetaMessage.Builder(null, clock.instant(), serviceDetails, clock)) + .withId("1111") + .withCorrelationId("2222") + .withTopics(new Topics("TopicTo", null, null)) + .build(); + + String jsonString = Utils.toJson(message, TestUtils.createMessageMapper()); + assertTrue("expect to find \"\"hostname\":\"unknown\"\" substring", + jsonString.indexOf("\"hostname\":\"unknown\"") > 0); + assertTrue("expect not to find \"ip\" substring", + jsonString.indexOf("ip") < 0); + + } + @Test(expected = ConfigurationException.class) public void testServiceDetailsWithoutName() { String serviceDetailsConfigStr = String.format("serviceDetails = {version = \"%s\", instanceId = \"%s\"}", version, instanceId); @@ -73,5 +125,4 @@ public void testNotRepeatableInstanceId() { assertNotEquals("expect different InstanceId values", instanceId1, instanceId2); } - } diff --git a/core/src/test/java/io/github/tcdl/msb/impl/MsbContextImplTest.java b/core/src/test/java/io/github/tcdl/msb/impl/MsbContextImplTest.java index 51476f6a..af1780e1 100644 --- a/core/src/test/java/io/github/tcdl/msb/impl/MsbContextImplTest.java +++ b/core/src/test/java/io/github/tcdl/msb/impl/MsbContextImplTest.java @@ -1,28 +1,25 @@ package io.github.tcdl.msb.impl; -import com.fasterxml.jackson.databind.ObjectMapper; import io.github.tcdl.msb.ChannelManager; import io.github.tcdl.msb.api.ObjectFactory; +import io.github.tcdl.msb.callback.MutableCallbackHandler; import io.github.tcdl.msb.collector.CollectorManagerFactory; import io.github.tcdl.msb.collector.TimeoutManager; import io.github.tcdl.msb.config.MsbConfig; import io.github.tcdl.msb.message.MessageFactory; -import io.github.tcdl.msb.support.TestUtils; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; -import java.time.Clock; - import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @RunWith(MockitoJUnitRunner.class) public class MsbContextImplTest { - private Clock clock = Clock.systemDefaultZone(); - @Mock private MsbConfig msbConfigurationsMock; @@ -41,23 +38,40 @@ public class MsbContextImplTest { @Mock private ObjectFactory objectFactoryMock; - private ObjectMapper messageMapper = TestUtils.createMessageMapper(); + @Mock + private MutableCallbackHandler shutdownCallbackHandlerMock; + + @Mock + private Runnable callbackMock; + + @InjectMocks + private MsbContextImpl msbContext; @Test public void testShutdown() { - MsbContextImpl msbContext = new MsbContextImpl(msbConfigurationsMock, messageFactoryMock, channelManagerMock, clock, - timeoutManagerMock, messageMapper, collectorManagerFactoryMock); msbContext.setObjectFactory(objectFactoryMock); msbContext.shutdown(); - verify(objectFactoryMock).shutdown(); - verify(timeoutManagerMock).shutdown(); - verify(channelManagerMock).shutdown(); + verifyShutdownOnce(); + + msbContext.shutdown(); + msbContext.shutdown(); + verifyShutdownOnce(); + } + + private void verifyShutdownOnce() { + verify(shutdownCallbackHandlerMock, times(1)).runCallbacks(); + verify(timeoutManagerMock, times(1)).shutdown(); + verify(channelManagerMock, times(1)).shutdown(); + } + + @Test + public void testAddShutdownCallback() { + msbContext.addShutdownCallback(callbackMock); + verify(shutdownCallbackHandlerMock).add(callbackMock); } @Test public void testSetObjectFactory() { - MsbContextImpl msbContext = new MsbContextImpl(msbConfigurationsMock, messageFactoryMock, channelManagerMock, clock, - timeoutManagerMock, messageMapper, collectorManagerFactoryMock); msbContext.setObjectFactory(objectFactoryMock); assertEquals(objectFactoryMock, msbContext.getObjectFactory()); } diff --git a/core/src/test/java/io/github/tcdl/msb/impl/ObjectFactoryImplTest.java b/core/src/test/java/io/github/tcdl/msb/impl/ObjectFactoryImplTest.java index 748d1fa7..4e3d2b7f 100644 --- a/core/src/test/java/io/github/tcdl/msb/impl/ObjectFactoryImplTest.java +++ b/core/src/test/java/io/github/tcdl/msb/impl/ObjectFactoryImplTest.java @@ -1,29 +1,14 @@ package io.github.tcdl.msb.impl; -import io.github.tcdl.msb.api.Callback; -import io.github.tcdl.msb.api.MessageTemplate; -import io.github.tcdl.msb.api.ObjectFactory; -import io.github.tcdl.msb.api.PayloadConverter; -import io.github.tcdl.msb.api.RequestOptions; -import io.github.tcdl.msb.api.Requester; -import io.github.tcdl.msb.api.ResponderServer; -import io.github.tcdl.msb.api.monitor.AggregatorStats; -import io.github.tcdl.msb.monitor.aggregator.DefaultChannelMonitorAggregator; +import io.github.tcdl.msb.api.*; import io.github.tcdl.msb.support.TestUtils; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.runners.MockitoJUnitRunner; -import java.util.concurrent.ScheduledExecutorService; - import static org.junit.Assert.assertNotNull; -import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class ObjectFactoryImplTest { @@ -49,32 +34,17 @@ public void testCreateResponderServer() { assertNotNull(expectedResponderServer); } - @Test - public void testShutdownNoAggregator() { - ObjectFactoryImpl objectFactory = new ObjectFactoryImpl(TestUtils.createMsbContextBuilder().build()); - objectFactory.shutdown(); - } - - @Test - public void testShutdownWithAggregator() { - ObjectFactoryImpl objectFactorySpy = spy(new ObjectFactoryImpl(TestUtils.createMsbContextBuilder().build())); - - @SuppressWarnings("unchecked") - Callback mockAggregatorCallback = mock(Callback.class); - DefaultChannelMonitorAggregator mockChannelMonitorAggregator = mock(DefaultChannelMonitorAggregator.class); - when(objectFactorySpy.createDefaultChannelMonitorAggregator(Mockito.eq(mockAggregatorCallback), any(ScheduledExecutorService.class))).thenReturn( - mockChannelMonitorAggregator); - - objectFactorySpy.createChannelMonitorAggregator(mockAggregatorCallback); - objectFactorySpy.shutdown(); - - verify(mockChannelMonitorAggregator).stop(); - } - @Test public void getPayloadConverter() { ObjectFactory objectFactory = new ObjectFactoryImpl(TestUtils.createMsbContextBuilder().build()); PayloadConverter payloadConverter = objectFactory.getPayloadConverter(); assertNotNull(payloadConverter); } + + @Test + public void testCreateFireAndForgetRequester() throws Exception { + ObjectFactory objectFactory = new ObjectFactoryImpl(TestUtils.createMsbContextBuilder().build()); + Requester expectedRequester = objectFactory.createRequesterForFireAndForget(NAMESPACE); + assertNotNull(expectedRequester); + } } diff --git a/core/src/test/java/io/github/tcdl/msb/impl/PayloadConverterImplTest.java b/core/src/test/java/io/github/tcdl/msb/impl/PayloadConverterImplTest.java index ec262152..687cb35f 100644 --- a/core/src/test/java/io/github/tcdl/msb/impl/PayloadConverterImplTest.java +++ b/core/src/test/java/io/github/tcdl/msb/impl/PayloadConverterImplTest.java @@ -21,7 +21,6 @@ public class PayloadConverterImplTest { public void setUp() { MsbContext msbContext = new MsbContextBuilder() .withPayloadMapper(new ObjectMapper()) - .enableChannelMonitorAgent(true) .enableShutdownHook(true) .build(); payloadConverter = msbContext.getObjectFactory().getPayloadConverter(); diff --git a/core/src/test/java/io/github/tcdl/msb/impl/RequesterImplTest.java b/core/src/test/java/io/github/tcdl/msb/impl/RequesterImplTest.java index 73a84060..35b99141 100644 --- a/core/src/test/java/io/github/tcdl/msb/impl/RequesterImplTest.java +++ b/core/src/test/java/io/github/tcdl/msb/impl/RequesterImplTest.java @@ -4,14 +4,11 @@ import io.github.tcdl.msb.ChannelManager; import io.github.tcdl.msb.Consumer; import io.github.tcdl.msb.Producer; -import io.github.tcdl.msb.api.Callback; -import io.github.tcdl.msb.api.MessageTemplate; -import io.github.tcdl.msb.api.RequestOptions; -import io.github.tcdl.msb.api.Requester; +import io.github.tcdl.msb.api.*; +import io.github.tcdl.msb.api.message.Acknowledge; import io.github.tcdl.msb.api.message.Message; import io.github.tcdl.msb.api.message.payload.RestPayload; import io.github.tcdl.msb.collector.Collector; -import io.github.tcdl.msb.events.EventHandlers; import io.github.tcdl.msb.support.TestUtils; import org.junit.Test; import org.junit.runner.RunWith; @@ -20,20 +17,15 @@ import org.mockito.runners.MockitoJUnitRunner; import java.time.Clock; +import java.util.concurrent.CompletableFuture; +import java.util.function.BiConsumer; +import static io.github.tcdl.msb.support.TestUtils.createPayloadWithTextBody; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertThat; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; /** * Created by rdro on 4/27/2015. @@ -41,10 +33,7 @@ @RunWith(MockitoJUnitRunner.class) public class RequesterImplTest { - private static final String NAMESPACE = "test:requester"; - - @Mock - private EventHandlers eventHandlerMock; + private static final String TOPIC = "test:hello:all"; @Mock private ChannelManager channelManagerMock; @@ -60,9 +49,9 @@ public class RequesterImplTest { @Test public void testPublishNoWaitForResponses() throws Exception { - RequesterImpl requester = initRequesterForResponsesWithTimeout(0); + RequesterImpl requester = initRequesterForResponsesWith(0, 0, 0, null, null, null, null); - requester.publish(TestUtils.createSimpleRequestPayload()); + publishByAllMethods(requester); verify(collectorMock, never()).listenForResponses(); verify(collectorMock, never()).waitForResponses(); @@ -70,22 +59,51 @@ public void testPublishNoWaitForResponses() throws Exception { @Test public void testPublishWaitForResponses() throws Exception { - RequesterImpl requester = initRequesterForResponsesWithTimeout(1); + RequesterImpl requester = initRequesterForResponsesWith(1, 0, 0, null, null, null, null); + + publishByAllMethods(requester); + + verify(collectorMock, times(4)).listenForResponses(); + verify(collectorMock, times(4)).waitForResponses(); + } + + private void publishByAllMethods(RequesterImpl requester) { + Message originalMessage = TestUtils.createMsbRequestMessage("some:topic", "body text"); + requester.publish(TestUtils.createSimpleRequestPayload()); + requester.publish(TestUtils.createSimpleRequestPayload(), "tag", "anotherTag"); + requester.publish(TestUtils.createSimpleRequestPayload(), originalMessage); + requester.publish(TestUtils.createSimpleRequestPayload(), originalMessage, "tag", "anotherTag"); + } + + @Test + public void testPublishWaitForResponsesAck() throws Exception { + RequesterImpl requester = initRequesterForResponsesWith(1, 1000, 800, null, null, null, arg -> fail()); - //doReturn(mock(CollectorManager.class)).when(collectorMock).findOrCreateCollectorManager(anyString()); + requester.publish(TestUtils.createSimpleRequestPayload()); + Message responseMessage = TestUtils.createMsbRequestMessage("some:topic", "body text"); + collectorMock.handleMessage(responseMessage, null); + } + + @Test + @SuppressWarnings("unchecked") + public void testPublishHandleErrorResponse() throws Exception { + RuntimeException ex = new RuntimeException(); + BiConsumer errorHandlerMock = mock(BiConsumer.class); + RequesterImpl requester = initRequesterForResponsesWith(1, 1000, 800, (p, c) -> { throw ex; }, null, errorHandlerMock, arg -> fail()); requester.publish(TestUtils.createSimpleRequestPayload()); - verify(collectorMock).listenForResponses(); - verify(collectorMock).waitForResponses(); + Message responseMessage = TestUtils.createMsbRequestMessage("some:topic", "body text"); + collectorMock.handleMessage(responseMessage, null); + verify(errorHandlerMock).accept(eq(ex), eq(responseMessage)); } @Test public void testProducerPublishWithPayload() throws Exception { String bodyText = "Body text"; - RequesterImpl requester = initRequesterForResponsesWithTimeout(0); + RequesterImpl requester = initRequesterForResponsesWith(0, 0, 0, null, null, null, null); ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(Message.class); - RestPayload payload = TestUtils.createPayloadWithTextBody(bodyText); + RestPayload payload = createPayloadWithTextBody(bodyText); requester.publish(payload); @@ -96,8 +114,8 @@ public void testProducerPublishWithPayload() throws Exception { @Test @SuppressWarnings("unchecked") public void testAcknowledgeEventHandlerIsAdded() throws Exception { - Callback onAckMock = mock(Callback.class); - RequesterImpl requester = initRequesterForResponsesWithTimeout(1); + BiConsumer onAckMock = mock(BiConsumer.class); + RequesterImpl requester = initRequesterForResponsesWith(1, 0, 0, null, null, null, null); requester.onAcknowledge(onAckMock); @@ -105,13 +123,14 @@ public void testAcknowledgeEventHandlerIsAdded() throws Exception { assertThat(requester.eventHandlers.onResponse(), not(onAckMock)); assertThat(requester.eventHandlers.onRawResponse(), not(onAckMock)); assertThat(requester.eventHandlers.onEnd(), not(onAckMock)); + assertThat(requester.eventHandlers.onError(), not(onAckMock)); } @Test @SuppressWarnings("unchecked") public void testResponseEventHandlerIsAdded() throws Exception { - Callback onResponseMock = mock(Callback.class); - RequesterImpl requester = initRequesterForResponsesWithTimeout(1); + BiConsumer onResponseMock = mock(BiConsumer.class); + RequesterImpl requester = initRequesterForResponsesWith(1, 0, 0, null, null, null, null); requester.onResponse(onResponseMock); @@ -119,14 +138,14 @@ public void testResponseEventHandlerIsAdded() throws Exception { assertThat(requester.eventHandlers.onResponse(), is(onResponseMock)); assertThat(requester.eventHandlers.onRawResponse(), not(onResponseMock)); assertThat(requester.eventHandlers.onEnd(), not(onResponseMock)); + assertThat(requester.eventHandlers.onError(), not(onResponseMock)); } - @Test @SuppressWarnings("unchecked") public void testRawResponseEventHandlerIsAdded() throws Exception { - Callback onRawResponseMock = mock(Callback.class); - RequesterImpl requester = initRequesterForResponsesWithTimeout(1); + BiConsumer onRawResponseMock = mock(BiConsumer.class); + RequesterImpl requester = initRequesterForResponsesWith(1, 0, 0, null, null, null, null); requester.onRawResponse(onRawResponseMock); @@ -134,14 +153,14 @@ public void testRawResponseEventHandlerIsAdded() throws Exception { assertThat(requester.eventHandlers.onRawResponse(), is(onRawResponseMock)); assertThat(requester.eventHandlers.onResponse(), not(onRawResponseMock)); assertThat(requester.eventHandlers.onEnd(), not(onRawResponseMock)); + assertThat(requester.eventHandlers.onError(), not(onRawResponseMock)); } - @Test @SuppressWarnings("unchecked") public void testEndEventHandlerIsAdded() throws Exception { Callback onEndMock = mock(Callback.class); - RequesterImpl requester = initRequesterForResponsesWithTimeout(1); + RequesterImpl requester = initRequesterForResponsesWith(1, 0, 0, null, null, null, null); requester.onEnd(onEndMock); @@ -149,25 +168,164 @@ public void testEndEventHandlerIsAdded() throws Exception { assertThat(requester.eventHandlers.onResponse(), not(onEndMock)); assertThat(requester.eventHandlers.onRawResponse(), not(onEndMock)); assertThat(requester.eventHandlers.onEnd(), is(onEndMock)); + assertThat(requester.eventHandlers.onError(), not(onEndMock)); + } + + @Test + @SuppressWarnings("unchecked") + public void testErrorEventHandlerIsAdded() throws Exception { + BiConsumer onErrorMock = mock(BiConsumer.class); + RequesterImpl requester = initRequesterForResponsesWith(1, 0, 0, null, null, null, null); + + requester.onError(onErrorMock); + + assertThat(requester.eventHandlers.onAcknowledge(), not(onErrorMock)); + assertThat(requester.eventHandlers.onResponse(), not(onErrorMock)); + assertThat(requester.eventHandlers.onRawResponse(), not(onErrorMock)); + assertThat(requester.eventHandlers.onEnd(), not(onErrorMock)); + assertThat(requester.eventHandlers.onError(), is(onErrorMock)); } @Test @SuppressWarnings("unchecked") public void testNoEventHandlerAdded() throws Exception { Callback onEndMock = mock(Callback.class); - RequesterImpl requester = initRequesterForResponsesWithTimeout(1); + RequesterImpl requester = initRequesterForResponsesWith(1, 0, 0, null, null, null, null); assertThat(requester.eventHandlers.onAcknowledge(), not(onEndMock)); assertThat(requester.eventHandlers.onResponse(), not(onEndMock)); assertThat(requester.eventHandlers.onRawResponse(), not(onEndMock)); assertThat(requester.eventHandlers.onEnd(), not(onEndMock)); + assertThat(requester.eventHandlers.onError(), not(onEndMock)); + } + + @SuppressWarnings("unchecked") + @Test + public void testRequest_customHandlersAreDiscarded() throws Exception { + + BiConsumer customOnResponseHandler = mock(BiConsumer.class); + BiConsumer customOnRawResponseHandler = mock(BiConsumer.class); + BiConsumer customOnAcknowledgeHandler = mock(BiConsumer.class); + BiConsumer customOnErrorHandler = mock(BiConsumer.class); + Callback customOnEndHandler = mock(Callback.class); + + RequesterImpl requester = initRequesterForResponsesWith(1, 0, 0, customOnResponseHandler, customOnAcknowledgeHandler, customOnErrorHandler, customOnEndHandler); + + requester.onRawResponse(customOnRawResponseHandler); + + requester.request(TestUtils.createSimpleRequestPayload()); + + assertThat(requester.eventHandlers.onAcknowledge(), not(customOnAcknowledgeHandler)); + assertThat(requester.eventHandlers.onResponse(), not(customOnResponseHandler)); + assertThat(requester.eventHandlers.onRawResponse(), not(customOnRawResponseHandler)); + assertThat(requester.eventHandlers.onEnd(), not(customOnEndHandler)); + assertThat(requester.eventHandlers.onError(), not(customOnErrorHandler)); + } + + @Test + public void testRequest_responseHandlerCompletesFuture() throws Exception { + RequesterImpl requester = initRequesterForResponsesWith(1, 0, 0, null, null, null, null); + CompletableFuture futureResult = requester.request(TestUtils.createSimpleRequestPayload()); + + assertFalse(futureResult.isDone()); + + RestPayload mockResponsePayload = mock(RestPayload.class); + MessageContext mockMessageContext = mock(MessageContext.class); + + requester.eventHandlers.onResponse().accept(mockResponsePayload, mockMessageContext); + assertTrue(futureResult.isDone()); + assertEquals(mockResponsePayload, futureResult.get()); + } + + @Test + public void testRequest_rawResponseHandlerDoesNotCompleteFuture() throws Exception { + RequesterImpl requester = initRequesterForResponsesWith(1, 0, 0, null, null, null, null); + CompletableFuture futureResult = requester.request(TestUtils.createSimpleRequestPayload()); + + assertFalse(futureResult.isDone()); + + Message responseMessage = TestUtils.createSimpleResponseMessage("anyNamespace"); + MessageContext mockMessageContext = mock(MessageContext.class); + + requester.eventHandlers.onRawResponse().accept(responseMessage, mockMessageContext); + assertFalse(futureResult.isDone()); + } + + @Test + public void testRequest_errorHandlerCancelsFuture() throws Exception { + RequesterImpl requester = initRequesterForResponsesWith(1, 0, 0, null, null, null, null); + CompletableFuture futureResult = requester.request(TestUtils.createSimpleRequestPayload()); + + assertFalse(futureResult.isDone()); + + Message responseMessage = TestUtils.createSimpleResponseMessage("anyNamespace"); + Exception e = new Exception("some message"); + + requester.eventHandlers.onError().accept(e, responseMessage); + assertTrue(futureResult.isCancelled()); + } + + @Test + public void testRequest_endHandlerCancelsNotCompletedFuture() throws Exception { + RequesterImpl requester = initRequesterForResponsesWith(1, 0, 0, null, null, null, null); + CompletableFuture futureResult = requester.request(TestUtils.createSimpleRequestPayload()); + + assertFalse(futureResult.isDone()); + + requester.eventHandlers.onEnd().call(null); + assertTrue(futureResult.isCancelled()); + } + + @Test + public void testRequest_endHandlerDoesNothingWithCompletedFuture() throws Exception { + RequesterImpl requester = initRequesterForResponsesWith(1, 0, 0, null, null, null, null); + CompletableFuture futureResult = requester.request(TestUtils.createSimpleRequestPayload()); + + RestPayload mockResponsePayload = mock(RestPayload.class); + MessageContext mockMessageContext = mock(MessageContext.class); + + requester.eventHandlers.onResponse().accept(mockResponsePayload, mockMessageContext); + requester.eventHandlers.onEnd().call(null); + assertFalse(futureResult.isCancelled()); + } + + @Test + public void testRequest_acknowledgeHandlerCancelsFutureOnNoResponses() throws Exception { + RequesterImpl requester = initRequesterForResponsesWith(1, 0, 0, null, null, null, null); + CompletableFuture futureResult = requester.request(TestUtils.createSimpleRequestPayload()); + + MessageContext mockMessageContext = mock(MessageContext.class); + Acknowledge acknowledge = new Acknowledge.Builder() + .withResponderId("responderId") + .withResponsesRemaining(0) + .withTimeoutMs(0) + .build(); + + requester.eventHandlers.onAcknowledge().accept(acknowledge, mockMessageContext); + assertTrue(futureResult.isCancelled()); + } + + @Test + public void testRequest_acknowledgeHandlerCancelsFutureOnTooManyResponses() throws Exception{ + RequesterImpl requester = initRequesterForResponsesWith(1, 0, 0, null, null, null, null); + CompletableFuture futureResult = requester.request(TestUtils.createSimpleRequestPayload()); + + MessageContext mockMessageContext = mock(MessageContext.class); + Acknowledge acknowledge = new Acknowledge.Builder() + .withResponderId("responderId") + .withResponsesRemaining(2) + .withTimeoutMs(0) + .build(); + + requester.eventHandlers.onAcknowledge().accept(acknowledge, mockMessageContext); + assertTrue(futureResult.isCancelled()); } @Test public void testRequestMessage() throws Exception { ChannelManager channelManagerMock = mock(ChannelManager.class); Producer producerMock = mock(Producer.class); - when(channelManagerMock.findOrCreateProducer(NAMESPACE)).thenReturn(producerMock); + when(channelManagerMock.findOrCreateProducer(eq(TOPIC), eq(false), eq(RequestOptions.DEFAULTS))).thenReturn(producerMock); ArgumentCaptor messageArgumentCaptor = ArgumentCaptor.forClass(Message.class); MsbContextImpl msbContext = TestUtils.createMsbContextBuilder() @@ -176,7 +334,7 @@ public void testRequestMessage() throws Exception { .build(); RestPayload requestPayload = TestUtils.createSimpleRequestPayload(); - Requester requester = RequesterImpl.create(NAMESPACE, TestUtils.createSimpleRequestOptions(), msbContext, new TypeReference() {}); + Requester requester = RequesterImpl.create(TOPIC, RequestOptions.DEFAULTS, msbContext, new TypeReference(){}); requester.publish(requestPayload); verify(producerMock).publish(messageArgumentCaptor.capture()); @@ -190,7 +348,6 @@ public void testRequestMessage() throws Exception { public void testRequestMessageWithTags() throws Exception { ChannelManager channelManagerMock = mock(ChannelManager.class); Producer producerMock = mock(Producer.class); - when(channelManagerMock.findOrCreateProducer(NAMESPACE)).thenReturn(producerMock); ArgumentCaptor messageArgumentCaptor = ArgumentCaptor.forClass(Message.class); MsbContextImpl msbContext = TestUtils.createMsbContextBuilder() @@ -199,41 +356,59 @@ public void testRequestMessageWithTags() throws Exception { .build(); String tag = "requester-tag"; - String dynamicTag = "dynamic-tag"; + String dynamicTag1 = "dynamic-tag1"; + String dynamicTag2 = "dynamic-tag2"; + String nullTag = null; RestPayload requestPayload = TestUtils.createSimpleRequestPayload(); RequestOptions requestOptions = TestUtils.createSimpleRequestOptionsWithTags(tag); - Requester requester = RequesterImpl.create(NAMESPACE, requestOptions, msbContext, new TypeReference() {}); - requester.publish(requestPayload, dynamicTag); + when(channelManagerMock.findOrCreateProducer(eq(TOPIC), eq(false), eq(requestOptions))).thenReturn(producerMock); + + Requester requester = RequesterImpl.create(TOPIC, requestOptions, msbContext, new TypeReference(){}); + requester.publish(requestPayload, dynamicTag1, dynamicTag2, nullTag); verify(producerMock).publish(messageArgumentCaptor.capture()); Message requestMessage = messageArgumentCaptor.getValue(); - assertArrayEquals(new String[]{tag, dynamicTag}, requestMessage.getTags().toArray()); + assertArrayEquals(new String[]{tag, dynamicTag1, dynamicTag2}, requestMessage.getTags().toArray()); } - private RequesterImpl initRequesterForResponsesWithTimeout(int numberOfResponses) throws Exception { - MessageTemplate messageTemplateMock = mock(MessageTemplate.class); + private RequesterImpl initRequesterForResponsesWith(Integer numberOfResponses, Integer respTimeout, Integer ackTimeout, + BiConsumer onResponse, BiConsumer onAcknowledge, + BiConsumer onError, + Callback endHandler) throws Exception { - RequestOptions requestOptionsMock = new RequestOptions.Builder().withMessageTemplate(messageTemplateMock).withWaitForResponses(numberOfResponses) - .withResponseTimeout(100).build(); + RequestOptions requestOptions = new RequestOptions.Builder() + .withMessageTemplate(mock(MessageTemplate.class)) + .withWaitForResponses(numberOfResponses) + .withResponseTimeout(respTimeout) + .withAckTimeout(ackTimeout) + .build(); - when(channelManagerMock.findOrCreateProducer(anyString())).thenReturn(producerMock); + when(channelManagerMock.findOrCreateProducer(anyString(), eq(false), any(RequestOptions.class))).thenReturn(producerMock); + return setUpRequester(TOPIC, onResponse, onAcknowledge, onError, endHandler, requestOptions); + } + + private RequesterImpl setUpRequester(String namespace, BiConsumer onResponse, BiConsumer onAcknowledge, BiConsumer onError, Callback endHandler, RequestOptions requestOptions) { MsbContextImpl msbContext = TestUtils.createMsbContextBuilder() .withChannelManager(channelManagerMock) .build(); - RequesterImpl requester = spy(RequesterImpl.create(NAMESPACE, requestOptionsMock, msbContext, new TypeReference() {})); + RequesterImpl requester = spy(RequesterImpl.create(namespace, requestOptions, msbContext, new TypeReference() { + })); + requester.onResponse(onResponse) + .onError(onError) + .onAcknowledge(onAcknowledge) + .onEnd(endHandler); - collectorMock = spy(new Collector<>(NAMESPACE, TestUtils.createMsbRequestMessageNoPayload(NAMESPACE), requestOptionsMock, msbContext, eventHandlerMock, - new TypeReference() {})); + collectorMock = spy(new Collector<>(namespace, TestUtils.createMsbRequestMessageNoPayload(namespace), requestOptions, msbContext, requester.eventHandlers, + new TypeReference() { + })); doReturn(collectorMock) .when(requester) - .createCollector(anyString(), any(Message.class), any(RequestOptions.class), any(MsbContextImpl.class), any()); - + .createCollector(any(Message.class), any(RequestOptions.class), any(MsbContextImpl.class), any(), anyBoolean()); return requester; } - } diff --git a/core/src/test/java/io/github/tcdl/msb/impl/ResponderImplTest.java b/core/src/test/java/io/github/tcdl/msb/impl/ResponderImplTest.java index 9ad08641..191cb627 100644 --- a/core/src/test/java/io/github/tcdl/msb/impl/ResponderImplTest.java +++ b/core/src/test/java/io/github/tcdl/msb/impl/ResponderImplTest.java @@ -3,6 +3,7 @@ import io.github.tcdl.msb.ChannelManager; import io.github.tcdl.msb.Producer; import io.github.tcdl.msb.api.MessageTemplate; +import io.github.tcdl.msb.api.RequestOptions; import io.github.tcdl.msb.api.Responder; import io.github.tcdl.msb.api.message.Message; import io.github.tcdl.msb.config.MsbConfig; @@ -14,17 +15,9 @@ import java.time.Clock; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.mockito.Matchers.anyObject; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; public class ResponderImplTest { @@ -56,7 +49,7 @@ public void setUp() { when(msbContextSpy.getChannelManager()).thenReturn(mockChannelManager); when(msbContextSpy.getMessageFactory()).thenReturn(spyMessageFactory); - when(mockChannelManager.findOrCreateProducer(anyString())).thenReturn(mockProducer); + when(mockChannelManager.findOrCreateProducer(anyString(), eq(true), any(RequestOptions.class))).thenReturn(mockProducer); responder = new ResponderImpl(messageTemplate, originalMessage, msbContextSpy); } @@ -73,7 +66,7 @@ public void testProducerWasCreatedForProperTopic() { ArgumentCaptor argument = ArgumentCaptor.forClass(String.class); responder.send(""); - verify(mockChannelManager).findOrCreateProducer(argument.capture()); + verify(mockChannelManager).findOrCreateProducer(argument.capture(), anyBoolean(), any(RequestOptions.class)); assertEquals(originalMessage.getTopics().getResponse(), argument.getValue()); } diff --git a/core/src/test/java/io/github/tcdl/msb/impl/ResponderServerImplTest.java b/core/src/test/java/io/github/tcdl/msb/impl/ResponderServerImplTest.java index 2e7ffd18..27425dff 100644 --- a/core/src/test/java/io/github/tcdl/msb/impl/ResponderServerImplTest.java +++ b/core/src/test/java/io/github/tcdl/msb/impl/ResponderServerImplTest.java @@ -1,31 +1,29 @@ package io.github.tcdl.msb.impl; import com.fasterxml.jackson.core.type.TypeReference; +import com.google.common.collect.Sets; import io.github.tcdl.msb.ChannelManager; import io.github.tcdl.msb.MessageHandler; import io.github.tcdl.msb.Producer; -import io.github.tcdl.msb.api.MessageTemplate; -import io.github.tcdl.msb.api.RequestOptions; -import io.github.tcdl.msb.api.Responder; -import io.github.tcdl.msb.api.ResponderServer; +import io.github.tcdl.msb.api.*; import io.github.tcdl.msb.api.message.Message; import io.github.tcdl.msb.api.message.payload.RestPayload; +import io.github.tcdl.msb.api.metrics.Gauge; +import io.github.tcdl.msb.api.metrics.MetricSet; import io.github.tcdl.msb.support.TestUtils; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.anyObject; -import static org.mockito.Mockito.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyZeroInteractions; -import static org.mockito.Mockito.when; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.same; +import static org.mockito.Mockito.*; public class ResponderServerImplTest { @@ -45,8 +43,15 @@ public void setUp() { @Test public void testResponderServerProcessPayloadSuccess() throws Exception { - ResponderServer.RequestHandler handler = (request, responder) -> { - }; + Message originalMessage = TestUtils.createSimpleRequestMessage(TOPIC); + + ResponderServer.RequestHandler, Object, Map>> handler = + (request, responderContext) -> { + assertEquals("MessageContext must contain original message during message handler execution", + originalMessage, MsbThreadContext.getMessageContext().getOriginalMessage()); + assertEquals("MsbThreadContext must contain a Request during message handler execution", + request, MsbThreadContext.getRequest()); + }; ArgumentCaptor subscriberCaptor = ArgumentCaptor.forClass(MessageHandler.class); ChannelManager spyChannelManager = spy(msbContext.getChannelManager()); @@ -54,19 +59,55 @@ public void testResponderServerProcessPayloadSuccess() throws Exception { when(spyMsbContext.getChannelManager()).thenReturn(spyChannelManager); - ResponderServerImpl responderServer = ResponderServerImpl - .create(TOPIC, requestOptions.getMessageTemplate(), spyMsbContext, handler, new TypeReference() {}); + ResponderOptions responderOptions = new ResponderOptions.Builder().withBindingKeys(Collections.emptySet()).withMessageTemplate(messageTemplate).build(); + + ResponderServerImpl, Object, Map>> responderServer = + ResponderServerImpl.create(TOPIC,responderOptions, spyMsbContext, handler, null, + new TypeReference, Object, Map>>() {}); ResponderServerImpl spyResponderServer = (ResponderServerImpl) spy(responderServer).listen(); - verify(spyChannelManager).subscribe(anyString(), subscriberCaptor.capture()); + verify(spyChannelManager).subscribe(anyString(), any(ResponderOptions.class), subscriberCaptor.capture()); - Message originalMessage = TestUtils.createSimpleRequestMessage(TOPIC); - subscriberCaptor.getValue().handleMessage(originalMessage); + assertNull("MessageContext must be absent outside message handler execution", MsbThreadContext.getMessageContext()); + assertNull("Request must be absent outside message handler execution", MsbThreadContext.getRequest()); + subscriberCaptor.getValue().handleMessage(originalMessage, null); + assertNull("MessageContext must be absent outside message handler execution", MsbThreadContext.getMessageContext()); + assertNull("Request must be absent outside message handler execution", MsbThreadContext.getRequest()); verify(spyResponderServer).onResponder(anyObject()); } + @Test + public void testResponderServerMetrics() { + ResponderServer.RequestHandler, Object, Map>> handler = + (request, responderContext) -> {}; + ResponderOptions responderOptions = new ResponderOptions.Builder().withBindingKeys(Collections.emptySet()).withMessageTemplate(messageTemplate).build(); + MsbContextImpl spyMsbContext = spy(msbContext); + + ChannelManager spyChannelManager = spy(msbContext.getChannelManager()); + when(spyMsbContext.getChannelManager()).thenReturn(spyChannelManager); + when(spyChannelManager.getAvailableMessageCount(anyString())).thenReturn(Optional.of(666L)); + when(spyChannelManager.isConnected(anyString())).thenReturn(Optional.of(true)); + + ResponderServer responderServer = + ResponderServerImpl.create(TOPIC,responderOptions, spyMsbContext, handler, null, + new TypeReference, Object, Map>>() {}) + .listen(); + + + MetricSet metricSet = responderServer.getMetrics(); + Gauge availableMessageCount = (Gauge) metricSet.getMetric("availableMessageCount"); + Gauge isConsumerConnected = (Gauge) metricSet.getMetric("consumerConnected"); + + assertEquals(666L, availableMessageCount.getValue().longValue()); + assertTrue(isConsumerConnected.getValue()); + + verify(spyChannelManager, times(1)).subscribe(eq(TOPIC), any(ResponderOptions.class), any(MessageHandler.class)); + verify(spyChannelManager, times(1)).getAvailableMessageCount(TOPIC); + verify(spyChannelManager, times(1)).isConnected(TOPIC); + } + @Test(expected = NullPointerException.class) public void testResponderServerProcessErrorNoHandler() throws Exception { msbContext.getObjectFactory().createResponderServer(TOPIC, messageTemplate, null); @@ -74,67 +115,97 @@ public void testResponderServerProcessErrorNoHandler() throws Exception { @Test public void testResponderServerProcessUnexpectedPayload() throws Exception { - ResponderServer.RequestHandler handler = (request, responder) -> { - }; + ResponderServer.RequestHandler handler = (request, responderContext) -> {}; String bodyText = "some body"; Message incomingMessage = TestUtils.createMsbRequestMessage(TOPIC, bodyText); + ResponderOptions responderOptions = new ResponderOptions.Builder().withMessageTemplate(messageTemplate).build(); + ResponderServerImpl responderServer = ResponderServerImpl - .create(TOPIC, messageTemplate, msbContext, handler, new TypeReference() {}); + .create(TOPIC, responderOptions, msbContext, handler, null, new TypeReference() {}); responderServer.listen(); // simulate incoming request - ArgumentCaptor responseCaptor = ArgumentCaptor.forClass(RestPayload.class); - ResponderImpl responder = spy( - new ResponderImpl(messageTemplate, incomingMessage, msbContext)); - responderServer.onResponder(responder); - verify(responder).send(responseCaptor.capture()); - assertEquals(ResponderServer.PAYLOAD_CONVERSION_ERROR_CODE, responseCaptor.getValue().getStatusCode().intValue()); - assertNotNull(responseCaptor.getValue().getStatusMessage()); + ResponderImpl responder = spy(new ResponderImpl(messageTemplate, incomingMessage, msbContext)); + + AcknowledgementHandler acknowledgeHandler = mock(AcknowledgementHandler.class); + ResponderContext responderContext = responderServer.createResponderContext(responder, acknowledgeHandler, incomingMessage); + + responderServer.onResponder(responderContext); + verify(responder).sendAck(0, 0); + verify(acknowledgeHandler).confirmMessage(); } @Test public void testResponderServerProcessHandlerThrowException() throws Exception { String exceptionMessage = "Test exception message"; Exception error = new Exception(exceptionMessage); - ResponderServer.RequestHandler handler = (request, responder) -> { + ResponderServer.RequestHandler handler = (request, responderContext) -> { throw error; }; + + ResponderOptions responderOptions = new ResponderOptions.Builder().withMessageTemplate(messageTemplate).build(); + + ResponderServerImpl responderServer = ResponderServerImpl + .create(TOPIC, responderOptions, msbContext, handler, null, new TypeReference() {}); + responderServer.listen(); + + // simulate incoming request + Message originalMessage = TestUtils.createMsbRequestMessageNoPayload(TOPIC); + ResponderImpl responder = spy( + new ResponderImpl(messageTemplate, originalMessage, msbContext)); + AcknowledgementHandler acknowledgeHandler = mock(AcknowledgementHandler.class); + ResponderContext responderContext = responderServer.createResponderContext(responder, acknowledgeHandler, originalMessage); + + responderServer.onResponder(responderContext); + verify(responder).sendAck(0, 0); + verify(acknowledgeHandler).confirmMessage(); + } + + @Test + public void testResponderServerProcessCustomHandlerThrowException() throws Exception { + String exceptionMessage = "Test exception message"; + Exception error = new Exception(exceptionMessage); + ResponderServer.RequestHandler handler = (request, responderContext) -> { throw error; }; + ResponderOptions responderOptions = new ResponderOptions.Builder().withMessageTemplate(messageTemplate).build(); + ResponderServer.ErrorHandler errorHandlerMock = mock(ResponderServer.ErrorHandler.class); ResponderServerImpl responderServer = ResponderServerImpl - .create(TOPIC, messageTemplate, msbContext, handler, new TypeReference() {}); + .create(TOPIC, responderOptions, msbContext, handler, errorHandlerMock, new TypeReference() {}); responderServer.listen(); // simulate incoming request - ArgumentCaptor responseCaptor = ArgumentCaptor.forClass(RestPayload.class); - ResponderImpl responder = spy( - new ResponderImpl(messageTemplate, TestUtils.createMsbRequestMessageNoPayload(TOPIC), msbContext)); - responderServer.onResponder(responder); + Message originalMessage = TestUtils.createMsbRequestMessageNoPayload(TOPIC); + Responder responder = mock(Responder.class); + AcknowledgementHandler acknowledgeHandler = mock(AcknowledgementHandler.class); + ResponderContext responderContext = responderServer.createResponderContext(responder, acknowledgeHandler, originalMessage); + + responderServer.onResponder(responderContext); - verify(responder).send(responseCaptor.capture()); - assertEquals(ResponderServer.INTERNAL_SERVER_ERROR_CODE, responseCaptor.getValue().getStatusCode().intValue()); - assertEquals(exceptionMessage, responseCaptor.getValue().getStatusMessage()); + verify(errorHandlerMock).handle(eq(error), eq(originalMessage)); } @Test public void testCreateResponderWithResponseTopic() { - ResponderServer.RequestHandler handler = (request, responder) -> { + ResponderServer.RequestHandler handler = (request, responderContext) -> { }; ChannelManager mockChannelManager = mock(ChannelManager.class); Producer mockProducer = mock(Producer.class); - when(mockChannelManager.findOrCreateProducer(anyString())).thenReturn(mockProducer); + when(mockChannelManager.findOrCreateProducer(anyString(), eq(true), any(RequestOptions.class))).thenReturn(mockProducer); MsbContextImpl msbContext1 = new TestUtils.TestMsbContextBuilder() .withChannelManager(mockChannelManager) .build(); + ResponderOptions responderOptions = new ResponderOptions.Builder().withBindingKeys(Collections.emptySet()).withMessageTemplate(messageTemplate).build(); ResponderServerImpl responderServer = ResponderServerImpl - .create(TOPIC, messageTemplate, msbContext1, handler, new TypeReference() {}); + .create(TOPIC, responderOptions, msbContext1, handler, null, new TypeReference() {}); Message incomingMessage = TestUtils.createMsbRequestMessageNoPayload(TOPIC); Responder responder = responderServer.createResponder(incomingMessage); - assertEquals(incomingMessage, responder.getOriginalMessage()); + ResponderContext responderContext = responderServer.createResponderContext(responder, null, incomingMessage); + assertEquals(incomingMessage, responderContext.getOriginalMessage()); responder.sendAck(1, 1); responder.send("response"); @@ -143,9 +214,48 @@ public void testCreateResponderWithResponseTopic() { verify(mockProducer, times(2)).publish(any(Message.class)); } + @Test + public void testCreateResponderWithRoutingKeys() throws Exception { + + ChannelManager mockChannelManager = mock(ChannelManager.class); + MsbContextImpl msbContext = new TestUtils.TestMsbContextBuilder() + .withChannelManager(mockChannelManager) + .build(); + + Set bindingKeys = Sets.newHashSet("routing.key.one", "routing.key.two"); + + ResponderServer.RequestHandler requestHandler = (request, responderContext) -> {}; + + ResponderOptions responderOptions = new ResponderOptions.Builder().withBindingKeys(bindingKeys).withMessageTemplate(messageTemplate).build(); + ResponderServerImpl responderServer = ResponderServerImpl + .create(TOPIC, responderOptions, msbContext, requestHandler, null, new TypeReference() {}); + + responderServer.listen(); + verify(mockChannelManager).subscribe(eq(TOPIC), eq(responderOptions), any(MessageHandler.class)); + } + + @Test + public void testCreateResponderWithoutRoutingKeys() throws Exception { + + ChannelManager mockChannelManager = mock(ChannelManager.class); + MsbContextImpl msbContext = new TestUtils.TestMsbContextBuilder() + .withChannelManager(mockChannelManager) + .build(); + + ResponderServer.RequestHandler requestHandler = (request, responderContext) -> {}; + + ResponderOptions responderOptions = new ResponderOptions.Builder().withBindingKeys(Collections.emptySet()).withMessageTemplate(messageTemplate).build(); + + ResponderServerImpl responderServer = ResponderServerImpl + .create(TOPIC, responderOptions, msbContext, requestHandler, null, new TypeReference() {}); + + responderServer.listen(); + verify(mockChannelManager).subscribe(eq(TOPIC), same(responderOptions), any(MessageHandler.class)); + } + @Test public void testCreateResponderNoResponseTopic() { - ResponderServer.RequestHandler handler = (request, responder) -> { + ResponderServer.RequestHandler handler = (request, responderContext) -> { }; ChannelManager mockChannelManager = mock(ChannelManager.class); @@ -153,12 +263,13 @@ public void testCreateResponderNoResponseTopic() { .withChannelManager(mockChannelManager) .build(); + ResponderOptions responderOptions = new ResponderOptions.Builder().withMessageTemplate(messageTemplate).build(); + ResponderServerImpl responderServer = ResponderServerImpl - .create(TOPIC, messageTemplate, msbContext, handler, new TypeReference() {}); + .create(TOPIC, responderOptions, msbContext, handler, null, new TypeReference() {}); Message incomingMessage = TestUtils.createMsbBroadcastMessageNoPayload(TOPIC); Responder responder = responderServer.createResponder(incomingMessage); - assertEquals(incomingMessage, responder.getOriginalMessage()); responder.sendAck(1, 1); responder.send("response"); @@ -166,4 +277,23 @@ public void testCreateResponderNoResponseTopic() { // Verify that no messages were published verifyZeroInteractions(mockChannelManager); } + + @Test + public void testStop() throws Exception { + ResponderServer.RequestHandler doNothingHandler = (request, responderContext) -> {}; + + ChannelManager mockChannelManager = mock(ChannelManager.class); + MsbContextImpl msbContext = new TestUtils.TestMsbContextBuilder() + .withChannelManager(mockChannelManager) + .build(); + + ResponderOptions responderOptions = new ResponderOptions.Builder().withMessageTemplate(messageTemplate).build(); + + ResponderServerImpl responderServer = ResponderServerImpl.create( + TOPIC, responderOptions, msbContext, doNothingHandler, null, new TypeReference() {} + ); + + responderServer.stop(); + verify(mockChannelManager).unsubscribe(TOPIC); + } } diff --git a/core/src/test/java/io/github/tcdl/msb/impl/SimpleMessageHandlerResolverImplTest.java b/core/src/test/java/io/github/tcdl/msb/impl/SimpleMessageHandlerResolverImplTest.java new file mode 100644 index 00000000..d6932f4d --- /dev/null +++ b/core/src/test/java/io/github/tcdl/msb/impl/SimpleMessageHandlerResolverImplTest.java @@ -0,0 +1,40 @@ +package io.github.tcdl.msb.impl; + +import io.github.tcdl.msb.MessageHandler; +import io.github.tcdl.msb.api.message.Message; +import io.github.tcdl.msb.support.TestUtils; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import java.util.UUID; + +import static org.junit.Assert.assertEquals; + +@RunWith(MockitoJUnitRunner.class) +public class SimpleMessageHandlerResolverImplTest { + + private final static String LOGGING_NAME = UUID.randomUUID().toString(); + + @Mock + MessageHandler messageHandler; + + Message message; + + SimpleMessageHandlerResolverImpl resolver; + + @Before + public void setUp() { + message = TestUtils.createSimpleResponseMessage("any"); + resolver = new SimpleMessageHandlerResolverImpl(messageHandler, LOGGING_NAME); + } + + @Test + public void testMessageHandlerResolutionByAnyMessage() { + assertEquals(messageHandler, resolver.resolveMessageHandler(message).get()); + assertEquals(LOGGING_NAME, resolver.getLoggingName()); + } + +} diff --git a/core/src/test/java/io/github/tcdl/msb/message/MessageFactoryTest.java b/core/src/test/java/io/github/tcdl/msb/message/MessageFactoryTest.java index 80401080..ac9a557e 100644 --- a/core/src/test/java/io/github/tcdl/msb/message/MessageFactoryTest.java +++ b/core/src/test/java/io/github/tcdl/msb/message/MessageFactoryTest.java @@ -61,6 +61,7 @@ public void testCreateRequestMessageWithPayload() { TestUtils.assertRawPayloadContainsBodyText(bodyText, message); assertNull(message.getAck()); + assertNull(message.getTopics().getForward()); } @Test @@ -71,6 +72,7 @@ public void testCreateRequestMessageWithoutPayload() { assertNull(message.getRawPayload()); assertNull(message.getAck()); + assertNull(message.getTopics().getForward()); } @Test @@ -88,6 +90,7 @@ public void testCreateResponseMessageWithPayloadAndAck() { TestUtils.assertRawPayloadContainsBodyText(bodyText, message); assertEquals(ack, message.getAck()); + assertNull(message.getTopics().getForward()); } @Test @@ -98,6 +101,7 @@ public void testCreateResponseMessageWithoutPayloadAndAck() { assertNull(message.getRawPayload()); assertNull(message.getAck()); + assertNull(message.getTopics().getForward()); } @Test @@ -110,18 +114,52 @@ public void testBroadcastMessage() { TestUtils.assertRawPayloadContainsBodyText(bodyText, message); assertNull(message.getAck()); + assertNull(message.getTopics().getForward()); } @Test public void testCreateRequestMessageBuilder() { String namespace = "test:request-builder"; - Builder requestMessageBuilder = messageFactory.createRequestMessageBuilder(namespace, messageOptions, null); + Builder requestMessageBuilder = messageFactory.createRequestMessageBuilder(namespace, null, messageOptions, null); Message message = requestMessageBuilder.build(); assertNotNull(message.getCorrelationId()); assertThat(message.getTopics().getTo(), is(namespace)); assertThat(message.getTopics().getResponse(), notNullValue()); + assertNull(message.getTopics().getForward()); + } + + @Test + public void testCreateRequestMessageBuilderWithForward() { + String namespace = "test:request-builder"; + + String forwardNamespace = "test:forward"; + + Builder requestMessageBuilder = messageFactory.createRequestMessageBuilder(namespace, forwardNamespace, messageOptions, null); + Message message = requestMessageBuilder.build(); + + assertNotNull(message.getCorrelationId()); + assertThat(message.getTopics().getTo(), is(namespace)); + assertThat(message.getTopics().getResponse(), nullValue()); + assertThat(message.getTopics().getRoutingKey(), nullValue()); + assertThat(message.getTopics().getForward(), is(forwardNamespace)); + } + + @Test + public void testCreateRequestMessageBuilderRoutingKey() { + String namespace = "test:request-builder"; + String routingKey = "banana.key"; + String forwardNamespace = "test:forward"; + + Builder requestMessageBuilder = messageFactory.createRequestMessageBuilder(namespace, forwardNamespace, routingKey, messageOptions, null); + Message message = requestMessageBuilder.build(); + + assertNotNull(message.getCorrelationId()); + assertThat(message.getTopics().getTo(), is(namespace)); + assertThat(message.getTopics().getResponse(), nullValue()); + assertThat(message.getTopics().getRoutingKey(), is(routingKey)); + assertThat(message.getTopics().getForward(), is(forwardNamespace)); } @Test @@ -130,7 +168,7 @@ public void testCreateRequestMessageBuilderWithTags() { String[] tags = new String[] {"tag1", "tag2"}; MessageTemplate messageTemplate = TestUtils.createSimpleMessageTemplate(tags); - Builder requestMessageBuilder = messageFactory.createRequestMessageBuilder(namespace, messageTemplate, null); + Builder requestMessageBuilder = messageFactory.createRequestMessageBuilder(namespace, null, messageTemplate, null); Message message = requestMessageBuilder.build(); assertArrayEquals(tags, message.getTags().toArray()); @@ -143,7 +181,7 @@ public void testCreateRequestMessageBuilderWithUniqueTags() { MessageTemplate messageTemplate = TestUtils.createSimpleMessageTemplate(tags); - Builder requestMessageBuilder = messageFactory.createRequestMessageBuilder(namespace, messageTemplate, null); + Builder requestMessageBuilder = messageFactory.createRequestMessageBuilder(namespace, null, messageTemplate, null); Message message = requestMessageBuilder.build(); String[] uniqueTags = Stream.of(tags).distinct().collect(Collectors.toList()).toArray(new String[] {}); @@ -155,12 +193,13 @@ public void testCreateRequestMessageBuilderFromOriginalMessage() { String namespace = "test:request-builder"; Message originalMessage = TestUtils.createMsbRequestMessageNoPayload(namespace); - Builder requestMessageBuilder = messageFactory.createRequestMessageBuilder(namespace, messageOptions, originalMessage); + Builder requestMessageBuilder = messageFactory.createRequestMessageBuilder(namespace, null, messageOptions, originalMessage); Message message = requestMessageBuilder.build(); assertNotEquals(originalMessage.getCorrelationId(), message.getCorrelationId()); assertThat(message.getTopics().getTo(), is(namespace)); assertThat(message.getTopics().getResponse(), notNullValue()); + assertNull(message.getTopics().getForward()); } @Test @@ -176,6 +215,7 @@ public void testCreateResponseMessageBuilder() { assertThat(message.getTopics().getTo(), not(originalMessage.getTopics().getTo())); assertThat(message.getTopics().getTo(), is(originalMessage.getTopics().getResponse())); assertThat(message.getTopics().getResponse(), nullValue()); + assertNull(message.getTopics().getForward()); } @Test @@ -189,6 +229,7 @@ public void testCreateResponseMessageBuilderWithTags() { Message message = requestMessageBuilder.build(); assertArrayEquals(tags, message.getTags().toArray()); + assertNull(message.getTopics().getForward()); } @Test @@ -215,6 +256,7 @@ public void testBroadcastMessageBuilder() { assertNotNull(message.getCorrelationId()); assertEquals(topic, message.getTopics().getTo()); assertThat(message.getTopics().getResponse(), nullValue()); + assertNull(message.getTopics().getForward()); } @Test diff --git a/core/src/test/java/io/github/tcdl/msb/mock/adapterfactory/TestMsbAdapterFactory.java b/core/src/test/java/io/github/tcdl/msb/mock/adapterfactory/TestMsbAdapterFactory.java new file mode 100644 index 00000000..5a355111 --- /dev/null +++ b/core/src/test/java/io/github/tcdl/msb/mock/adapterfactory/TestMsbAdapterFactory.java @@ -0,0 +1,61 @@ +package io.github.tcdl.msb.mock.adapterfactory; + +import io.github.tcdl.msb.adapters.AdapterFactory; +import io.github.tcdl.msb.adapters.ConsumerAdapter; +import io.github.tcdl.msb.adapters.ProducerAdapter; +import io.github.tcdl.msb.api.RequestOptions; +import io.github.tcdl.msb.api.ResponderOptions; +import io.github.tcdl.msb.config.MsbConfig; + +/** + * This AdapterFactory implementation is used to capture/submit raw messages as JSON and could be used during testing. + */ +public class TestMsbAdapterFactory implements AdapterFactory { + + private TestMsbStorageForAdapterFactory storage = new TestMsbStorageForAdapterFactory(); + + public TestMsbStorageForAdapterFactory getStorage() { + return storage; + } + + public void setStorage(TestMsbStorageForAdapterFactory storage) { + this.storage = storage; + } + + @Override + public void init(MsbConfig msbConfig) { + + } + + @Override + public ProducerAdapter createProducerAdapter(String topic, boolean isResponseTopic, RequestOptions requestOptions) { + TestMsbProducerAdapter producerAdapter = new TestMsbProducerAdapter(topic, storage); + storage.addProducerAdapter(topic, producerAdapter); + return producerAdapter; + } + + @Override + public ConsumerAdapter createConsumerAdapter(String namespace, boolean isResponseTopic) { + TestMsbConsumerAdapter consumerAdapter = new TestMsbConsumerAdapter(namespace, storage); + storage.addConsumerAdapter(namespace, consumerAdapter); + return consumerAdapter; + } + + @Override + public ConsumerAdapter createConsumerAdapter(String topic, boolean isResponseTopic, ResponderOptions responderOptions) { + ResponderOptions effectiveResponderOptions = responderOptions != null ? responderOptions: ResponderOptions.DEFAULTS; + TestMsbConsumerAdapter consumerAdapter = new TestMsbConsumerAdapter(topic, storage); + storage.addConsumerAdapter(topic, effectiveResponderOptions.getBindingKeys(), consumerAdapter); + return consumerAdapter; + } + + @Override + public boolean isUseMsbThreadingModel() { + return false; + } + + @Override + public void shutdown() { + + } +} diff --git a/core/src/test/java/io/github/tcdl/msb/mock/adapterfactory/TestMsbConsumerAdapter.java b/core/src/test/java/io/github/tcdl/msb/mock/adapterfactory/TestMsbConsumerAdapter.java new file mode 100644 index 00000000..3ed36712 --- /dev/null +++ b/core/src/test/java/io/github/tcdl/msb/mock/adapterfactory/TestMsbConsumerAdapter.java @@ -0,0 +1,48 @@ +package io.github.tcdl.msb.mock.adapterfactory; + +import io.github.tcdl.msb.acknowledge.AcknowledgementHandlerInternal; +import io.github.tcdl.msb.adapters.ConsumerAdapter; + +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +import static org.mockito.Mockito.mock; + +public class TestMsbConsumerAdapter implements ConsumerAdapter { + + private final Set rawMessageHandlers = new HashSet<>(); + private final TestMsbStorageForAdapterFactory storage; + + private final String namespace; + + public TestMsbConsumerAdapter(String namespace, TestMsbStorageForAdapterFactory storage) { + this.namespace = namespace; + this.storage = storage; + } + + @Override + public void subscribe(RawMessageHandler onMessageHandler) { + rawMessageHandlers.add(onMessageHandler); + } + + @Override + public void unsubscribe() { + + } + + @Override + public Optional messageCount() { + throw new UnsupportedOperationException("This method is not implemented in this test class."); + } + + @Override + public Optional isConnected() { + throw new UnsupportedOperationException("This method is not implemented in this test class."); + } + + public void pushTestMessage(String jsonMessage) { + AcknowledgementHandlerInternal ackHandler = mock(AcknowledgementHandlerInternal.class); + rawMessageHandlers.forEach((handler)-> handler.onMessage(jsonMessage, ackHandler)); + } +} diff --git a/core/src/test/java/io/github/tcdl/msb/mock/adapterfactory/TestMsbProducerAdapter.java b/core/src/test/java/io/github/tcdl/msb/mock/adapterfactory/TestMsbProducerAdapter.java new file mode 100644 index 00000000..fa39e1a8 --- /dev/null +++ b/core/src/test/java/io/github/tcdl/msb/mock/adapterfactory/TestMsbProducerAdapter.java @@ -0,0 +1,30 @@ +package io.github.tcdl.msb.mock.adapterfactory; + +import io.github.tcdl.msb.adapters.ProducerAdapter; +import org.apache.commons.lang3.StringUtils; + +import java.util.Set; + +public class TestMsbProducerAdapter implements ProducerAdapter { + + private final String namespace; + + private final TestMsbStorageForAdapterFactory storage; + + public TestMsbProducerAdapter(String namespace, TestMsbStorageForAdapterFactory storage) { + this.namespace = namespace; + this.storage = storage; + } + + @Override + public void publish(String jsonMessage) { + storage.addPublishedTestMessage(namespace, StringUtils.EMPTY, jsonMessage); + storage.publishIncomingMessage(namespace, StringUtils.EMPTY, jsonMessage); + } + + @Override + public void publish(String jsonMessage, String routingKey) { + storage.addPublishedTestMessage(namespace, routingKey, jsonMessage); + storage.publishIncomingMessage(namespace, routingKey, jsonMessage); + } +} diff --git a/core/src/test/java/io/github/tcdl/msb/mock/adapterfactory/TestMsbStorageForAdapterFactory.java b/core/src/test/java/io/github/tcdl/msb/mock/adapterfactory/TestMsbStorageForAdapterFactory.java new file mode 100644 index 00000000..484c07ad --- /dev/null +++ b/core/src/test/java/io/github/tcdl/msb/mock/adapterfactory/TestMsbStorageForAdapterFactory.java @@ -0,0 +1,136 @@ +package io.github.tcdl.msb.mock.adapterfactory; + +import io.github.tcdl.msb.ChannelManager; +import io.github.tcdl.msb.api.MsbContext; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.reflect.FieldUtils; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * This class provides storage and accessors for {@link TestMsbAdapterFactory}-based + * testing. + */ +public class TestMsbStorageForAdapterFactory { + + private final HashMap, TestMsbConsumerAdapter>> multicastConsumers = new HashMap<>(); + private final HashMap broadcastConsumers = new HashMap<>(); + private final HashMap producers = new HashMap<>(); + private final HashMap>> publishedMessages = new HashMap<>(); + + /** + * Get TestMsbStorageForAdapterFactory instance used by a MsbContext. + * + * @param msbContext + * @return + */ + public static TestMsbStorageForAdapterFactory extract(MsbContext msbContext) { + try { + ChannelManager channelManager = (ChannelManager) FieldUtils.readField(msbContext, "channelManager", true); + TestMsbAdapterFactory adapterFactory = (TestMsbAdapterFactory) FieldUtils.readField(channelManager, "adapterFactory", true); + return adapterFactory.getStorage(); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + /** + * Force other context to use this TestMsbStorageForAdapterFactory instance so messaging will be shared + * between different contexts. Without this action, an MsbContext handles its own messages only. + * + * @param otherContext + */ + public void connect(MsbContext otherContext) { + try { + ChannelManager channelManager = (ChannelManager) FieldUtils.readField(otherContext, "channelManager", true); + TestMsbAdapterFactory adapterFactory = (TestMsbAdapterFactory) FieldUtils.readField(channelManager, "adapterFactory", true); + adapterFactory.setStorage(this); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + synchronized void addProducerAdapter(String namespace, TestMsbProducerAdapter adapter) { + producers.put(namespace, adapter); + } + + synchronized void addConsumerAdapter(String namespace, Set routingKeys, TestMsbConsumerAdapter adapter) { + multicastConsumers.computeIfAbsent(namespace, ns -> new HashMap<>()).put(routingKeys, adapter); + } + + synchronized void addConsumerAdapter(String namespace, TestMsbConsumerAdapter adapter) { + broadcastConsumers.put(namespace, adapter); + } + + synchronized void addPublishedTestMessage(String namespace, String routingKey, String jsonMessage) { + publishedMessages.computeIfAbsent(namespace, ns -> new HashMap<>()) + .computeIfAbsent(routingKey, rk -> new ArrayList<>()) + .add(jsonMessage); + } + + /** + * Reset the storage. + */ + public synchronized void cleanup() { + multicastConsumers.clear(); + broadcastConsumers.clear(); + producers.clear(); + publishedMessages.clear(); + } + + /** + * Publish a raw JSON message that should be handled as an incoming message. + */ + public synchronized void publishIncomingMessage(String namespace, String routingKey, String jsonMessage) { + if (broadcastConsumers.get(namespace) != null) { + broadcastConsumers.get(namespace).pushTestMessage(jsonMessage); + } else { + multicastConsumers.getOrDefault(namespace, Collections.emptyMap()).entrySet().stream() + .filter(entry -> entry.getKey().contains(routingKey)) + .forEach(entry -> entry.getValue().pushTestMessage(jsonMessage)); + } + } + + /** + * Get a list of outgoing raw JSON messages. + */ + public synchronized List getOutgoingMessages(String namespace) { + return publishedMessages.getOrDefault(namespace, Collections.emptyMap()) + .values() + .stream() + .flatMap(List::stream) + .collect(Collectors.toList()); + } + + /** + * Get a list of outgoing raw JSON messages. + */ + public synchronized List getOutgoingMessages(String namespace, Set routingKeys) { + Validate.notNull(routingKeys, "routingKeys should not be null"); + return publishedMessages.getOrDefault(namespace, Collections.emptyMap()) + .entrySet() + .stream() + .filter(entry -> routingKeys.contains(entry.getKey())) + .flatMap(entry -> entry.getValue().stream()) + .collect(Collectors.toList()); + } + + /** + * Get a single outgoing raw JSON message. + */ + public synchronized String getOutgoingMessage(String namespace) { + return this.getOutgoingMessage(namespace, StringUtils.EMPTY); + } + + public synchronized String getOutgoingMessage(String namespace, String routingKey) { + List messages = publishedMessages.getOrDefault(namespace, Collections.emptyMap()) + .getOrDefault(routingKey, Collections.emptyList()); + + if (messages == null || messages.size() != 1) { + throw new RuntimeException("A single outgoing message is expected"); + } + return messages.get(0); + } +} diff --git a/core/src/test/java/io/github/tcdl/msb/mock/objectfactory/AbstractCapture.java b/core/src/test/java/io/github/tcdl/msb/mock/objectfactory/AbstractCapture.java new file mode 100644 index 00000000..a099349d --- /dev/null +++ b/core/src/test/java/io/github/tcdl/msb/mock/objectfactory/AbstractCapture.java @@ -0,0 +1,34 @@ +package io.github.tcdl.msb.mock.objectfactory; + +import com.fasterxml.jackson.core.type.TypeReference; + +/** + * Abstract capture class for requesters and responders. + * @param + */ +public abstract class AbstractCapture { + private final String namespace; + private final TypeReference payloadTypeReference; + private final Class payloadClass; + + public AbstractCapture(String namespace, + TypeReference payloadTypeReference, Class payloadClass) { + this.namespace = namespace; + this.payloadTypeReference = payloadTypeReference; + this.payloadClass = payloadClass; + } + + public String getNamespace() { + return namespace; + } + + public TypeReference getPayloadTypeReference() { + return payloadTypeReference; + } + + public Class getPayloadClass() { + return payloadClass; + } + + +} \ No newline at end of file diff --git a/core/src/test/java/io/github/tcdl/msb/mock/objectfactory/RequesterCapture.java b/core/src/test/java/io/github/tcdl/msb/mock/objectfactory/RequesterCapture.java new file mode 100644 index 00000000..3f5aa763 --- /dev/null +++ b/core/src/test/java/io/github/tcdl/msb/mock/objectfactory/RequesterCapture.java @@ -0,0 +1,73 @@ +package io.github.tcdl.msb.mock.objectfactory; + +import com.fasterxml.jackson.core.type.TypeReference; +import io.github.tcdl.msb.api.Callback; +import io.github.tcdl.msb.api.MessageContext; +import io.github.tcdl.msb.api.RequestOptions; +import io.github.tcdl.msb.api.Requester; +import io.github.tcdl.msb.api.message.Acknowledge; +import io.github.tcdl.msb.api.message.Message; +import org.mockito.ArgumentCaptor; + +import java.util.function.BiConsumer; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Captured params of a requester. Handlers are captured by {@link ArgumentCaptor}. + * @param + */ +public class RequesterCapture extends AbstractCapture { + private final RequestOptions requestOptions; + private final Requester requesterMock; + + private final ArgumentCaptor> onAcknowledgeCaptor + = ArgumentCaptor.forClass((Class>)(Class)BiConsumer.class); + + private final ArgumentCaptor> onResponseCaptor + = ArgumentCaptor.forClass((Class>)(Class)BiConsumer.class); + + private final ArgumentCaptor> onRawResponseCaptor + = ArgumentCaptor.forClass((Class>)(Class)BiConsumer.class); + + private final ArgumentCaptor> onEndCaptor + = ArgumentCaptor.forClass((Class>)(Class)Callback.class); + + public RequesterCapture(String namespace, RequestOptions requestOptions, + TypeReference payloadTypeReference, Class payloadClass) { + super(namespace, payloadTypeReference, payloadClass); + this.requestOptions = requestOptions; + this.requesterMock = mock(Requester.class); + + when(this.requesterMock.onAcknowledge(onAcknowledgeCaptor.capture())).thenReturn(this.requesterMock); + when(this.requesterMock.onResponse(onResponseCaptor.capture())).thenReturn(this.requesterMock); + when(this.requesterMock.onRawResponse(onRawResponseCaptor.capture())).thenReturn(this.requesterMock); + when(this.requesterMock.onEnd(onEndCaptor.capture())).thenReturn(this.requesterMock); + + } + + public RequestOptions getRequestOptions() { + return requestOptions; + } + + public Requester getRequesterMock() { + return requesterMock; + } + + public ArgumentCaptor> getOnAcknowledgeCaptor() { + return onAcknowledgeCaptor; + } + + public ArgumentCaptor> getOnResponseCaptor() { + return onResponseCaptor; + } + + public ArgumentCaptor> getOnRawResponseCaptor() { + return onRawResponseCaptor; + } + + public ArgumentCaptor> getOnEndCaptor() { + return onEndCaptor; + } +} \ No newline at end of file diff --git a/core/src/test/java/io/github/tcdl/msb/mock/objectfactory/ResponderCapture.java b/core/src/test/java/io/github/tcdl/msb/mock/objectfactory/ResponderCapture.java new file mode 100644 index 00000000..8662fbb0 --- /dev/null +++ b/core/src/test/java/io/github/tcdl/msb/mock/objectfactory/ResponderCapture.java @@ -0,0 +1,63 @@ +package io.github.tcdl.msb.mock.objectfactory; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.google.common.collect.Sets; +import io.github.tcdl.msb.api.MessageTemplate; +import io.github.tcdl.msb.api.ResponderServer; +import org.apache.commons.lang3.StringUtils; + +import java.util.Collections; +import java.util.Set; + +import static org.mockito.Mockito.mock; + +/** + * Captured params of a responder, including a requestHandler. + * @param + */ +public class ResponderCapture extends AbstractCapture { + private final MessageTemplate messageTemplate; + private final Set routingKeys; + private final ResponderServer.RequestHandler requestHandler; + private final ResponderServer.ErrorHandler errorHandler; + private final ResponderServer responderServerMock; + + public ResponderCapture(String namespace, MessageTemplate messageTemplate, + ResponderServer.RequestHandler requestHandler, + ResponderServer.ErrorHandler errorHandler, + TypeReference payloadTypeReference, Class payloadClass) { + this(namespace, Collections.emptySet(), messageTemplate, requestHandler, errorHandler, payloadTypeReference, payloadClass); + } + + public ResponderCapture(String namespace, Set routingKeys, MessageTemplate messageTemplate, + ResponderServer.RequestHandler requestHandler, + ResponderServer.ErrorHandler errorHandler, + TypeReference payloadTypeReference, Class payloadClass) { + super(namespace, payloadTypeReference, payloadClass); + this.routingKeys = routingKeys; + this.messageTemplate = messageTemplate; + this.requestHandler = requestHandler; + this.errorHandler = errorHandler; + this.responderServerMock = mock(ResponderServer.class); + } + + public MessageTemplate getMessageTemplate() { + return messageTemplate; + } + + public ResponderServer.RequestHandler getRequestHandler() { + return requestHandler; + } + + public ResponderServer.ErrorHandler getErrorHandler() { + return errorHandler; + } + + public ResponderServer getResponderServerMock() { + return responderServerMock; + } + + public Set getRoutingKeys() { + return Collections.unmodifiableSet(routingKeys); + } +} \ No newline at end of file diff --git a/core/src/test/java/io/github/tcdl/msb/mock/objectfactory/TestMsbObjectFactory.java b/core/src/test/java/io/github/tcdl/msb/mock/objectfactory/TestMsbObjectFactory.java new file mode 100644 index 00000000..c93f69f0 --- /dev/null +++ b/core/src/test/java/io/github/tcdl/msb/mock/objectfactory/TestMsbObjectFactory.java @@ -0,0 +1,84 @@ +package io.github.tcdl.msb.mock.objectfactory; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import io.github.tcdl.msb.api.*; +import org.apache.commons.lang3.Validate; + +import java.lang.reflect.Type; + +/** + * {@link ObjectFactory} implementation that captures all requesters/responders params and callbacks to be + * used during testing. + */ +public class TestMsbObjectFactory implements ObjectFactory { + + private final TestMsbStorageForObjectFactory storage = new TestMsbStorageForObjectFactory(); + + public TestMsbStorageForObjectFactory getStorage() { + return storage; + } + + @Override + public Requester createRequester(String namespace, RequestOptions requestOptions, TypeReference payloadTypeReference) { + RequesterCapture capture = new RequesterCapture<>(namespace, requestOptions, payloadTypeReference, null); + storage.addCapture(capture); + return capture.getRequesterMock(); + } + + @Override + public Requester createRequesterForSingleResponse(String namespace, Class payloadClass, RequestOptions baseRequestOptions) { + Validate.notNull(baseRequestOptions); + RequestOptions requestOptions = baseRequestOptions + .asBuilder() + .withWaitForResponses(1) + .withAckTimeout(0) + .build(); + + return createRequester(namespace, requestOptions, toTypeReference(payloadClass)); + } + + @Override + public Requester createRequester(String namespace, RequestOptions requestOptions) { + RequesterCapture capture = new RequesterCapture<>(namespace, requestOptions, null, null); + storage.addCapture(capture); + return capture.getRequesterMock(); + } + + @Override + public Requester createRequester(String namespace, RequestOptions requestOptions, Class payloadClass) { + RequesterCapture capture = new RequesterCapture<>(namespace, requestOptions, null, payloadClass); + storage.addCapture(capture); + return capture.getRequesterMock(); + } + + @Override + public ResponderServer createResponderServer(String namespace, ResponderOptions responderOptions, ResponderServer.RequestHandler requestHandler, ResponderServer.ErrorHandler errorHandler, TypeReference payloadTypeReference) { + ResponderCapture capture = new ResponderCapture<>(namespace, responderOptions.getBindingKeys(), responderOptions.getMessageTemplate(), requestHandler, null, payloadTypeReference, null); + storage.addCapture(capture); + return capture.getResponderServerMock(); + } + + @Override + public Requester createRequesterForFireAndForget(String namespace, RequestOptions requestOptions) { + Validate.notNull(requestOptions); + RequestOptions fireAndForgetRequestOptions = requestOptions.asBuilder().withWaitForResponses(0).build(); + + return createRequester(namespace, fireAndForgetRequestOptions, (Class) null); + } + + @Override + public PayloadConverter getPayloadConverter() { + return null; + } + + private static TypeReference toTypeReference(Class clazz) { + return new TypeReference() { + @Override + public Type getType() { + return clazz; + } + }; + } + +} diff --git a/core/src/test/java/io/github/tcdl/msb/mock/objectfactory/TestMsbStorageForObjectFactory.java b/core/src/test/java/io/github/tcdl/msb/mock/objectfactory/TestMsbStorageForObjectFactory.java new file mode 100644 index 00000000..ba62309f --- /dev/null +++ b/core/src/test/java/io/github/tcdl/msb/mock/objectfactory/TestMsbStorageForObjectFactory.java @@ -0,0 +1,58 @@ +package io.github.tcdl.msb.mock.objectfactory; +import io.github.tcdl.msb.api.MsbContext; +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * This class provides storage and accessors for {@link TestMsbObjectFactory}-based + * testing. + */ +public class TestMsbStorageForObjectFactory { + private final Map requesters = new HashMap<>(); + private final Map responders = new HashMap<>(); + + public static TestMsbStorageForObjectFactory extract(MsbContext msbContext) { + return ((TestMsbObjectFactory)msbContext.getObjectFactory()).getStorage(); + } + + synchronized void addCapture(RequesterCapture requesterCapture) { + requesters.put(requesterCapture.getNamespace(), requesterCapture); + } + + synchronized void addCapture(ResponderCapture responderCapture) { + responders.put(responderCapture.getNamespace(), responderCapture); + } + + + /** + * Reset the storage. + */ + public synchronized void cleanup() { + requesters.clear(); + responders.clear(); + } + + /** + * Get captured requester params (including handlers). + * @param namespace + * @param + * @return + */ + public synchronized RequesterCapture getRequesterCapture(String namespace) { + return (RequesterCapture) requesters.get(namespace); + } + + /** + * Get captured responder params (including handlers). + * @param namespace + * @param + * @return + */ + public synchronized ResponderCapture getResponderCapture(String namespace) { + return (ResponderCapture) responders.get(namespace); + } +} diff --git a/core/src/test/java/io/github/tcdl/msb/monitor/agent/DefaultChannelMonitorAgentTest.java b/core/src/test/java/io/github/tcdl/msb/monitor/agent/DefaultChannelMonitorAgentTest.java deleted file mode 100644 index 984be173..00000000 --- a/core/src/test/java/io/github/tcdl/msb/monitor/agent/DefaultChannelMonitorAgentTest.java +++ /dev/null @@ -1,157 +0,0 @@ -package io.github.tcdl.msb.monitor.agent; - -import io.github.tcdl.msb.ChannelManager; -import io.github.tcdl.msb.MessageHandler; -import io.github.tcdl.msb.Producer; -import io.github.tcdl.msb.api.message.Message; -import io.github.tcdl.msb.collector.TimeoutManager; -import io.github.tcdl.msb.config.ServiceDetails; -import io.github.tcdl.msb.impl.MsbContextImpl; -import io.github.tcdl.msb.message.MessageFactory; -import io.github.tcdl.msb.support.TestUtils; -import org.junit.Before; -import org.junit.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.Mockito; - -import java.time.Clock; -import java.time.Instant; -import java.time.ZoneId; - -import static io.github.tcdl.msb.support.Utils.TOPIC_ANNOUNCE; -import static io.github.tcdl.msb.support.Utils.TOPIC_HEARTBEAT; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -public class DefaultChannelMonitorAgentTest { - private static final Instant CLOCK_INSTANT = Instant.parse("2007-12-03T10:15:30.00Z"); - - private DefaultChannelMonitorAgent channelMonitorAgent; - private ChannelManager mockChannelManager; - - @Before - public void setUp() { - mockChannelManager = mock(ChannelManager.class); - Clock clock = Clock.fixed(CLOCK_INSTANT, ZoneId.systemDefault()); - ServiceDetails serviceDetails = new ServiceDetails.Builder().build(); - MessageFactory messageFactory = new MessageFactory(serviceDetails, clock, TestUtils.createMessageMapper()); - TimeoutManager mockTimeoutManager = mock(TimeoutManager.class); - MsbContextImpl msbContext = TestUtils.createMsbContextBuilder() - .withMessageFactory(messageFactory) - .withChannelManager(mockChannelManager) - .withClock(clock) - .withTimeoutManager(mockTimeoutManager) - .build(); - channelMonitorAgent = new DefaultChannelMonitorAgent(msbContext); - } - - @Test - public void testAnnounceProducerForServiceTopic() { - channelMonitorAgent.producerTopicCreated(TOPIC_ANNOUNCE); - - verify(mockChannelManager, never()).findOrCreateProducer(anyString()); - } - - @Test - public void testAnnounceProducerForNormalTopic() { - String topicName = "search:parsers:facets:v1"; - Producer mockProducer = mock(Producer.class); - when(mockChannelManager.findOrCreateProducer(TOPIC_ANNOUNCE)).thenReturn(mockProducer); - - // method under test - channelMonitorAgent.producerTopicCreated(topicName); - - // verify internal data structures - assertTrue(channelMonitorAgent.topicInfoMap.containsKey(topicName)); - assertTrue(channelMonitorAgent.topicInfoMap.get(topicName).isProducers()); - - Message message = verifyProducerInvokedAndReturnMessage(mockProducer); - verifyMessageContainsTopic(message, topicName); - } - - @Test - public void testAnnounceConsumerForServiceTopic() { - channelMonitorAgent.consumerTopicCreated(TOPIC_ANNOUNCE); - verify(mockChannelManager, never()).subscribe(anyString(), Mockito.any(MessageHandler.class)); - } - - @Test - public void testAnnounceConsumerForNormalTopic() { - String topicName = "search:parsers:facets:v1"; - Producer mockProducer = mock(Producer.class); - when(mockChannelManager.findOrCreateProducer(TOPIC_ANNOUNCE)).thenReturn(mockProducer); - - // method under test - channelMonitorAgent.consumerTopicCreated(topicName); - - // verify internal data structures - assertTrue(channelMonitorAgent.topicInfoMap.containsKey(topicName)); - assertTrue(channelMonitorAgent.topicInfoMap.get(topicName).isConsumers()); - - Message message = verifyProducerInvokedAndReturnMessage(mockProducer); - verifyMessageContainsTopic(message, topicName); - } - - @Test - public void testRemoveConsumerForNormalTopic() { - String topicName = "search:parsers:facets:v1"; - Producer mockProducer = mock(Producer.class); - when(mockChannelManager.findOrCreateProducer(TOPIC_ANNOUNCE)).thenReturn(mockProducer); - channelMonitorAgent.consumerTopicCreated(topicName); // subscribe to the topic as a preparation step - - // method under test - channelMonitorAgent.consumerTopicRemoved(topicName); - - assertTrue(channelMonitorAgent.topicInfoMap.containsKey(topicName)); - assertFalse(channelMonitorAgent.topicInfoMap.get(topicName).isConsumers()); - } - - @Test - public void testMessageProduce() { - String topicName = "search:parsers:facets:v1"; - - channelMonitorAgent.producerMessageSent(topicName); - - assertTrue(channelMonitorAgent.topicInfoMap.containsKey(topicName)); - assertEquals(CLOCK_INSTANT, channelMonitorAgent.topicInfoMap.get(topicName).getLastProducedAt()); - } - - @Test - public void testMessageConsumed() { - String topicName = "search:parsers:facets:v1"; - - channelMonitorAgent.consumerMessageReceived(topicName); - - assertTrue(channelMonitorAgent.topicInfoMap.containsKey(topicName)); - assertEquals(CLOCK_INSTANT, channelMonitorAgent.topicInfoMap.get(topicName).getLastConsumedAt()); - } - - @Test - public void testStart() { - ChannelMonitorAgent startedAgent = channelMonitorAgent.start(); - - assertSame(channelMonitorAgent, startedAgent); - verify(mockChannelManager).subscribe(Mockito.eq(TOPIC_HEARTBEAT), Mockito.any(MessageHandler.class)); - } - - private Message verifyProducerInvokedAndReturnMessage(Producer mockProducer) { - verify(mockChannelManager).findOrCreateProducer(TOPIC_ANNOUNCE); - ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(Message.class); - verify(mockProducer).publish(messageCaptor.capture()); - return messageCaptor.getValue(); - } - - private void verifyMessageContainsTopic(Message message, String topicName) { - assertNotNull(message.getRawPayload()); - assertNotNull(message.getRawPayload().get("body")); - assertTrue(message.getRawPayload().get("body").has(topicName)); - } -} \ No newline at end of file diff --git a/core/src/test/java/io/github/tcdl/msb/monitor/aggregator/DefaultChannelMonitorAggregatorTest.java b/core/src/test/java/io/github/tcdl/msb/monitor/aggregator/DefaultChannelMonitorAggregatorTest.java deleted file mode 100644 index fe95239d..00000000 --- a/core/src/test/java/io/github/tcdl/msb/monitor/aggregator/DefaultChannelMonitorAggregatorTest.java +++ /dev/null @@ -1,303 +0,0 @@ -package io.github.tcdl.msb.monitor.aggregator; - -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; -import io.github.tcdl.msb.ChannelManager; -import io.github.tcdl.msb.MessageHandler; -import io.github.tcdl.msb.api.Callback; -import io.github.tcdl.msb.api.ObjectFactory; -import io.github.tcdl.msb.api.message.Message; -import io.github.tcdl.msb.api.message.payload.RestPayload; -import io.github.tcdl.msb.api.monitor.AggregatorStats; -import io.github.tcdl.msb.api.monitor.AggregatorTopicStats; -import io.github.tcdl.msb.config.ServiceDetails; -import io.github.tcdl.msb.impl.MsbContextImpl; -import io.github.tcdl.msb.monitor.agent.AgentTopicStats; -import io.github.tcdl.msb.support.TestUtils; -import io.github.tcdl.msb.support.Utils; -import org.junit.Test; - -import java.time.Instant; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - -import static io.github.tcdl.msb.api.monitor.ChannelMonitorAggregator.DEFAULT_HEARTBEAT_INTERVAL_MS; -import static io.github.tcdl.msb.api.monitor.ChannelMonitorAggregator.DEFAULT_HEARTBEAT_TIMEOUT_MS; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; - -public class DefaultChannelMonitorAggregatorTest { - - private ChannelManager mockChannelManager = mock(ChannelManager.class); - private ObjectFactory mockObjectFactory = mock(ObjectFactory.class); - - private MsbContextImpl testMsbContext = TestUtils.createMsbContextBuilder() - .withObjectFactory(mockObjectFactory) - .withChannelManager(mockChannelManager) - .build(); - - private ScheduledExecutorService mockScheduledExecutorService = mock(ScheduledExecutorService.class); - @SuppressWarnings("unchecked") - private Callback mockHandler = mock(Callback.class); - private DefaultChannelMonitorAggregator channelMonitor = new DefaultChannelMonitorAggregator(testMsbContext, mockScheduledExecutorService, mockHandler); - - @Test - public void testOnAnnounce() { - String TOPIC1 = "topic1"; - String TOPIC2 = "topic2"; - String INSTANCE_ID = "instanceId"; - Message announcementMessage = createAnnouncementMessageWith2Topics(INSTANCE_ID, TOPIC1, TOPIC2); - - channelMonitor.onAnnounce(announcementMessage); - - assertEquals(1, channelMonitor.masterAggregatorStats.getServiceDetailsById().size()); - assertTrue(channelMonitor.masterAggregatorStats.getServiceDetailsById().containsKey(INSTANCE_ID)); - assertEquals(2, channelMonitor.masterAggregatorStats.getTopicInfoMap().size()); - - verify(mockHandler).call(channelMonitor.masterAggregatorStats); - } - - @Test - public void testOnHeartbeatResponses() { - String INSTANCE_ID_OBSOLETE = "instance_id_obsolete"; - String TOPIC_OBSOLETE = "topic_obsolete"; - - // Prepopulate stats to verify that obsolete data are cleared upon heartbeats - channelMonitor.masterAggregatorStats.getTopicInfoMap().put(TOPIC_OBSOLETE, new AggregatorTopicStats()); - channelMonitor.masterAggregatorStats.getServiceDetailsById().put(INSTANCE_ID_OBSOLETE, new ServiceDetails.Builder().build()); - - String INSTANCE_ID_1 = "instanceId1"; - String INSTANCE_ID_2 = "instanceId2"; - String TOPIC1 = "topic1"; - String TOPIC2 = "topic2"; - - Message hbMessage1 = createAnnouncementMessageWith2Topics(INSTANCE_ID_1, TOPIC1, TOPIC2); - Message hbMessage2 = createAnnouncementMessageWith2Topics(INSTANCE_ID_2, TOPIC1, TOPIC2); - - channelMonitor.onHeartbeatResponses(Arrays.asList(hbMessage1, hbMessage2)); - - assertEquals(2, channelMonitor.masterAggregatorStats.getServiceDetailsById().size()); - assertTrue(channelMonitor.masterAggregatorStats.getServiceDetailsById().containsKey(INSTANCE_ID_1)); - assertTrue(channelMonitor.masterAggregatorStats.getServiceDetailsById().containsKey(INSTANCE_ID_2)); - - assertEquals(2, channelMonitor.masterAggregatorStats.getTopicInfoMap().size()); - assertTrue(channelMonitor.masterAggregatorStats.getTopicInfoMap().containsKey(TOPIC1)); - assertTrue(channelMonitor.masterAggregatorStats.getTopicInfoMap().containsKey(TOPIC2)); - - verify(mockHandler, times(1)).call(channelMonitor.masterAggregatorStats); - } - - @Test - public void testAggregateInfo() { - String INSTANCE_ID = "instanceId"; - String TOPIC1 = "topic1"; - String TOPIC2 = "topic2"; - - Message announcementMessage = createAnnouncementMessageWith2Topics(INSTANCE_ID, TOPIC1, TOPIC2); - - AggregatorStats aggregatorStats = new AggregatorStats(); - channelMonitor.aggregateInfo(aggregatorStats, announcementMessage); - - assertEquals(1, aggregatorStats.getServiceDetailsById().size()); - assertTrue(aggregatorStats.getServiceDetailsById().containsKey(INSTANCE_ID)); - assertEquals(2, aggregatorStats.getTopicInfoMap().size()); - } - - @Test - public void testAggregateTopicStatsInitial() { - // Given - String INSTANCE_ID = "instanceId"; - String TOPIC_1 = "topic1"; - String TOPIC_2 = "topic2"; - Instant LAST_PRODUCED_AT = Instant.parse("2007-12-03T10:15:30.00Z"); - Instant LAST_CONSUMED_AT = Instant.parse("2007-12-03T10:15:32.00Z"); - - Map agentTopicStats = ImmutableMap.of( - TOPIC_1, - new AgentTopicStats() - .withProducers(true) - .withLastProducedAt(LAST_PRODUCED_AT), - - TOPIC_2, - new AgentTopicStats() - .withConsumers(true) - .withLastConsumedAt(LAST_CONSUMED_AT) - ); - - // When - AggregatorStats aggregatorStats = new AggregatorStats(); - channelMonitor.aggregateTopicStats(aggregatorStats, agentTopicStats, INSTANCE_ID); - - // Then - Map topicInfoMap = aggregatorStats.getTopicInfoMap(); - Map expectedTopicInfoMap = ImmutableMap.of( - TOPIC_1, - new AggregatorTopicStats() - .withProducers(ImmutableSet.of(INSTANCE_ID)) - .withConsumers(Collections.emptySet()) - .withLastProducedAt(LAST_PRODUCED_AT) - .withLastConsumedAt(null), - - TOPIC_2, - new AggregatorTopicStats() - .withProducers(Collections.emptySet()) - .withConsumers(ImmutableSet.of(INSTANCE_ID)) - .withLastProducedAt(null) - .withLastConsumedAt(LAST_CONSUMED_AT) - ); - - assertEquals(expectedTopicInfoMap, topicInfoMap); - } - - @Test - public void testAggregateTopicStatsServiceProducerInstances() { - String INSTANCE_ID_1 = "instanceId"; - String INSTANCE_ID_2 = "instanceId2"; - String TOPIC = "topic1"; - - AggregatorStats aggregatorStats = new AggregatorStats(); - // Process message from one instance - assertProducers(aggregatorStats, ImmutableSet.of(INSTANCE_ID_1), INSTANCE_ID_1, TOPIC); - // Process message from another instance - assertProducers(aggregatorStats, ImmutableSet.of(INSTANCE_ID_1, INSTANCE_ID_2), INSTANCE_ID_2, TOPIC); - // Process message from the first instance again - assertProducers(aggregatorStats, ImmutableSet.of(INSTANCE_ID_1, INSTANCE_ID_2), INSTANCE_ID_1, TOPIC); - - // Verify that consumers left untouched - assertTrue(aggregatorStats.getTopicInfoMap().get(TOPIC).getConsumers().isEmpty()); - } - - @Test - public void testAggregateTopicStatsServiceConsumerInstances() { - String INSTANCE_ID_1 = "instanceId"; - String INSTANCE_ID_2 = "instanceId2"; - String TOPIC = "topic1"; - - AggregatorStats aggregatorStats = new AggregatorStats(); - // Process message from one instance - assertConsumers(aggregatorStats, ImmutableSet.of(INSTANCE_ID_1), INSTANCE_ID_1, TOPIC); - // Process message from another instance - assertConsumers(aggregatorStats, ImmutableSet.of(INSTANCE_ID_1, INSTANCE_ID_2), INSTANCE_ID_2, TOPIC); - // Process message from the first instance again - assertConsumers(aggregatorStats, ImmutableSet.of(INSTANCE_ID_1, INSTANCE_ID_2), INSTANCE_ID_1, TOPIC); - - // Verify that consumers left untouched - assertTrue(aggregatorStats.getTopicInfoMap().get(TOPIC).getProducers().isEmpty()); - } - - @Test - public void testAggregateTopicStatsProducingTime() { - String INSTANCE_ID = "instanceId"; - String TOPIC = "topic1"; - Instant LAST_PRODUCED_AT_OLDER = Instant.parse("2007-12-03T10:15:30.00Z"); - Instant LAST_PRODUCED_AT_NEWER = Instant.parse("2007-12-03T10:15:31.00Z"); - Instant LAST_PRODUCED_AT_NEWEST = Instant.parse("2007-12-03T10:15:32.00Z"); - - AggregatorStats aggregatorStats = new AggregatorStats(); - assertLastProducedAt(aggregatorStats, LAST_PRODUCED_AT_NEWER, LAST_PRODUCED_AT_NEWER, INSTANCE_ID, TOPIC); - // Process message with newer date - assertLastProducedAt(aggregatorStats, LAST_PRODUCED_AT_NEWEST, LAST_PRODUCED_AT_NEWEST, INSTANCE_ID, TOPIC); - // Process message with older date - assertLastProducedAt(aggregatorStats, LAST_PRODUCED_AT_NEWEST, LAST_PRODUCED_AT_OLDER, INSTANCE_ID, TOPIC); - // Process message with null date - assertLastProducedAt(aggregatorStats, LAST_PRODUCED_AT_NEWEST, null, INSTANCE_ID, TOPIC); - } - - @Test - public void testAggregateTopicStatsConsumingTime() { - String INSTANCE_ID = "instanceId"; - String TOPIC = "topic1"; - Instant LAST_CONSUMED_AT_OLDER = Instant.parse("2007-12-03T10:15:30.00Z"); - Instant LAST_CONSUMED_AT_NEWER = Instant.parse("2007-12-03T10:15:31.00Z"); - Instant LAST_CONSUMED_AT_NEWEST = Instant.parse("2007-12-03T10:15:32.00Z"); - - AggregatorStats aggregatorStats = new AggregatorStats(); - assertLastConsumedAt(aggregatorStats, LAST_CONSUMED_AT_NEWER, LAST_CONSUMED_AT_NEWER, INSTANCE_ID, TOPIC); - // Process message with newer date - assertLastConsumedAt(aggregatorStats, LAST_CONSUMED_AT_NEWEST, LAST_CONSUMED_AT_NEWEST, INSTANCE_ID, TOPIC); - // Process message with older date - assertLastConsumedAt(aggregatorStats, LAST_CONSUMED_AT_NEWEST, LAST_CONSUMED_AT_OLDER, INSTANCE_ID, TOPIC); - // Process message with null date - assertLastConsumedAt(aggregatorStats, LAST_CONSUMED_AT_NEWEST, null, INSTANCE_ID, TOPIC); - } - - @Test - public void testStartWithHeartbeat() { - channelMonitor.start(true, DEFAULT_HEARTBEAT_INTERVAL_MS, DEFAULT_HEARTBEAT_TIMEOUT_MS); - - verify(mockChannelManager).subscribe(eq(Utils.TOPIC_ANNOUNCE), any(MessageHandler.class)); - verify(mockScheduledExecutorService).scheduleAtFixedRate(any(HeartbeatTask.class), eq(0L), eq(DEFAULT_HEARTBEAT_INTERVAL_MS), - eq(TimeUnit.MILLISECONDS)); - } - - @Test - public void testStartWithoutHeartbeat() { - channelMonitor.start(false, DEFAULT_HEARTBEAT_INTERVAL_MS, DEFAULT_HEARTBEAT_TIMEOUT_MS); - - verify(mockChannelManager).subscribe(eq(Utils.TOPIC_ANNOUNCE), any(MessageHandler.class)); - verifyNoMoreInteractions(mockScheduledExecutorService); - } - - @Test - public void testStop() { - channelMonitor.start(true, DEFAULT_HEARTBEAT_INTERVAL_MS, DEFAULT_HEARTBEAT_TIMEOUT_MS); - channelMonitor.stop(); - - verify(mockChannelManager).unsubscribe(Utils.TOPIC_ANNOUNCE); - verify(mockScheduledExecutorService).shutdown(); - } - - private void assertProducers(AggregatorStats aggregatorStats, Set expectedProducers, String producerId, String topic) { - Map agentTopicStats = ImmutableMap.of(topic, new AgentTopicStats().withProducers(true)); - channelMonitor.aggregateTopicStats(aggregatorStats, agentTopicStats, producerId); - Map topicInfoMap = aggregatorStats.getTopicInfoMap(); - assertEquals(expectedProducers, topicInfoMap.get(topic).getProducers()); - } - - private void assertConsumers(AggregatorStats aggregatorStats, Set expectedConsumers, String consumerId, String topic) { - Map agentTopicStats = ImmutableMap.of(topic, new AgentTopicStats().withConsumers(true)); - channelMonitor.aggregateTopicStats(aggregatorStats, agentTopicStats, consumerId); - Map topicInfoMap = aggregatorStats.getTopicInfoMap(); - assertEquals(expectedConsumers, topicInfoMap.get(topic).getConsumers()); - } - - private void assertLastProducedAt(AggregatorStats aggregatorStats, Instant expectedlastProducedAt, Instant lastProducedAt, String instanceId, String topic) { - Map agentTopicStats = ImmutableMap.of(topic, new AgentTopicStats() - .withProducers(true) - .withLastProducedAt(lastProducedAt)); - channelMonitor.aggregateTopicStats(aggregatorStats, agentTopicStats, instanceId); - assertEquals(expectedlastProducedAt, aggregatorStats.getTopicInfoMap().get(topic).getLastProducedAt()); - } - - private void assertLastConsumedAt(AggregatorStats aggregatorStats, Instant expectedLastConsumedAt, Instant lastConsumedAt, String instanceId, String topic) { - Map agentTopicStats = ImmutableMap.of(topic, new AgentTopicStats() - .withConsumers(true) - .withLastConsumedAt(lastConsumedAt)); - channelMonitor.aggregateTopicStats(aggregatorStats, agentTopicStats, instanceId); - assertEquals(expectedLastConsumedAt, aggregatorStats.getTopicInfoMap().get(topic).getLastConsumedAt()); - } - - private Message createAnnouncementMessageWith2Topics(String instanceId, String topic1, String topic2) { - Map topicInfoMap = new HashMap<>(); - topicInfoMap.put(topic1, new AgentTopicStats().withProducers(true).withLastProducedAt(Instant.now())); - topicInfoMap.put(topic2, new AgentTopicStats().withConsumers(true).withLastConsumedAt(Instant.now())); - - RestPayload> announcementPayload = new RestPayload.Builder>() - .withBody(topicInfoMap) - .build(); - - return TestUtils.createMsbRequestMessage("to", instanceId, announcementPayload); - } - -} \ No newline at end of file diff --git a/core/src/test/java/io/github/tcdl/msb/monitor/aggregator/HeartbeatTaskTest.java b/core/src/test/java/io/github/tcdl/msb/monitor/aggregator/HeartbeatTaskTest.java deleted file mode 100644 index 3fa2b818..00000000 --- a/core/src/test/java/io/github/tcdl/msb/monitor/aggregator/HeartbeatTaskTest.java +++ /dev/null @@ -1,83 +0,0 @@ -package io.github.tcdl.msb.monitor.aggregator; - -import com.fasterxml.jackson.databind.JsonNode; -import io.github.tcdl.msb.api.Callback; -import io.github.tcdl.msb.api.ObjectFactory; -import io.github.tcdl.msb.api.RequestOptions; -import io.github.tcdl.msb.api.Requester; -import io.github.tcdl.msb.api.message.Message; -import io.github.tcdl.msb.api.message.payload.RestPayload; -import io.github.tcdl.msb.api.monitor.ChannelMonitorAggregator; -import io.github.tcdl.msb.support.TestUtils; -import io.github.tcdl.msb.support.Utils; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.mockito.ArgumentCaptor; - -import java.util.List; - -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -public class HeartbeatTaskTest { - - private ObjectFactory mockObjectFactory = mock(ObjectFactory.class); - @SuppressWarnings("unchecked") - private Requester mockRequester = mock(Requester.class); - @SuppressWarnings("unchecked") - private Callback> mockMessageHandler = mock(Callback.class); - private HeartbeatTask heartbeatTask = new HeartbeatTask(ChannelMonitorAggregator.DEFAULT_HEARTBEAT_TIMEOUT_MS, mockObjectFactory, mockMessageHandler); - - @Before - public void setUp() { - when(mockObjectFactory.createRequester(anyString(), any(RequestOptions.class))).thenReturn(mockRequester); - @SuppressWarnings("unchecked") - Callback responseHandler = any(Callback.class); - when(mockRequester.onRawResponse(responseHandler)).thenReturn(mockRequester); - @SuppressWarnings("unchecked") - Callback endHandler = any(Callback.class); - when(mockRequester.onEnd(endHandler)).thenReturn(mockRequester); - } - - @SuppressWarnings("unchecked") - @Test - public void testRun() { - heartbeatTask.run(); - - ArgumentCaptor onResponseCaptor = ArgumentCaptor.forClass(Callback.class); - ArgumentCaptor onEndCaptor = ArgumentCaptor.forClass(Callback.class); - ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(List.class); - - verify(mockObjectFactory).createRequester(eq(Utils.TOPIC_HEARTBEAT), any(RequestOptions.class)); - verify(mockRequester).onRawResponse(onResponseCaptor.capture()); - verify(mockRequester).onEnd(onEndCaptor.capture()); - verify(mockRequester).publish(any(RestPayload.class)); - - // simulate incoming messages - Message msg1 = TestUtils.createSimpleRequestMessage("from:responder"); - Message msg2 = TestUtils.createSimpleRequestMessage("from:responder"); - onResponseCaptor.getValue().call(msg1); - onResponseCaptor.getValue().call(msg2); - onEndCaptor.getValue().call(null); - - verify(mockMessageHandler).call(messageCaptor.capture()); - - Assert.assertArrayEquals(new Message[] {msg1, msg2}, messageCaptor.getValue().toArray()); - } - - @Test - public void testRunWithException() { - try { - when(mockObjectFactory.createRequester(anyString(), any(RequestOptions.class))).thenThrow(new RuntimeException()); - heartbeatTask.run(); - } catch (Exception e) { - Assert.fail("Exception should not be thrown"); - } - } - -} \ No newline at end of file diff --git a/core/src/test/java/io/github/tcdl/msb/support/TestUtils.java b/core/src/test/java/io/github/tcdl/msb/support/TestUtils.java index 85d69a4b..15a88016 100644 --- a/core/src/test/java/io/github/tcdl/msb/support/TestUtils.java +++ b/core/src/test/java/io/github/tcdl/msb/support/TestUtils.java @@ -6,6 +6,8 @@ import com.typesafe.config.ConfigFactory; import com.typesafe.config.ConfigValueFactory; import io.github.tcdl.msb.ChannelManager; +import io.github.tcdl.msb.adapters.AdapterFactory; +import io.github.tcdl.msb.adapters.AdapterFactoryLoader; import io.github.tcdl.msb.api.MessageTemplate; import io.github.tcdl.msb.api.MsbContextBuilder; import io.github.tcdl.msb.api.ObjectFactory; @@ -16,12 +18,17 @@ import io.github.tcdl.msb.api.message.MetaMessage; import io.github.tcdl.msb.api.message.Topics; import io.github.tcdl.msb.api.message.payload.RestPayload; +import io.github.tcdl.msb.callback.MutableCallbackHandler; import io.github.tcdl.msb.collector.CollectorManagerFactory; import io.github.tcdl.msb.collector.TimeoutManager; import io.github.tcdl.msb.config.MsbConfig; import io.github.tcdl.msb.impl.MsbContextImpl; import io.github.tcdl.msb.impl.ObjectFactoryImpl; import io.github.tcdl.msb.message.MessageFactory; +import io.github.tcdl.msb.threading.ConsumerExecutorFactoryImpl; +import io.github.tcdl.msb.threading.DirectMessageHandlerInvoker; +import io.github.tcdl.msb.threading.MessageHandlerInvoker; +import io.github.tcdl.msb.threading.ThreadPoolMessageHandlerInvoker; import java.io.IOException; import java.time.Clock; @@ -97,7 +104,23 @@ public static Message createMsbRequestMessageNoPayload(String namespace, String MsbConfig msbConf = createMsbConfigurations(); Clock clock = Clock.systemDefaultZone(); - Topics topic = new Topics(namespace, replyTopic); + Topics topic = new Topics(namespace, replyTopic, null); + + MetaMessage.Builder metaBuilder = createSimpleMetaBuilder(msbConf, clock); + return new Message.Builder() + .withCorrelationId(Utils.generateId()) + .withId(Utils.generateId()) + .withTopics(topic) + .withMetaBuilder(metaBuilder) + .withPayload(null) + .build(); + } + + public static Message createMsbForwardMessageNoPayload(String namespace, String forwardTopic) { + MsbConfig msbConf = createMsbConfigurations(); + Clock clock = Clock.systemDefaultZone(); + + Topics topic = new Topics(namespace, null, forwardTopic); MetaMessage.Builder metaBuilder = createSimpleMetaBuilder(msbConf, clock); return new Message.Builder() @@ -136,16 +159,20 @@ public static Message createMsbRequestMessage(String topicTo, String payloadStri } public static Message createMsbRequestMessage(String topicTo, String instanceId, RestPayload payload, String... tags) { + return createMsbRequestMessage(topicTo, instanceId, null, payload, tags); + } + + public static Message createMsbRequestMessage(String topicTo, String instanceId, String correlationId, RestPayload payload, String... tags) { ObjectMapper payloadMapper = createMessageMapper(); JsonNode payloadNode = Utils.convert(payload, JsonNode.class, payloadMapper); - return createMsbRequestMessage(topicTo, instanceId, null, payloadNode, tags); + return createMsbRequestMessage(topicTo, instanceId, correlationId, payloadNode, tags); } private static Message createMsbRequestMessage(String topicTo, String instanceId, String correlationId, JsonNode payloadNode, String... tags) { MsbConfig msbConf = createMsbConfigurations(instanceId); Clock clock = Clock.systemDefaultZone(); - Topics topic = new Topics(topicTo, topicTo + ":response:" + msbConf.getServiceDetails().getInstanceId()); + Topics topic = new Topics(topicTo, topicTo + ":response:" + msbConf.getServiceDetails().getInstanceId(), null); MetaMessage.Builder metaBuilder = createSimpleMetaBuilder(msbConf, clock); Message.Builder builder = new Message.Builder() @@ -177,7 +204,7 @@ public static Message createMsbResponseMessageWithAckNoPayload(Acknowledge ack, MsbConfig msbConf = createMsbConfigurations(); Clock clock = Clock.systemDefaultZone(); - Topics topic = new Topics(topicTo, null); + Topics topic = new Topics(topicTo, null, null); MetaMessage.Builder metaBuilder = createSimpleMetaBuilder(msbConf, clock); return new Message.Builder() .withCorrelationId(Utils.ifNull(correlationId, Utils.generateId())) @@ -189,10 +216,26 @@ public static Message createMsbResponseMessageWithAckNoPayload(Acknowledge ack, .build(); } + public static Message createMsbResponseMessage(Acknowledge ack, JsonNode payload, String topicTo, String correlationId) { + MsbConfig msbConf = createMsbConfigurations(); + Clock clock = Clock.systemDefaultZone(); + + Topics topic = new Topics(topicTo, null, null); + MetaMessage.Builder metaBuilder = createSimpleMetaBuilder(msbConf, clock); + return new Message.Builder() + .withCorrelationId(Utils.ifNull(correlationId, Utils.generateId())) + .withId(Utils.generateId()) + .withTopics(topic) + .withMetaBuilder(metaBuilder) + .withPayload(payload) + .withAck(ack) + .build(); + } + public static Message.Builder createMessageBuilder(Clock clock) { MsbConfig msbConf = createMsbConfigurations(); - Topics topic = new Topics("", ""); + Topics topic = new Topics("", "", null); MetaMessage.Builder metaBuilder = createSimpleMetaBuilder(msbConf, clock); return new Message.Builder() .withCorrelationId(Utils.generateId()) @@ -396,7 +439,8 @@ public MsbContextImpl build() { MsbConfig msbConfig = msbConfigOp.orElse(TestUtils.createMsbConfigurations()); Clock clock = clockOp.orElse(Clock.systemDefaultZone()); ObjectMapper messageMapper = createMessageMapper(); - ChannelManager channelManager = channelManagerOp.orElseGet(() -> new ChannelManager(msbConfig, clock, new JsonValidator(), messageMapper)); + ChannelManager channelManager = channelManagerOp.orElseGet(() -> new ChannelManager( + msbConfig, clock, new JsonValidator(), messageMapper, new AdapterFactoryLoader(msbConfig).getAdapterFactory(), new DirectMessageHandlerInvoker())); MessageFactory messageFactory = messageFactoryOp.orElseGet(() -> new MessageFactory(msbConfig.getServiceDetails(), clock, messageMapper)); TimeoutManager timeoutManager = timeoutManagerOp.orElseGet(() -> new TimeoutManager(1)); CollectorManagerFactory collectorManagerFactory = collectorManagerFactoryOp.orElseGet(() -> new CollectorManagerFactory(channelManager)); @@ -410,7 +454,7 @@ public MsbContextImpl build() { private static class TestMsbContext extends MsbContextImpl { TestMsbContext(MsbConfig msbConfig, MessageFactory messageFactory, ChannelManager channelManager, Clock clock, TimeoutManager timeoutManager, CollectorManagerFactory collectorManagerFactory) { - super(msbConfig, messageFactory, channelManager, clock, timeoutManager, createMessageMapper(), collectorManagerFactory); + super(msbConfig, messageFactory, channelManager, clock, timeoutManager, createMessageMapper(), collectorManagerFactory, new MutableCallbackHandler()); } public void setFactory(ObjectFactory objectFactory) { diff --git a/core/src/test/java/io/github/tcdl/msb/support/UtilsTest.java b/core/src/test/java/io/github/tcdl/msb/support/UtilsTest.java index 35ee33e9..bebbffa6 100644 --- a/core/src/test/java/io/github/tcdl/msb/support/UtilsTest.java +++ b/core/src/test/java/io/github/tcdl/msb/support/UtilsTest.java @@ -2,17 +2,19 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.github.tcdl.msb.api.exception.JsonConversionException; +import org.junit.Ignore; import org.junit.Test; import java.time.Instant; import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; import static io.github.tcdl.msb.support.Utils.TOPIC_ANNOUNCE; import static io.github.tcdl.msb.support.Utils.TOPIC_HEARTBEAT; import static io.github.tcdl.msb.support.Utils.isServiceTopic; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; public class UtilsTest { @Test @@ -54,6 +56,20 @@ public void testJsonDeserializationWithDefaultMapper() throws Exception { assertEquals("value", bean.getField()); } + @Test + public void testJsonDeserializationFromNull() throws Exception { + ObjectMapper objectMapper = new ObjectMapper(); + SimpleBean bean = Utils.fromJson(null, SimpleBean.class, objectMapper); + assertNull(bean); + } + + @Test + public void testJsonDeserializationFromEmptyString() throws Exception { + ObjectMapper objectMapper = new ObjectMapper(); + SimpleBean bean = Utils.fromJson("", SimpleBean.class, objectMapper); + assertNull(bean); + } + @Test public void testConvert() { int VALUE = 10; @@ -76,4 +92,26 @@ public void testConversionError() { Utils.convert(pojo, Integer.class, objectMapper); } + @Test + public void testGracefulShutdown() throws Exception { + ExecutorService executorService = mock(ExecutorService.class); + when(executorService.awaitTermination(anyInt(), eq(TimeUnit.SECONDS))) + .thenReturn(false, false, true); + Utils.gracefulShutdown(executorService, "any"); + verify(executorService, times(1)).shutdown(); + verify(executorService, times(3)).awaitTermination(anyInt(), eq(TimeUnit.SECONDS)); + + } + + @Test + public void testGracefulShutdownInterrupted() throws Exception { + ExecutorService executorService = mock(ExecutorService.class); + doThrow(InterruptedException.class) + .when(executorService) + .awaitTermination(anyInt(), eq(TimeUnit.SECONDS)); + Utils.gracefulShutdown(executorService, "any"); + verify(executorService, times(1)).shutdown(); + verify(executorService, times(1)).awaitTermination(anyInt(), eq(TimeUnit.SECONDS)); + } + } \ No newline at end of file diff --git a/core/src/test/java/io/github/tcdl/msb/threading/ConsumerExecutorFactoryTest.java b/core/src/test/java/io/github/tcdl/msb/threading/ConsumerExecutorFactoryTest.java new file mode 100644 index 00000000..74c58e6a --- /dev/null +++ b/core/src/test/java/io/github/tcdl/msb/threading/ConsumerExecutorFactoryTest.java @@ -0,0 +1,66 @@ +package io.github.tcdl.msb.threading; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +@RunWith(MockitoJUnitRunner.class) +public class ConsumerExecutorFactoryTest { + + private ConsumerExecutorFactoryImpl factory; + + @Before + public void setUp() { + factory = new ConsumerExecutorFactoryImpl(); + } + + @Test + public void testCreateConsumerThreadPoolBoundedQueue() { + ExecutorService consumerThreadPool = factory.createConsumerThreadPool(5, 20); + + assertNotNull(consumerThreadPool); + assertTrue(consumerThreadPool instanceof ThreadPoolExecutor); + ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) consumerThreadPool; + assertEquals(5, threadPoolExecutor.getCorePoolSize()); + BlockingQueue queue = threadPoolExecutor.getQueue(); + assertNotNull(queue); + assertTrue(queue instanceof ArrayBlockingQueue); + assertEquals(20, queue.remainingCapacity()); + } + + @Test + public void testCreateConsumerThreadPoolUnboundedQueue() { + ExecutorService consumerThreadPool = factory.createConsumerThreadPool(5, -1); + + assertNotNull(consumerThreadPool); + assertTrue(consumerThreadPool instanceof ThreadPoolExecutor); + ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) consumerThreadPool; + assertEquals(5, threadPoolExecutor.getCorePoolSize()); + + BlockingQueue queue = threadPoolExecutor.getQueue(); + assertNotNull(queue); + assertTrue(queue instanceof LinkedBlockingQueue); + } + + @Test + public void testThreadNames() { + ExecutorService executor1 = factory.createConsumerThreadPool(1, -1); + ExecutorService executor2 = factory.createConsumerThreadPool(1, -1); + + executor1.execute(() -> assertThat(Thread.currentThread().getName(), is("msb-consumer-thread-1"))); + executor2.execute(() -> assertThat(Thread.currentThread().getName(), is("msb-consumer-thread-2"))); + } +} diff --git a/core/src/test/java/io/github/tcdl/msb/threading/DirectInvocationCapableInvokerTest.java b/core/src/test/java/io/github/tcdl/msb/threading/DirectInvocationCapableInvokerTest.java new file mode 100644 index 00000000..0704ec2d --- /dev/null +++ b/core/src/test/java/io/github/tcdl/msb/threading/DirectInvocationCapableInvokerTest.java @@ -0,0 +1,78 @@ +package io.github.tcdl.msb.threading; + +import io.github.tcdl.msb.MessageHandler; +import io.github.tcdl.msb.acknowledge.AcknowledgementHandlerInternal; +import io.github.tcdl.msb.api.message.Message; +import io.github.tcdl.msb.collector.ExecutionOptionsAwareMessageHandler; +import io.github.tcdl.msb.support.TestUtils; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class DirectInvocationCapableInvokerTest { + + + final String namespace = "some:namespace"; + + @Mock + AcknowledgementHandlerInternal ackHandler; + @Mock + MessageHandlerInvoker clientMessageHandlerInvoker; + @Mock + DirectMessageHandlerInvoker directMessageHandlerInvoker; + + DirectInvocationCapableInvoker instance; + + @Before + public void setUp() throws Exception { + instance = new DirectInvocationCapableInvoker(clientMessageHandlerInvoker, directMessageHandlerInvoker); + } + + @Test + public void execute_shouldUseInternalDirectInvoker() throws Exception { + ExecutionOptionsAwareMessageHandler messageHandler = mock(ExecutionOptionsAwareMessageHandler.class); + when(messageHandler.forceDirectInvocation()).thenReturn(true); + + Message message = TestUtils.createSimpleRequestMessage(namespace); + instance.execute(messageHandler, message, ackHandler); + + verify(directMessageHandlerInvoker).execute(eq(messageHandler), eq(message), eq(ackHandler)); + verify(clientMessageHandlerInvoker, never()).execute(any(MessageHandler.class), any(Message.class), any(AcknowledgementHandlerInternal.class)); + } + + @Test + public void execute_shouldUseClientInvoker_whenNoDirectInvocationNeeded() throws Exception { + ExecutionOptionsAwareMessageHandler messageHandler = mock(ExecutionOptionsAwareMessageHandler.class); + when(messageHandler.forceDirectInvocation()).thenReturn(false); + + Message message = TestUtils.createSimpleRequestMessage(namespace); + instance.execute(messageHandler, message, ackHandler); + + verify(clientMessageHandlerInvoker).execute(eq(messageHandler), eq(message), eq(ackHandler)); + verify(directMessageHandlerInvoker, never()).execute(any(MessageHandler.class), any(Message.class), any(AcknowledgementHandlerInternal.class)); + } + + @Test + public void execute_shouldUseClientInvoker_whenHandlerIsNotDirectlyInvokable() throws Exception { + MessageHandler messageHandler = mock(MessageHandler.class); + + Message message = TestUtils.createSimpleRequestMessage(namespace); + instance.execute(messageHandler, message, ackHandler); + + verify(clientMessageHandlerInvoker).execute(eq(messageHandler), eq(message), eq(ackHandler)); + } + + @Test + public void shutdown() throws Exception { + instance.shutdown(); + verify(clientMessageHandlerInvoker).shutdown(); + verify(directMessageHandlerInvoker).shutdown(); + } +} \ No newline at end of file diff --git a/core/src/test/java/io/github/tcdl/msb/threading/DirectMessageHandlerInvokerTest.java b/core/src/test/java/io/github/tcdl/msb/threading/DirectMessageHandlerInvokerTest.java new file mode 100644 index 00000000..e08811b0 --- /dev/null +++ b/core/src/test/java/io/github/tcdl/msb/threading/DirectMessageHandlerInvokerTest.java @@ -0,0 +1,36 @@ +package io.github.tcdl.msb.threading; + +import io.github.tcdl.msb.MessageHandler; +import io.github.tcdl.msb.acknowledge.AcknowledgementHandlerInternal; +import io.github.tcdl.msb.api.message.Message; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@RunWith(MockitoJUnitRunner.class) +public class DirectMessageHandlerInvokerTest { + + @Mock + MessageHandler messageHandler; + + @Mock + AcknowledgementHandlerInternal acknowledgeHandler; + + Message message; + + @InjectMocks + DirectMessageHandlerInvoker strategy; + + @Test + public void testDirectInvoke() { + strategy.execute(messageHandler, message, acknowledgeHandler); + verify(messageHandler, times(1)).handleMessage(message, acknowledgeHandler); + verify(acknowledgeHandler, times(1)).autoConfirm(); + } + +} \ No newline at end of file diff --git a/core/src/test/java/io/github/tcdl/msb/threading/GroupedMessageHandlerInvokerTest.java b/core/src/test/java/io/github/tcdl/msb/threading/GroupedMessageHandlerInvokerTest.java new file mode 100644 index 00000000..11f37bc7 --- /dev/null +++ b/core/src/test/java/io/github/tcdl/msb/threading/GroupedMessageHandlerInvokerTest.java @@ -0,0 +1,173 @@ +package io.github.tcdl.msb.threading; + +import io.github.tcdl.msb.MessageHandler; +import io.github.tcdl.msb.acknowledge.AcknowledgementHandlerInternal; +import io.github.tcdl.msb.api.message.Message; +import io.github.tcdl.msb.config.MsbConfig; +import io.github.tcdl.msb.support.TestUtils; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class GroupedMessageHandlerInvokerTest { + private static final int CONFIG_THREADS = 5; + private static final int CONFIG_QUEUE = -1; + + private ExecutorService[] executors; + + @Mock + AcknowledgementHandlerInternal acknowledgeHandler; + + @Mock + MessageHandler messageHandler; + + @Mock + ConsumerExecutorFactory consumerExecutorFactory; + + @Mock + MsbConfig msbConfig; + + @Mock + MessageGroupStrategy messageGroupStrategy; + + private Message message = TestUtils.createMsbRequestMessage("any0", "any0"); + + private Message message1 = TestUtils.createMsbRequestMessage("any1", "any1"); + + private Message message2 = TestUtils.createMsbRequestMessage("any2", "any2"); + + private GroupedMessageHandlerInvoker invoker; + + @Before + public void setUp() throws Exception { + + executors = new ExecutorService[CONFIG_THREADS]; + + for(int i = 0; i< CONFIG_THREADS ; i++) { + executors[i] = mock(ExecutorService.class); + when(executors[i].awaitTermination(10, TimeUnit.SECONDS)).thenReturn(true); + } + + when(msbConfig.getConsumerThreadPoolSize()).thenReturn(CONFIG_THREADS); + when(msbConfig.getConsumerThreadPoolQueueCapacity()).thenReturn(CONFIG_QUEUE); + + when(consumerExecutorFactory.createConsumerThreadPool(1, CONFIG_QUEUE)) + .thenReturn(executors[0], Arrays.copyOfRange(executors, 1, CONFIG_THREADS)); + + List invokers = IntStream + .range(0, CONFIG_THREADS) + .boxed() + .map(i -> new ThreadPoolMessageHandlerInvoker(1, CONFIG_QUEUE, consumerExecutorFactory)) + .collect(Collectors.toList()); + invoker = new GroupedMessageHandlerInvoker<>(invokers, messageGroupStrategy); + + when(messageGroupStrategy.getMessageGroupId(message)).thenReturn(Optional.of(0)); + when(messageGroupStrategy.getMessageGroupId(message1)).thenReturn(Optional.of(1)); + when(messageGroupStrategy.getMessageGroupId(message2)).thenReturn(Optional.of(2)); + } + + @Test + public void testExecutorsInitialized() { + verify(consumerExecutorFactory, times(CONFIG_THREADS)).createConsumerThreadPool(1, CONFIG_QUEUE); + } + + @Test + public void testMessageHandling() { + invoker.execute(messageHandler, message, acknowledgeHandler); + verify(messageHandler, never()).handleMessage(any(), any()); + ArgumentCaptor taskCaptor = ArgumentCaptor.forClass(MessageProcessingTask.class); + verify(executors[0], times(1)).submit(taskCaptor.capture()); + MessageProcessingTask task = taskCaptor.getValue(); + + assertEquals(message, task.getMessage()); + assertEquals(messageHandler, task.getMessageHandler()); + assertEquals(acknowledgeHandler, task.getAckHandler()); + } + + @Test + public void testMessageRouting() { + invoker.execute(messageHandler, message, acknowledgeHandler); + verify(executors[0], times(1)).submit(any(MessageProcessingTask.class)); + + verify(executors[1], times(0)).submit(any(MessageProcessingTask.class)); + + invoker.execute(messageHandler, message2, acknowledgeHandler); + verify(executors[2], times(1)).submit(any(MessageProcessingTask.class)); + } + + @Test + public void testMessageRoutingGroupOverflow() { + when(messageGroupStrategy.getMessageGroupId(message)).thenReturn(Optional.of(CONFIG_THREADS * 1231 + 3)); + + invoker.execute(messageHandler, message, acknowledgeHandler); + verify(executors[3], times(1)).submit(any(MessageProcessingTask.class)); + } + + public void testMessageRoutingGroupNegative() { + when(messageGroupStrategy.getMessageGroupId(message)).thenReturn(Optional.of(-1)); + + invoker.execute(messageHandler, message, acknowledgeHandler); + verify(executors[CONFIG_THREADS - 1], times(1)).submit(any(MessageProcessingTask.class)); + } + + @Test + public void testMessageRoutingGroupMissing() { + when(messageGroupStrategy.getMessageGroupId(message)).thenReturn(Optional.empty()); + + Set executorsInvolved = new HashSet<>(); + AtomicInteger invocationsCount = new AtomicInteger(0); + + Arrays.stream(executors).forEach((executor) -> + when(executor.submit(any(MessageProcessingTask.class))) + .thenAnswer((invocation) -> { + executorsInvolved.add(executor); + invocationsCount.incrementAndGet(); + return null; + })); + + int executeCount = 0; + int maxIterations = 5000; + do { + executeCount ++; + invoker.execute(messageHandler, message, acknowledgeHandler); + if(executeCount > maxIterations) { + fail(String.format("All available executors should be involved when message group is defined," + + " failed to check this requirement in %d iterations", maxIterations)); + } + } while (executorsInvolved.size() < CONFIG_THREADS); + + assertEquals(executeCount, invocationsCount.get()); + assertEquals(CONFIG_THREADS, executorsInvolved.size()); + } + + @Test + public void testShutdown() { + Arrays.stream(executors).forEach((executor)->verify(executor, times(0)).shutdown()); + invoker.shutdown(); + Arrays.stream(executors).forEach((executor)->verify(executor, times(1)).shutdown()); + } +} \ No newline at end of file diff --git a/core/src/test/java/io/github/tcdl/msb/threading/MessageHandlerInvokerFactoryTest.java b/core/src/test/java/io/github/tcdl/msb/threading/MessageHandlerInvokerFactoryTest.java new file mode 100644 index 00000000..7b169218 --- /dev/null +++ b/core/src/test/java/io/github/tcdl/msb/threading/MessageHandlerInvokerFactoryTest.java @@ -0,0 +1,61 @@ +package io.github.tcdl.msb.threading; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import java.util.concurrent.ExecutorService; + +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.Assert.assertThat; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class MessageHandlerInvokerFactoryTest { + @Mock + private ConsumerExecutorFactory consumerExecutorFactory; + + private MessageHandlerInvokerFactory messageHandlerInvokerFactory; + + @Before + public void setUp() { + messageHandlerInvokerFactory = new MessageHandlerInvokerFactoryImpl(consumerExecutorFactory); + when(consumerExecutorFactory.createConsumerThreadPool(anyInt(), anyInt())) + .thenReturn(mock(ExecutorService.class)); + } + + @Test + public void testCreateDirectHandlerInvoker() { + MessageHandlerInvoker directInvoker = messageHandlerInvokerFactory.createDirectHandlerInvoker(); + + assertThat(directInvoker, instanceOf(DirectMessageHandlerInvoker.class)); + verifyZeroInteractions(consumerExecutorFactory); + } + + @Test + public void testCreateExecutorBasedHandlerInvoker() { + MessageHandlerInvoker executorBasedInvoker = messageHandlerInvokerFactory.createExecutorBasedHandlerInvoker(5, 10); + + assertThat(executorBasedInvoker, instanceOf(ExecutorBasedMessageHandlerInvoker.class)); + verify(consumerExecutorFactory).createConsumerThreadPool(5, 10); + } + + @Test + public void testCreateGroupedExecutorBasedHandlerInvoker() { + MessageHandlerInvokerFactory messageHandlerInvokerFactorySpy = spy(messageHandlerInvokerFactory); + MessageHandlerInvoker executorBasedInvoker = messageHandlerInvokerFactorySpy + .createGroupedExecutorBasedHandlerInvoker(2, -1, mock(MessageGroupStrategy.class)); + + assertThat(executorBasedInvoker, instanceOf(GroupedMessageHandlerInvoker.class)); + verify(messageHandlerInvokerFactorySpy, times(2)) + .createExecutorBasedHandlerInvoker(1, -1); + } +} diff --git a/core/src/test/java/io/github/tcdl/msb/threading/MessageProcessingTaskTest.java b/core/src/test/java/io/github/tcdl/msb/threading/MessageProcessingTaskTest.java new file mode 100644 index 00000000..fcad0eae --- /dev/null +++ b/core/src/test/java/io/github/tcdl/msb/threading/MessageProcessingTaskTest.java @@ -0,0 +1,107 @@ +package io.github.tcdl.msb.threading; + +import static org.junit.Assert.*; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.*; + +import io.github.tcdl.msb.support.TestUtils; + +import io.github.tcdl.msb.MessageHandler; + +import java.io.Closeable; +import java.io.IOException; +import java.util.concurrent.*; +import java.util.function.Supplier; + +import io.github.tcdl.msb.acknowledge.AcknowledgementHandlerImpl; +import io.github.tcdl.msb.api.message.Message; +import org.junit.Before; +import org.junit.Test; + +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.slf4j.MDC; + +@RunWith(MockitoJUnitRunner.class) +public class MessageProcessingTaskTest { + private final String MDC_KEY = "key"; + private final String MDC_VALUE = "any"; + + private Message message; + + @Mock + private MessageHandler mockMessageHandler; + + @Mock + private AcknowledgementHandlerImpl mockAcknowledgementHandler; + + private MessageProcessingTask task; + + @Before + public void setUp() { + message = TestUtils.createSimpleRequestMessage("any"); + task = new MessageProcessingTask(mockMessageHandler, message, mockAcknowledgementHandler); + } + + @Test + public void testMessageProcessing() throws IOException { + task.run(); + verify(mockMessageHandler).handleMessage(any(), eq(mockAcknowledgementHandler)); + verify(mockAcknowledgementHandler, times(1)).autoConfirm(); + verifyNoMoreInteractions(mockAcknowledgementHandler); + } + + @Test + public void testExceptionDuringProcessing() { + testThrowableDuringProcessing(RuntimeException::new); + } + + @Test + public void testErrorDuringProcessing() { + testThrowableDuringProcessing(AssertionError::new); + } + + private void testThrowableDuringProcessing(Supplier throwable) { + doThrow(throwable.get()).when(mockMessageHandler).handleMessage(any(), any()); + + try { + task.run(); + verify(mockAcknowledgementHandler, times(1)).autoRetry(); + verifyNoMoreInteractions(mockAcknowledgementHandler); + } catch (Exception e) { + fail(); + } + } + + @Test + public void testMdcProvided() throws Exception { + try(Closeable mdcCloseable = MDC.putCloseable(MDC_KEY, MDC_VALUE)) { + assertTrue("MDC data is missing in a thread while was provided", isMdcPresentInThread()); + } + } + + @Test + public void testMdcNotProvided() throws Exception { + assertFalse("MDC data is present in a thread while was not provided", isMdcPresentInThread()); + } + + private boolean isMdcPresentInThread() throws Exception{ + CompletableFuture isMdcPresentInTaskRun = new CompletableFuture<>(); + CompletableFuture isMdcPresentInOtherRun = new CompletableFuture<>(); + MessageHandler mdcMessageHandler = (message, acknowledgeHandler) -> { + isMdcPresentInTaskRun.complete(isMdcPresent()); + }; + MessageProcessingTask mdcTask = + new MessageProcessingTask(mdcMessageHandler, message, mockAcknowledgementHandler); + ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(); + singleThreadExecutor.execute(mdcTask); + singleThreadExecutor.execute(() -> isMdcPresentInOtherRun.complete(isMdcPresent())); + assertFalse("MDC data cleanup was not performed", isMdcPresentInOtherRun.get()); + return isMdcPresentInTaskRun.get(); + } + + private boolean isMdcPresent() { + return MDC_VALUE.equals(MDC.get(MDC_KEY)); + } +} \ No newline at end of file diff --git a/core/src/test/java/io/github/tcdl/msb/threading/ThreadPoolMessageHandlerInvokerImplTest.java b/core/src/test/java/io/github/tcdl/msb/threading/ThreadPoolMessageHandlerInvokerImplTest.java new file mode 100644 index 00000000..478515e0 --- /dev/null +++ b/core/src/test/java/io/github/tcdl/msb/threading/ThreadPoolMessageHandlerInvokerImplTest.java @@ -0,0 +1,81 @@ +package io.github.tcdl.msb.threading; + +import com.typesafe.config.ConfigFactory; +import io.github.tcdl.msb.MessageHandler; +import io.github.tcdl.msb.acknowledge.AcknowledgementHandlerInternal; +import io.github.tcdl.msb.api.message.Message; +import io.github.tcdl.msb.config.MsbConfig; +import io.github.tcdl.msb.support.TestUtils; +import io.github.tcdl.msb.threading.ThreadPoolMessageHandlerInvoker; +import io.github.tcdl.msb.threading.MessageProcessingTask; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.*; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class ThreadPoolMessageHandlerInvokerImplTest { + + private static final int CONFIG_THREADS = 5; + private static final int CONFIG_QUEUE = -1; + + @Mock + ExecutorService mockExecutor; + + @Mock + AcknowledgementHandlerInternal acknowledgeHandler; + + @Mock + MessageHandler messageHandler; + + @Mock + ConsumerExecutorFactory consumerExecutorFactory; + + Message message = TestUtils.createMsbRequestMessage("any","any"); + + ThreadPoolMessageHandlerInvoker invoker; + + @Before + public void setUp() throws Exception { + + when(consumerExecutorFactory.createConsumerThreadPool(CONFIG_THREADS, CONFIG_QUEUE)).thenReturn(mockExecutor); + + invoker = new ThreadPoolMessageHandlerInvoker(CONFIG_THREADS, CONFIG_QUEUE, consumerExecutorFactory); + + when(mockExecutor.awaitTermination(10, TimeUnit.SECONDS)).thenReturn(true); + } + + @Test + public void testSingleExecutorInitialized() { + verify(consumerExecutorFactory, times(1)).createConsumerThreadPool(CONFIG_THREADS, CONFIG_QUEUE); + } + + @Test + public void testMessageHandling() { + invoker.execute(messageHandler, message, acknowledgeHandler); + verify(messageHandler, never()).handleMessage(any(), any()); + ArgumentCaptor taskCaptor = ArgumentCaptor.forClass(MessageProcessingTask.class); + verify(mockExecutor, times(1)).submit(taskCaptor.capture()); + MessageProcessingTask task = taskCaptor.getValue(); + + assertEquals(message, task.getMessage()); + assertEquals(messageHandler, task.getMessageHandler()); + assertEquals(acknowledgeHandler, task.getAckHandler()); + } + + @Test + public void testShutdown() { + verify(mockExecutor, times(0)).shutdown(); + invoker.shutdown(); + verify(mockExecutor, times(1)).shutdown(); + } +} \ No newline at end of file diff --git a/core/src/test/resources/log4j.xml b/core/src/test/resources/log4j.xml deleted file mode 100644 index da0ed90a..00000000 --- a/core/src/test/resources/log4j.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/core/src/test/resources/logback-test.xml b/core/src/test/resources/logback-test.xml new file mode 100644 index 00000000..200166fb --- /dev/null +++ b/core/src/test/resources/logback-test.xml @@ -0,0 +1,15 @@ + + + + + UTF-8 + %d{HH:mm:ss.SSS} %-5level %logger [%t] - %m%n + + + + + + + + + \ No newline at end of file diff --git a/core/src/test/resources/reference.conf b/core/src/test/resources/reference.conf index efc69b31..483858a1 100644 --- a/core/src/test/resources/reference.conf +++ b/core/src/test/resources/reference.conf @@ -13,11 +13,20 @@ msbConfig { # Enable/disable message validation against json schema validateMessage = true - brokerAdapterFactory = "io.github.tcdl.msb.adapters.mock.MockAdapterFactory" # in memory broker + brokerAdapterFactory = "io.github.tcdl.msb.mock.adapterfactory.TestMsbAdapterFactory" # in memory broker # Broker Adapter Defaults brokerConfig = { } + # Mapped Diagnostic Context logging settings + mdcLogging = { + enabled = true + splitTagsBy = ":" + messageKeys = { # Mapped Diagnostic Context keys + messageTags = "msbTags" + correlationId = "msbCorrelationId" + } + } } diff --git a/doc/MSB-TEST-MOCKING.md b/doc/MSB-TEST-MOCKING.md new file mode 100644 index 00000000..28940ff0 --- /dev/null +++ b/doc/MSB-TEST-MOCKING.md @@ -0,0 +1,116 @@ +# MSB-Java mocking support +This documents describes experimental MSB-Java mocking approaches. +There are two different testing approaches available: AdapterFactory-based and ObjectFactory-based. +Both of them are Mockito-based. + +## Maven dependency +In order to use mocking utilities described, please use the following Maven dependency: +``` + + io.github.tcdl.msb + msb-java-core + test-jar + ${msb.version} + test + +``` + +## ObjectFactory-based approach +**Related package:** _io.github.tcdl.msb.mock.objectfactory_ + +This type of test support is used to test client's code. For example, it gives an ability +to invoke incoming handlers directly (with a custom test payload as an argument). +So MSB internal code is not involved in the testing. + +Typical usage: + - Inject mocked MsbContext configured to return TestMsbObjectFactory and extract a storage from this mock: + +``` java +@Mock +private MsbContext msbContext; +private TestMsbStorageForObjectFactory storage; +... +@Before +public void setUp() throws Exception { + when(msbContext.getObjectFactory()) + .thenReturn(new TestMsbObjectFactory()); + storage = TestMsbStorageForObjectFactory.extract(msbContext); + ... +} +``` + + - Perform captured requester testing (including direct handlers invocations): +``` java +RequesterCapture requesterCapture = + storage.getRequesterCapture("my:namespace:out"); +assertEquals(MyPalyload.class, requesterCapture.getPayloadClass()); + +//invoke onEnd handler +Callback onEndHandler = requesterCapture.getOnEndCaptor().getValue(); +onEndHandler.call(null); + +//invoke onResponse handler +BiConsumer onResponseHandler = requesterCapture.getOnResponseCaptor().getValue(); +onResponseHandler.accept(myPalyload, messageContextMock); +``` + + - Perform captured responder testing (including direct handlers invocations): +``` java +ResponderCapture responderCapture = storage.getResponderCapture("my:namespace:in"); +assertEquals(MyPalyload.class, responderCapture.getPayloadClass()); + +//invoke requestHandler +ResponderServer.RequestHandler requestHandler = responderCapture.getRequestHandler(); +requestHandler.process(myPalyload, responderContextMock); +``` + +## AdapterFactory-based approach +**Related package:** _io.github.tcdl.msb.mock.adapterfactory_ + +This type of test support is used to test the flow of an incoming message through all MSB layers. +Using this option makes it possible both submit a raw message JSON into a namespace, and capture outgoing messages raw JSON. + +Typical usage: + - Configure Msb to use the test AdapterFactory implementation: +``` +msbConfig { + brokerAdapterFactory = "io.github.tcdl.msb.mock.adapterfactory.TestMsbAdapterFactory" +} +``` + + - Inject non-mocked MsbContext that is using this configuration and extract a storage from this mock: +``` java +private MsbContext msbContext; +private TestMsbStorageForAdapterFactory storage; +... +@Before +public void setUp() throws Exception { + msbContext = TestUtils.createSimpleMsbContext(); + storage = TestMsbStorageForAdapterFactory.extract(msbContext); +} + +``` + + - Submit incoming messages JSON using: +``` java +storage.publishIncomingMessage("my:incoming:namespace", "{my: 'message_json'}"); +``` + + - Verify outgoing message JSON using: +``` java +List publishedTestMessages = storage.getOutgoingMessages("my:out:namespace"); +String publishedMessage = storage.getOutgoingMessage("my:out:namespace"); +``` + + - During the testing, perform JSON String - Message conversions using MSB utilities if required: +``` java +Utils.fromJson(json, Message.class, objectMapper); +Utils.toJson(message, objectMapper); +``` + + - If several MsbContext instances are involved into the testing, they will be able to handle only their own messages. + In order to use shared messaging, it is required to connect MsbContext instances to a single storage: +``` java +MsbContext otherContext = TestUtils.createSimpleMsbContext(); +storage.connect(otherContext); +``` diff --git a/doc/MSB.md b/doc/MSB.md index a7d6c3ac..26f41eb5 100644 --- a/doc/MSB.md +++ b/doc/MSB.md @@ -14,6 +14,12 @@ The diagram below shows one possible approach when microservices communicate thr MSB = MicroService Bus. +MSb-Java version number MAJOR.MINOR.PATCH, is incremented when: + +MAJOR version - MSB compatible protocol was changed +MINOR version - incompatible API changes was made, +PATCH version - added functionality in a backwards-compatible manner, or backwards-compatible bug fixes was made. + # Supported microservice models Below we consider different ways in which microservices can interact with each other using MSB-Java. @@ -53,7 +59,9 @@ Here's an example of a message: "correlationId": "3c19407acf3218000003598f", "topics": { "to": "test:aggregator", - "response": "test:aggregator:response:3c19407acf32180000016402" + "response": "test:aggregator:response:3c19407acf32180000016402", + "forward": "test:proxy", + "routingKey": "to.santa.claus" }, "meta": { "ttl": null, @@ -93,6 +101,8 @@ correlationId | unique id of message sequence related to single conver topics | section for routing information to | name of the topic this message is sent to response | name of the topic where response to this message is expected + forward | name of the topic for a message forwarding + routingKey | routing key that was used when message was published or should be used for forwarding meta | section for message meta information ttl | time to live of a message. If ttl is exceeded an incoming message is ignored createdAt | timezone-aware date/time when message was created @@ -126,8 +136,10 @@ MSB-Java has pluggable architecture that allows to use different bus adapters tr It consists of the following Maven modules: - msb-java-core: core classes - msb-java-amqp: AMQP adapter that allow to use AMQP broker (for example RabbitMQ) as a bus -- msb-java-cli: CLI monitoring tool +- msb-spring-boot-starter: Spring Boot auto-configuration classes +- msb-java-acceptance: acceptance tests - msb-java-examples: examples of various microservices +- ApacheJmeter_msb: MSB JMeter Sampler ## Core classes @@ -204,11 +216,7 @@ public class PingService { ```java package io.github.tcdl.msb.examples; -import io.github.tcdl.msb.api.MessageTemplate; -import io.github.tcdl.msb.api.MsbContext; -import io.github.tcdl.msb.api.MsbContextBuilder; -import io.github.tcdl.msb.api.ObjectFactory; -import io.github.tcdl.msb.api.ResponderServer; +import io.github.tcdl.msb.api.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -222,11 +230,13 @@ public class PongService { ObjectFactory objectFactory = msbContext.getObjectFactory(); MessageTemplate messageTemplate = new MessageTemplate().withTags("pong-static-tag"); - ResponderServer responderServer = objectFactory.createResponderServer("pingpong:namespace", messageTemplate, (request, responder) -> { + ResponderOptions responderOptions = new ResponderOptions.Builder().withMessageTemplate(messageTemplate).build(); + ResponderServer responderServer = objectFactory.createResponderServer("pingpong:namespace", responderOptions, + (request, responderContext) -> { // Response handling logic LOG.info(String.format("Handling %s...", request)); - responder.send("PONG"); + responderContext.getResponder().send("PONG"); LOG.info("Response sent"); }, String.class); @@ -239,9 +249,9 @@ To launch those you need to have RabbitMQ up and running. Also you have to start Another caveat is that the business logic of `PongService` (passed as lambda in the last argument during `ResponderServer` creation) can be invoked from different threads concurrently. So the lambda should be __thread-safe__. The same applies to the lambda passed as an argument to `onResponse` in `PingService`. -## CLI tool +## Test support - mocking approaches. -See [Readme for CLI tool](/cli/README.md). +See [MSB-Java mocking support](MSB-TEST-MOCKING.md) # Appendix @@ -267,21 +277,23 @@ See [reference.conf example](/core/src/main/resources/reference.conf) _application.conf_ – applications should provide an application.conf with any settings specific for this particular application. Overrides values from _reference.conf_. -See [application.conf example](/cli/src/main/resources/application.conf) - All configuration files use _key-value pair_ structure. ### Configuration files hierarchy - [amqp.conf](/amqp/src/main/resources/amqp.conf) - [reference.conf](/core/src/main/resources/reference.conf) - overrides values from amqp.conf -- [application.conf](/cli/src/main/resources/application.conf) - overrides values from reference.conf +- [application.conf](/acceptance/src/main/resources/application.conf) - overrides values from reference.conf ### Description of MSB configuration fields + +There is a feature flag for enable/disable MSB: +- `msb-config.enabled` true or missing value means enabled. false for disable whole MSB. It's very useful for testing and for enable/disable the msb capabilities depending of environments (f.e. as feature flag if the functionality is not desirable and/or not tested yet). + Service details section describes microservice parameters. -- `name ` – microservice name. All running instances of the same microservice must have the same name. -- `version` – microservice version. At the moment MSB-Java doesn't handle the value. -- `instanceId` – unique microservice instance id. All running instances of the same microservice must have different instanceId. +- `name ` – microservice name. All running instances of the same microservice must have the same name. Mandatory, has no default value. +- `version` – microservice version. At the moment MSB-Java doesn't handle the value. Mandatory, has no default value. +- `instanceId` – unique microservice instance id. All running instances of the same microservice must have different instanceId. If it is not set explicitly than it is set to random UUID at startup. Value is not preserved on restart. Additional instances of a microservice can help with load balancing. The simplest way to have multiple instances is to deploy, configure and run a microservice from different locations with different instanceId values. In this case a value of a instanceId need to be specified directly (or by means of environment variables, see below) in the application.conf, for example: @@ -307,32 +319,70 @@ Here, the override field `name = ${?MSB_SERVICE_NAME}` simply vanishes if there' `brokerAdapterFactory` – message broker class. Defaults to `"io.github.tcdl.adapters.amqp.AmqpAdapterFactory"`. +### Environment Variables + +- MSB_SERVICE_NAME, mandatory +- MSB_SERVICE_VERSION, mandatory +- MSB_SERVICE_INSTANCE_ID, default random UUID (generated on each startup) +- MSB_BROKER_HOST, default "127.0.0.1". +- MSB_BROKER_PORT, default 5672. +- MSB_BROKER_USER_NAME, default "guest". +- MSB_BROKER_PASSWORD, default "guest". +- MSB_BROKER_VIRTUAL_HOST, default "/". +- MSB_BROKER_USE_SSL, default false. + +### Mapped Diagnostic Context settings +This section provides settings for Mapped Diagnostic Context logging that gives a possibility to save some parameters of the incoming messages into a thread-local storage so it would be easier to track message processing. +The section `mdcLogging`: + +`enabled` - automatic Mapped Diagnostic Context logging toggle, true/false. Defaults to true. + +`splitTagsBy` - if a separator like ":" is provided and is not empty, then for tags like "myTag:myTagValue" the tag value "myTagValue" will be available by a key "myTag" in a Mapped Diagnostic Context. Defaults to ":". + +The nested section `messageKeys`: + +`messageTags` - Mapped Diagnostic Context key for message tags. Defaults to `msbTags`. + +`correlationId` - Mapped Diagnostic Context key for message correlationId. Defaults to `msbCorrelationId`. + +### Description of multithreading configuration +The section `threadingConfig` from [reference.conf](/core/src/main/resources/reference.conf). + +`consumerThreadPoolSize` – number of consumer threads used to process incoming messages. +Defines the level of parallelism. Default is 5. + +`consumerThreadPoolQueueCapacity` – maximum number of requests waiting in FIFO queue to be processed by consumer thread pool. +Incoming messages will be discarded in case of the exceeded limit. +Should be positive integer or -1. Value of -1 stands for unlimited. The default value is -1. + +Please take into an account, then when handling messages in multithreaded mode (with `consumerThreadPoolSize` other than 1), +incoming messages could be processed out of incoming topic order. If the order of incoming messages matters: + - Use `consumerThreadPoolSize` = 1 - process all incoming messages in a single-threaded mode; + - Use MsbContextBuilder.withMessageGroupStrategy() - so messages with the same "groupId" will be processed + in a single-threaded mode while messages with different "groupId" could be processed in parallel. + ### Description of AMQP connection configuration fields The _key values pairs_ described in this section are specific for the chosen Broker. The section `brokerConfig` from [reference.conf](/core/src/main/resources/reference.conf) file override values from [amqp.conf](/amqp/src/main/resources/amqp.conf). - `charsetName` – specifies charset for encoding and decoding of messages. Defaults to "UTF-8" as +`charsetName` – specifies charset for encoding and decoding of messages. Defaults to "UTF-8" as this encoding is used in MSB (Node.js). `host` – IP address of message broker, defaults to "127.0.0.1" `port` – port number -`useSSL` – defines whether we need to use SSL for AMQP connection. For the moment only simplest form of security is implemented that provides encryption but does not check remote certificates. +`useSSL` – defines whether we need to use SSL for AMQP connection. For the moment only simplest form of security is implemented that provides encryption but does not check remote certificates. Is set fo `false` by default. `groupId` – microservices with the same `groupId` subscribed to the same namespace will receive messages from that namespace in round-robin fashion. If microservices have different `groupId`s and subscribed to the same namespace then all of those microservices are going to receive a copy of a message from that namespace. -`durable` – queue durability, true/false. Defaults to false. -Specifies one of the two types of the queue: -`durable` = `true` – durable queue, that survives broker restart -`durable` = `false` – transient queue, is auto removed once the listening service is stopped. +`durable` – queue durability, true/false. Opposite for this value is used to set auto-delete option. Defaults to false. +Specifies types of the queue used for incomming messages (except for responses that are always non-durable and auto-delete): +`durable` = `true` – durable queue, that survives broker restart. Auto-delete is set to false. +`durable` = `false` – transient queue, not survives broker restart. Auto-delete is set to true. See for more [detail](https://www.rabbitmq.com/tutorials/amqp-concepts.html). -`consumerThreadPoolSize` – number of consumer threads used to process incoming messages. Defines the level of parallelism. Default is 5. - -`consumerThreadPoolQueueCapacity` – maximum number of requests waiting in FIFO queue to be processed by consumer thread pool. Should be positive integer or -1. Value of -1 stands for unlimited. - The following fields are optional in case of broker running on local machine but are mandatory when using broker on remote computer. When there is a need to override the default values these fields are specified in application.conf file as additional `brokerConfig` parameters. @@ -347,7 +397,30 @@ More references on how to configure the broker to allow the remote access with t `heartbeatIntervalSec` - interval of the heartbeats that are used to detect broken connections. Zero for none. See for more details: https://www.rabbitmq.com/heartbeats.html. Defaults to 1 second. -`networkRecoveryIntervalMs` - interval of connection recovery attempts. See for more details: https://www.rabbitmq.com/api-guide.html#connection-recovery. Defaults to 5 seconds. +`prefetchCount` - Specify the limit number of unacknowledged messages on a channel when consuming. Value of 0 stands for unlimited. The default value is 10. + +###Autoconfiguration for Srping Boot +Integration with Spring Boot has been improved by adding an [autoconfiguration module](https://github.com/tcdl/msb-java/tree/master/spring-boot-starter). If your application is based on Spring Boot, this module can simplify the usage of msb-java. Using this type of connection msb to your project you'll get thinner dependency list, preconfigured spring beans in your application context and no need to write a single line of configuration (presuming that you have rabbitmq on your local machine with all default values). +####How to start +If you use maven, just add the following dependency: +```xml + + io.github.tcdl.msb + msb-spring-boot-starter + ${msb.version} + +``` +In this case, you'll get MsbConfig, MessageTemplate and MsbContext beans ready to work with your local instance of integration bus (rabbitmq). +####Customization +You can use the full power of spring configuration with msb-spring-boot-starter. Complete list of configuration ways can be found in [Spring Boot documentation](http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-external-config). The idea lies in the fact that we have a simple mapping from [typesafe configuration parameters](https://github.com/tcdl/msb-java/blob/master/core/src/main/resources/reference.conf) to spring ones. Thus msbConfig{...serviceDetails={name=xxx}} will be translated into spring config property msbConfig.serviceDetails.name and can be set in application.yml or .properties or passed as a command line argument or set as an environment variable MSB_CONFIG_SERVICE_DETAILS_NAME and so on. Another aspect of configuration is using your instances of MessageGroupStrategy, MessageTemplate or MsbContext. Each of these beans is present in context but uses @ConditionalOnMissingBean annotation. So, your instances will be used if they appear in spring context. +####Overridden default values +Some of the default values has been overridden in this module to not bring unexpected side effects to projects which already use msb-java without autoconfiguration. +|value |reference|autoconfigure| +|:--------------------------------:|:-------:|:-----------:| +|msbConfig.serviceDetails.name |required |random value | +|msbConfig.serviceDetails.version |required |1.0.0 | +|msbProperties.brokerConfig.durable|false |true | +|msbConfig.timerThreadPoolSize |10 |2 | ## AMQP adapter @@ -361,6 +434,8 @@ An interest twist is related to consumption of incoming messages. The adapter ge The adapter supports AMQP connection recovery out of the box and it's always enabled. It's regulated by `heartbeatIntervalSec` and `networkRecoveryIntervalMs` configuration values (see [this section](#description-of-amqp-connection-configuration-fields) for more details). +The AMQP adapter supports explicit and automation message confirm/reject/retry acknowledgment. If a message was successfully processed, a microservice should enable confirms. In exceptional cases when the microservice is unable to handle messages successfully, reject or retry acknowledgment need be to send. If microservice doesn't explicitly send acknowledgment, MSB-Java can do it automatically after completion of message processing in current thread. If microservice provides more complexity message processing, for example in additional threads, AutoAcknowledgement need to be set to false. In this case a microservice is responsible for acknowledgment. + ## Channel monitoring Built-in channel monitoring allows to monitor micorservices/channels on the bus level. It consists of 2 components: diff --git a/examples/pom.xml b/examples/pom.xml index eff3d6b0..e4a9e475 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -3,7 +3,7 @@ io.github.tcdl.msb msb-java - 1.3.0-SNAPSHOT + 1.6.7-SNAPSHOT ../pom.xml 4.0.0 @@ -17,11 +17,22 @@ https://github.com/tcdl/msb-java HEAD + tcdl https://github.com/tcdl + + + + ch.qos.logback + logback-classic + 1.1.7 + + + + io.github.tcdl.msb @@ -31,6 +42,12 @@ io.github.tcdl.msb msb-java-amqp + + + ch.qos.logback + logback-classic + + \ No newline at end of file diff --git a/examples/src/main/java/io/github/tcdl/msb/examples/ConsumerWithRoutingKeys.java b/examples/src/main/java/io/github/tcdl/msb/examples/ConsumerWithRoutingKeys.java new file mode 100644 index 00000000..64025572 --- /dev/null +++ b/examples/src/main/java/io/github/tcdl/msb/examples/ConsumerWithRoutingKeys.java @@ -0,0 +1,36 @@ +package io.github.tcdl.msb.examples; + +import com.google.common.collect.Sets; +import io.github.tcdl.msb.api.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Run this consumer in conjunction with {@link ProducerWithRoutingKey} to see that only messages sent with + * routing keys 'zero' and 'two' are consumed and the messages sent with routing key 'one' are not. + */ +public class ConsumerWithRoutingKeys { + + private static final Logger LOG = LoggerFactory.getLogger(ConsumerWithRoutingKeys.class); + + public static void main(String[] args) { + MsbContext msbContext = new MsbContextBuilder(). + enableShutdownHook(true). + build(); + + + ObjectFactory objectFactory = msbContext.getObjectFactory(); + + ResponderOptions responderOptions = new AmqpResponderOptions.Builder() + .withBindingKeys(Sets.newHashSet("zero", "two")) + .withExchangeType(ExchangeType.TOPIC) + .build(); + + ResponderServer responderServer = objectFactory.createResponderServer("routing:namespace", + responderOptions, + (request, responderContext) -> { + LOG.info("Received message: {}", request); + }, String.class); + responderServer.listen(); + } +} diff --git a/examples/src/main/java/io/github/tcdl/msb/examples/DateExtractor.java b/examples/src/main/java/io/github/tcdl/msb/examples/DateExtractor.java deleted file mode 100644 index c3b8299d..00000000 --- a/examples/src/main/java/io/github/tcdl/msb/examples/DateExtractor.java +++ /dev/null @@ -1,160 +0,0 @@ -package io.github.tcdl.msb.examples; - -import io.github.tcdl.msb.api.MessageTemplate; -import io.github.tcdl.msb.api.MsbContext; -import io.github.tcdl.msb.api.MsbContextBuilder; -import io.github.tcdl.msb.api.message.payload.RestPayload; -import io.github.tcdl.msb.examples.payload.Query; -import io.github.tcdl.msb.examples.payload.Request; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Simple example of date parser micro-service - * It listens requests from facets-aggregator and parses year from query string. - */ -public class DateExtractor { - - public static void main(String... args) { - MsbContext msbContext = new MsbContextBuilder() - .enableChannelMonitorAgent(true) - .enableShutdownHook(true) - .build(); - new DateExtractor().start(msbContext); - } - - public void start(MsbContext msbContext) { - MessageTemplate messageTemplate = new MessageTemplate().withTags("date-extractor"); - final String namespace = "search:parsers:facets:v1"; - - msbContext.getObjectFactory().createResponderServer(namespace, messageTemplate, (request, responder) -> { - - Query query = request.getQuery(); - String queryString = query.getQ(); - String year = DateExtractorUtils.retrieveYear(queryString); - - if (year != null) { - // send acknowledge - responder.sendAck(500, null); - - // populate response body - Result result = new Result(); - result.setStr(year); - result.setStartIndex(queryString.indexOf(year)); - result.setEndIndex(queryString.indexOf(year) + year.length() - 1); - result.setInferredDate(new HashMap<>()); - result.setProbability(0.9f); - - Result.Date date = new Result.Date(); - date.setYear(Integer.parseInt(year.substring(2, year.length()))); - result.setDate(date); - - ResponseBody responseBody = new ResponseBody(); - responseBody.setResults(Arrays.asList(result)); - RestPayload responsePayload = new RestPayload.Builder() - .withStatusCode(200) - .withBody(responseBody) - .build(); - - responder.send(responsePayload); - } - }, Request.class).listen(); - } - - private static class RequestQuery { - - private String q; - - public String getQ() { - return q; - } - - public void setQ(String q) { - this.q = q; - } - } - - private static class ResponseBody { - private List results; - - public List getResults() { - return results; - } - - public void setResults(List results) { - this.results = results; - } - } - - private static class Result { - private String str; - private int startIndex; - private int endIndex; - private Date date; - private Map inferredDate; - private float probability; - - public String getStr() { - return str; - } - - public void setStr(String str) { - this.str = str; - } - - public int getStartIndex() { - return startIndex; - } - - public void setStartIndex(int startIndex) { - this.startIndex = startIndex; - } - - public int getEndIndex() { - return endIndex; - } - - public void setEndIndex(int endIndex) { - this.endIndex = endIndex; - } - - public Date getDate() { - return date; - } - - public void setDate(Date date) { - this.date = date; - } - - public Map getInferredDate() { - return inferredDate; - } - - public void setInferredDate(Map inferredDate) { - this.inferredDate = inferredDate; - } - - public float getProbability() { - return probability; - } - - public void setProbability(float probability) { - this.probability = probability; - } - - private static class Date { - private int year; - - public int getYear() { - return year; - } - - public void setYear(int year) { - this.year = year; - } - } - } -} diff --git a/examples/src/main/java/io/github/tcdl/msb/examples/DateExtractorUtils.java b/examples/src/main/java/io/github/tcdl/msb/examples/DateExtractorUtils.java deleted file mode 100644 index 4615e209..00000000 --- a/examples/src/main/java/io/github/tcdl/msb/examples/DateExtractorUtils.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.github.tcdl.msb.examples; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class DateExtractorUtils { - private static final Pattern DATE_PATTERN = - Pattern.compile(".*?(((0?[1-9]|[12][0-9]|3[01])(/|\\.))?((0?[1-9]|1[012])(/|\\.))?((19|20)\\d\\d)).*"); - - public static String retrieveYear(String s) { - Matcher dateMatcher = DATE_PATTERN.matcher(s); - if (dateMatcher.matches()) { - String str = dateMatcher.group(1); - if (str.contains(".")) { - return str.split("\\.")[str.split("\\.").length - 1]; - } else if (str.contains("/")) { - return str.split("/")[str.split("/").length - 1]; - } else { - return str; - } - } - return null; - } -} diff --git a/examples/src/main/java/io/github/tcdl/msb/examples/FacetsAggregator.java b/examples/src/main/java/io/github/tcdl/msb/examples/FacetsAggregator.java index f4911380..39c2da0b 100644 --- a/examples/src/main/java/io/github/tcdl/msb/examples/FacetsAggregator.java +++ b/examples/src/main/java/io/github/tcdl/msb/examples/FacetsAggregator.java @@ -1,15 +1,9 @@ package io.github.tcdl.msb.examples; -import io.github.tcdl.msb.api.MessageTemplate; -import io.github.tcdl.msb.api.MsbContext; -import io.github.tcdl.msb.api.MsbContextBuilder; -import io.github.tcdl.msb.api.RequestOptions; -import io.github.tcdl.msb.api.Requester; -import io.github.tcdl.msb.api.Responder; +import io.github.tcdl.msb.api.*; import io.github.tcdl.msb.api.message.payload.RestPayload; import io.github.tcdl.msb.examples.payload.Request; -import javax.script.ScriptException; import java.io.FileNotFoundException; import java.util.ArrayList; import java.util.Collections; @@ -18,6 +12,8 @@ import java.util.Map; import java.util.UUID; +import javax.script.ScriptException; + /** * Microservice which is listening for incoming messages, creates requests to another microservices( * data-extractor, airport-extractor, resort-extractor), concatenates responses and returns result response @@ -27,16 +23,19 @@ public class FacetsAggregator { public static void main(String[] args) throws ScriptException, FileNotFoundException, NoSuchMethodException { MsbContext msbContext = new MsbContextBuilder() - .enableChannelMonitorAgent(true) .enableShutdownHook(true) .build(); MessageTemplate messageTemplate = new MessageTemplate().withTags("facets-aggregator"); final String namespace = "search:aggregator:facets:v1"; - msbContext.getObjectFactory().createResponderServer(namespace, messageTemplate, (Request facetsRequest, Responder responder) -> { + ResponderOptions responderOptions = new ResponderOptions.Builder().withMessageTemplate(messageTemplate).build(); + + msbContext.getObjectFactory().createResponderServer( + namespace, responderOptions, (Request facetsRequest, ResponderContext responderContext) -> { String q = facetsRequest.getQuery().getQ(); + Responder responder = responderContext.getResponder(); if (q == null) { RestPayload responsePayload = new RestPayload.Builder() @@ -75,7 +74,7 @@ public static void main(String[] args) throws ScriptException, FileNotFoundExcep final String[] result = {""}; List responses = Collections.synchronizedList(new ArrayList<>()); - requester.onResponse(responses::add) + requester.onResponse((message, ackHandler) -> {responses.add(message);}) .onEnd(end -> { for (RestPayload payload : responses) { System.out.println(">>> MESSAGE: " + payload); @@ -90,7 +89,7 @@ public static void main(String[] args) throws ScriptException, FileNotFoundExcep responder.send(responsePayload); }); - requester.publish(facetsRequest, responder.getOriginalMessage(), UUID.randomUUID().toString()); + requester.publish(facetsRequest, responderContext.getOriginalMessage(), UUID.randomUUID().toString()); } }, Request.class).listen(); } diff --git a/examples/src/main/java/io/github/tcdl/msb/examples/Monitor.java b/examples/src/main/java/io/github/tcdl/msb/examples/Monitor.java deleted file mode 100644 index 5378c95a..00000000 --- a/examples/src/main/java/io/github/tcdl/msb/examples/Monitor.java +++ /dev/null @@ -1,29 +0,0 @@ -package io.github.tcdl.msb.examples; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.datatype.jsr310.JSR310Module; -import io.github.tcdl.msb.api.MsbContext; -import io.github.tcdl.msb.api.MsbContextBuilder; -import io.github.tcdl.msb.api.monitor.ChannelMonitorAggregator; -import io.github.tcdl.msb.support.Utils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class Monitor { - private static final Logger LOG = LoggerFactory.getLogger(Monitor.class); - - public static void main(String... args) { - MsbContext msbContext = new MsbContextBuilder() - .enableChannelMonitorAgent(false) - .enableShutdownHook(true) - .build(); - - ObjectMapper aggregatorStatsMapper = new ObjectMapper(); - aggregatorStatsMapper.registerModule(new JSR310Module()); - aggregatorStatsMapper.enable(SerializationFeature.INDENT_OUTPUT); - - ChannelMonitorAggregator channelMonitorAggregator = msbContext.getObjectFactory().createChannelMonitorAggregator(arg -> LOG.info("Received monitoring info " + Utils.toJson(arg, aggregatorStatsMapper))); - channelMonitorAggregator.start(); - } -} diff --git a/examples/src/main/java/io/github/tcdl/msb/examples/PingService.java b/examples/src/main/java/io/github/tcdl/msb/examples/PingService.java index 36e33d14..feaa9ae7 100644 --- a/examples/src/main/java/io/github/tcdl/msb/examples/PingService.java +++ b/examples/src/main/java/io/github/tcdl/msb/examples/PingService.java @@ -6,11 +6,12 @@ import io.github.tcdl.msb.api.ObjectFactory; import io.github.tcdl.msb.api.RequestOptions; import io.github.tcdl.msb.api.Requester; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.util.UUID; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + public class PingService { private static final Logger LOG = LoggerFactory.getLogger(PingService.class); @@ -29,7 +30,7 @@ public static void main(String[] args) { ObjectFactory objectFactory = msbContext.getObjectFactory(); Requester requester = objectFactory.createRequester("pingpong:namespace", requestOptions, String.class) - .onResponse(payload -> LOG.info(String.format("Received response '%s'", payload))) // Handling the one response + .onResponse((payload, ackHandler) -> LOG.info(String.format("Received response '%s'", payload))) // Handling the one response .onEnd(arg -> LOG.info("Received all expected responses")); // Handling all response arrival or timeout requester.publish("PING", UUID.randomUUID().toString()); // Send the message diff --git a/examples/src/main/java/io/github/tcdl/msb/examples/PongService.java b/examples/src/main/java/io/github/tcdl/msb/examples/PongService.java index e0eff238..c1e4b65c 100644 --- a/examples/src/main/java/io/github/tcdl/msb/examples/PongService.java +++ b/examples/src/main/java/io/github/tcdl/msb/examples/PongService.java @@ -1,10 +1,6 @@ package io.github.tcdl.msb.examples; -import io.github.tcdl.msb.api.MessageTemplate; -import io.github.tcdl.msb.api.MsbContext; -import io.github.tcdl.msb.api.MsbContextBuilder; -import io.github.tcdl.msb.api.ObjectFactory; -import io.github.tcdl.msb.api.ResponderServer; +import io.github.tcdl.msb.api.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,11 +14,13 @@ public static void main(String[] args) { ObjectFactory objectFactory = msbContext.getObjectFactory(); MessageTemplate messageTemplate = new MessageTemplate().withTags("pong-static-tag"); - ResponderServer responderServer = objectFactory.createResponderServer("pingpong:namespace", messageTemplate, (request, responder) -> { + ResponderOptions responderOptions = new ResponderOptions.Builder().withMessageTemplate(messageTemplate).build(); + ResponderServer responderServer = objectFactory.createResponderServer("pingpong:namespace", responderOptions, + (request, responderContext) -> { // Response handling logic LOG.info(String.format("Handling %s...", request)); - responder.send("PONG"); + responderContext.getResponder().send("PONG"); LOG.info("Response sent"); }, String.class); diff --git a/examples/src/main/java/io/github/tcdl/msb/examples/ProducerWithRoutingKey.java b/examples/src/main/java/io/github/tcdl/msb/examples/ProducerWithRoutingKey.java new file mode 100644 index 00000000..8efa4fd3 --- /dev/null +++ b/examples/src/main/java/io/github/tcdl/msb/examples/ProducerWithRoutingKey.java @@ -0,0 +1,67 @@ +package io.github.tcdl.msb.examples; + +import io.github.tcdl.msb.api.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Run this producer in conjunction with {@link ConsumerWithRoutingKeys} to see that only messages sent with + * routing keys 'zero' and 'two' are consumed and the messages sent with routing key 'one' are not. + */ +public class ProducerWithRoutingKey { + + private static final Logger LOG = LoggerFactory.getLogger(ProducerWithRoutingKey.class); + + public static void main(String[] args) { + MsbContext msbContext = new MsbContextBuilder(). + enableShutdownHook(true). + build(); + + Map tikToRoutingKey = new HashMap<>(3); + tikToRoutingKey.put(0, "zero"); + tikToRoutingKey.put(1, "one"); + tikToRoutingKey.put(2, "two"); + + MessageTemplate messageTemplate = new MessageTemplate().withTags("publish-with-routing-key"); + + AtomicInteger tik = new AtomicInteger(0); + + Runnable task = () -> { + + String routingKey = tikToRoutingKey.get(tik.getAndIncrement() % 3); + RequestOptions requestOptions = new AmqpRequestOptions.Builder() + .withExchangeType(ExchangeType.TOPIC) + .withRoutingKey(routingKey) + .withMessageTemplate(messageTemplate) + .build(); + + LOG.info("Sending message with routing key '{}'", routingKey); + msbContext.getObjectFactory() + .createRequester("routing:namespace", requestOptions, String.class) + .publish(routingKey.toUpperCase(), UUID.randomUUID().toString()); + }; + + runEachNSeconds(2, task); + } + + private static void runEachNSeconds(int secondsNumber, Runnable task) { + ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + executor.shutdownNow(); + } + }); + + executor.scheduleAtFixedRate(task, 0, secondsNumber, TimeUnit.SECONDS); + } +} diff --git a/examples/src/main/resources/application.conf b/examples/src/main/resources/application.conf index 52825bd1..8cb3232f 100644 --- a/examples/src/main/resources/application.conf +++ b/examples/src/main/resources/application.conf @@ -7,25 +7,9 @@ msbConfig { instanceId = "msbd06a-ed59-4a39-9f95-811c5fb6ab87" } - brokerAdapterFactory = "io.github.tcdl.msb.adapters.amqp.AmqpAdapterFactory" - - # Thread pool used for scheduling ack and response timeout tasks - timerThreadPoolSize: 2 - - # Enable/disable message validation against json schema - validateMessage = true - - # Broker Adapter Defaults - brokerConfig = { - host = "127.0.0.1" - port = "5672" - groupId = "msb-java" - durable = false - consumerThreadPoolSize = 5 - # -1 means unlimited - consumerThreadPoolQueueCapacity = 20 - requeueRejectedMessages = true - } - + # Broker Adapter Defaults + brokerConfig = { + durable = true + } } diff --git a/examples/src/main/resources/log4j.xml b/examples/src/main/resources/log4j.xml deleted file mode 100644 index 0361fe73..00000000 --- a/examples/src/main/resources/log4j.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/examples/src/main/resources/logback.xml b/examples/src/main/resources/logback.xml new file mode 100644 index 00000000..200166fb --- /dev/null +++ b/examples/src/main/resources/logback.xml @@ -0,0 +1,15 @@ + + + + + UTF-8 + %d{HH:mm:ss.SSS} %-5level %logger [%t] - %m%n + + + + + + + + + \ No newline at end of file diff --git a/examples/src/test/java/io/github/tcdl/msb/examples/DateExtractorUtilsTest.java b/examples/src/test/java/io/github/tcdl/msb/examples/DateExtractorUtilsTest.java deleted file mode 100644 index d25511ad..00000000 --- a/examples/src/test/java/io/github/tcdl/msb/examples/DateExtractorUtilsTest.java +++ /dev/null @@ -1,78 +0,0 @@ -package io.github.tcdl.msb.examples; - -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -public class DateExtractorUtilsTest { - - @Test - public void testOnlyYear() { - assertEquals("2015", DateExtractorUtils.retrieveYear("2015")); - } - - @Test - public void testOnlyYearSpaces() { - assertEquals("2015", DateExtractorUtils.retrieveYear(" 2015 ")); - } - - @Test - public void testMonthYear() { - assertEquals("2015", DateExtractorUtils.retrieveYear("02/2015")); - } - - @Test - public void testMonthYearDot() { - assertEquals("2015", DateExtractorUtils.retrieveYear("02.2015")); - } - - @Test - public void testDayMonthYear() { - assertEquals("2015", DateExtractorUtils.retrieveYear("12/03/2015")); - } - - @Test - public void testDayMonthYearDot() { - assertEquals("2015", DateExtractorUtils.retrieveYear("12.03.2015")); - } - - @Test - public void testDateTextStart() { - assertEquals("2015", DateExtractorUtils.retrieveYear("12/03/2015 London holidays")); - } - - @Test - public void testDateTextStartEnd() { - assertEquals("2015", DateExtractorUtils.retrieveYear("Hotels 12/03/2015 London holidays")); - } - - @Test - public void testDateTextStartEndDot() { - assertEquals("2015", DateExtractorUtils.retrieveYear("Hotels 12.03.2015 London holidays")); - } - - @Test - public void testDateSpaceAtTheBeginning() { - assertEquals("2015", DateExtractorUtils.retrieveYear(" 12/03/2015 London holidays")); - } - - @Test - public void testDateSpaceAtEnd() { - assertEquals("2015", DateExtractorUtils.retrieveYear("12/03/2015 London holidays ")); - } - - @Test - public void testDateSpaceAtTheBeginningEnd() { - assertEquals("2015", DateExtractorUtils.retrieveYear(" 12/03/2015 London holidays ")); - } - - @Test - public void testDateYear() { - assertEquals("2015", DateExtractorUtils.retrieveYear("London 03.03.2015-counter-2078")); - } - - @Test - public void testTwoYears() { - assertEquals("2015", DateExtractorUtils.retrieveYear("London 2015-counter-2078")); - } -} \ No newline at end of file diff --git a/jmeter/pom.xml b/jmeter/pom.xml new file mode 100644 index 00000000..75228d70 --- /dev/null +++ b/jmeter/pom.xml @@ -0,0 +1,78 @@ + + + + 4.0.0 + + + io.github.tcdl.msb + msb-java + 1.6.7-SNAPSHOT + ../pom.xml + + + ApacheJmeter_msb + jar + + + scm:git:https://github.com/tcdl/msb-java.git + scm:git:git@github.com:tcdl/msb-java.git + https://github.com/tcdl/msb-java + HEAD + + + + tcdl + https://github.com/tcdl + + + + UTF-8 + UTF-8 + 3.0 + + + + + org.apache.jmeter + ApacheJMeter_core + ${apache.jmeter.version} + provided + + + + io.github.tcdl.msb + msb-java-core + + + io.github.tcdl.msb + msb-java-amqp + + + + + install + + + maven-jar-plugin + + + org.apache.maven.plugins + maven-shade-plugin + 2.4.3 + + + package + + shade + + + ${project.artifactId}-${project.version} + false + + + + + + + + \ No newline at end of file diff --git a/jmeter/src/main/java/io/github/tcdl/msb/jmeter/sampler/MsbRequesterSampler.java b/jmeter/src/main/java/io/github/tcdl/msb/jmeter/sampler/MsbRequesterSampler.java new file mode 100644 index 00000000..9237c5f4 --- /dev/null +++ b/jmeter/src/main/java/io/github/tcdl/msb/jmeter/sampler/MsbRequesterSampler.java @@ -0,0 +1,147 @@ +package io.github.tcdl.msb.jmeter.sampler; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import com.typesafe.config.ConfigValueFactory; +import io.github.tcdl.msb.api.MessageTemplate; +import io.github.tcdl.msb.api.MsbContext; +import io.github.tcdl.msb.api.MsbContextBuilder; +import io.github.tcdl.msb.api.RequestOptions; +import io.github.tcdl.msb.support.Utils; +import org.apache.commons.lang3.StringUtils; +import org.apache.jmeter.samplers.AbstractSampler; +import org.apache.jmeter.samplers.Entry; +import org.apache.jmeter.samplers.SampleResult; +import org.apache.jorphan.logging.LoggingManager; +import org.apache.log.Logger; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +public class MsbRequesterSampler extends AbstractSampler { + + private static final Logger LOG = LoggingManager.getLoggerForClass(); + + private static AtomicInteger classCount = new AtomicInteger(0); + + private String MSB_BROKER_CONFIG_ROOT = "msbConfig.brokerConfig"; + + private RequesterConfig currentRequesterConfig; + private MsbContext msbContext; + + private ObjectMapper objectMapper = new ObjectMapper() + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .setSerializationInclusion(JsonInclude.Include.NON_NULL); + + public MsbRequesterSampler() { + classCount.incrementAndGet(); + trace("MsbRequesterSampler()"); + } + + public void init() { + // do nothing + } + + private void initMsb(RequesterConfig requesterConfig) { + Config msbConfig = ConfigFactory.load() + .withValue(MSB_BROKER_CONFIG_ROOT + ".host", ConfigValueFactory.fromAnyRef(requesterConfig.getHost())) + .withValue(MSB_BROKER_CONFIG_ROOT + ".port", ConfigValueFactory.fromAnyRef(requesterConfig.getPort())) + .withValue(MSB_BROKER_CONFIG_ROOT + ".virtualHost", ConfigValueFactory.fromAnyRef(requesterConfig.getVirtualHost())) + .withValue(MSB_BROKER_CONFIG_ROOT + ".username", ConfigValueFactory.fromAnyRef(requesterConfig.getUserName())) + .withValue(MSB_BROKER_CONFIG_ROOT + ".password", ConfigValueFactory.fromAnyRef(requesterConfig.getPassword())); + + msbContext = new MsbContextBuilder() + .withConfig(msbConfig) + .enableShutdownHook(true) + .build(); + } + + public SampleResult sample(Entry e) { + trace("sample()"); + + RequesterConfig requesterConfig = (RequesterConfig)getProperty(RequesterConfig.TEST_ELEMENT_CONFIG).getObjectValue(); + if (msbContext == null || !requesterConfig.equals(currentRequesterConfig)) { + currentRequesterConfig = requesterConfig; + initMsb(requesterConfig); + } + + String namespace = requesterConfig.getNamespace(); + RequestOptions requestOptions = new RequestOptions.Builder() + .withMessageTemplate(new MessageTemplate()) + .withResponseTimeout(requesterConfig.getTimeout()) + .withForwardNamespace(requesterConfig.getForwardNamespace()) + .withWaitForResponses(requesterConfig.getWaitForResponses() ? requesterConfig.getNumberOfResponses() : 0) + .build(); + + String payloadJson = StringUtils.isNotEmpty(requesterConfig.getRequestPayload()) ? requesterConfig.getRequestPayload() : "{}"; + JsonNode payload = Utils.fromJson(payloadJson, JsonNode.class, objectMapper); + + final CountDownLatch waitForResponse = new CountDownLatch(requesterConfig.getNumberOfResponses()); + final ArrayNode responses = objectMapper.createArrayNode(); + + SampleResult res = new SampleResult(); + res.setSampleLabel(this.getName()); + + res.sampleStart(); + + msbContext + .getObjectFactory() + .createRequester(namespace, requestOptions, JsonNode.class) + .onResponse((response, messageContext) -> { + waitForResponse.countDown(); + responses.add(response); + }) + .publish(payload); + + if (requesterConfig.getWaitForResponses()) { + try { + waitForResponse.await(2 * requesterConfig.getTimeout(), TimeUnit.MILLISECONDS); + } catch (InterruptedException ie) { + res.setResponseMessage(ie.getMessage()); + } + } + + res.sampleEnd(); + + int numberOfReceivedResponses = requesterConfig.getNumberOfResponses() - (int) waitForResponse.getCount(); + trace("Received " + numberOfReceivedResponses + " responses from " + requesterConfig.getNumberOfResponses()); + boolean isResultOk = !requesterConfig.getWaitForResponses() || waitForResponse.getCount() == 0; + + if (isResultOk) { + String responsesAsJson = responses.toString(); + trace("Responses: " + responsesAsJson); + + res.setSuccessful(true); + res.setResponseCodeOK(); + res.setResponseMessageOK(); + + res.setDataType(SampleResult.TEXT); + res.setResponseData(responsesAsJson, null); + } else { + res.setSuccessful(false); + res.setResponseCode("500"); + res.setResponseMessage("No response(s)"); + } + + return res; + } + + private void trace(String message) { + LOG.info(Thread.currentThread().getName() + " (" + classCount.get() + ") " + this.getName() + " " + message); + } + + @Override + protected void finalize() throws Throwable { + if (msbContext != null) { + msbContext.shutdown(); + } + } +} \ No newline at end of file diff --git a/jmeter/src/main/java/io/github/tcdl/msb/jmeter/sampler/RequesterConfig.java b/jmeter/src/main/java/io/github/tcdl/msb/jmeter/sampler/RequesterConfig.java new file mode 100644 index 00000000..b73edd10 --- /dev/null +++ b/jmeter/src/main/java/io/github/tcdl/msb/jmeter/sampler/RequesterConfig.java @@ -0,0 +1,154 @@ +package io.github.tcdl.msb.jmeter.sampler; + +import java.util.Objects; + +import static io.github.tcdl.msb.support.Utils.ifNull; +import static org.apache.commons.lang3.StringUtils.defaultIfBlank; + +/** + * Created by rdro-tc on 28.07.16. + */ +public class RequesterConfig { + + public final static String TEST_ELEMENT_CONFIG = "TestElement.msb_requester"; + + private String host; + private Integer port; + private String virtualHost; + private String userName; + private String password; + private String namespace; + private String forwardNamespace; + private Boolean waitForResponses; + private Integer numberOfResponses; + private Integer timeout; + private String requestPayload; + + public RequesterConfig() { + setDefaults(); + } + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } + + public String getVirtualHost() { + return virtualHost; + } + + public void setVirtualHost(String virtualHost) { + this.virtualHost = virtualHost; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + public String getForwardNamespace() { + return forwardNamespace; + } + + public void setForwardNamespace(String forwardNamespace) { + this.forwardNamespace = forwardNamespace; + } + + public Boolean getWaitForResponses() { + return waitForResponses; + } + + public void setWaitForResponses(Boolean waitForResponses) { + this.waitForResponses = waitForResponses; + } + + public Integer getNumberOfResponses() { + return numberOfResponses; + } + + public void setNumberOfResponses(Integer numberOfResponses) { + this.numberOfResponses = numberOfResponses; + } + + public Integer getTimeout() { + return timeout; + } + + public void setTimeout(Integer timeout) { + this.timeout = timeout; + } + + public String getRequestPayload() { + return requestPayload; + } + + public void setRequestPayload(String requestPayload) { + this.requestPayload = requestPayload; + } + + public void setDefaults() { + this.host = defaultIfBlank(this.host, "localhost"); + this.port = this.port == null || this.port <= 0 ? 5672 : this.port; + this.virtualHost = defaultIfBlank(this.virtualHost, "/"); + this.userName = defaultIfBlank(this.userName, "guest"); + this.password = defaultIfBlank(this.password, "guest"); + this.namespace = defaultIfBlank(this.namespace, "jmeter:test"); + this.waitForResponses = ifNull(this.waitForResponses, true); + this.numberOfResponses = ifNull(this.numberOfResponses, 1); + this.timeout = this.timeout == null || this.timeout <= 0 ? 3000 : this.timeout; + this.requestPayload = defaultIfBlank(this.requestPayload, "{}"); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RequesterConfig that = (RequesterConfig) o; + return Objects.equals(host, that.host) && + Objects.equals(port, that.port) && + Objects.equals(virtualHost, that.virtualHost) && + Objects.equals(userName, that.userName) && + Objects.equals(password, that.password) && + Objects.equals(namespace, that.namespace) && + Objects.equals(forwardNamespace, that.forwardNamespace) && + Objects.equals(waitForResponses, that.waitForResponses) && + Objects.equals(numberOfResponses, that.numberOfResponses) && + Objects.equals(timeout, that.timeout) && + Objects.equals(requestPayload, that.requestPayload); + } + + @Override + public int hashCode() { + return Objects.hash(host, port, virtualHost, userName, password, namespace, forwardNamespace, waitForResponses, numberOfResponses, timeout, requestPayload); + } +} diff --git a/jmeter/src/main/java/io/github/tcdl/msb/jmeter/sampler/gui/MsbRequesterSamplerGui.java b/jmeter/src/main/java/io/github/tcdl/msb/jmeter/sampler/gui/MsbRequesterSamplerGui.java new file mode 100644 index 00000000..f98f1810 --- /dev/null +++ b/jmeter/src/main/java/io/github/tcdl/msb/jmeter/sampler/gui/MsbRequesterSamplerGui.java @@ -0,0 +1,73 @@ +package io.github.tcdl.msb.jmeter.sampler.gui; + +import io.github.tcdl.msb.jmeter.sampler.MsbRequesterSampler; +import io.github.tcdl.msb.jmeter.sampler.RequesterConfig; +import org.apache.jmeter.samplers.gui.AbstractSamplerGui; +import org.apache.jmeter.testelement.TestElement; +import org.apache.jmeter.testelement.property.ObjectProperty; + +import java.awt.*; + +public class MsbRequesterSamplerGui extends AbstractSamplerGui { + + private RequesterConfigForm configForm; + + public MsbRequesterSamplerGui() { + init(); + } + + public String getName() { + return "MSB Requester Sampler"; + } + + public String getLabelResource() { + return null; + } + + public String getStaticLabel() { + return "MSB Requester Sampler"; + } + + public void configure(TestElement testElement) { + super.configure(testElement); + + MsbRequesterSampler sampler = (MsbRequesterSampler) testElement; + RequesterConfig config = (RequesterConfig)sampler.getProperty(RequesterConfig.TEST_ELEMENT_CONFIG).getObjectValue(); + + if (config == null) { + sampler.setProperty(new ObjectProperty(RequesterConfig.TEST_ELEMENT_CONFIG, configForm.getConfig())); + } else { + configForm.setConfig(config); + } + + sampler.init(); + } + + public TestElement createTestElement() { + MsbRequesterSampler sampler = new MsbRequesterSampler(); + modifyTestElement(sampler); + return sampler; + } + + public void modifyTestElement(TestElement testElement) { + configureTestElement(testElement); + MsbRequesterSampler sampler = (MsbRequesterSampler) testElement; + sampler.setProperty(new ObjectProperty(RequesterConfig.TEST_ELEMENT_CONFIG, configForm.getConfig())); + } + + private void init() { + setLayout(new BorderLayout(0, 0)); + setBorder(makeBorder()); + add(makeTitlePanel(), BorderLayout.NORTH); + + RequesterConfig config = new RequesterConfig(); + configForm = new RequesterConfigForm(config); + add(configForm.getUIComponent(), BorderLayout.WEST); + } + + @Override + public void clearGui() { + configForm.setConfig(new RequesterConfig()); + super.clearGui(); + } +} \ No newline at end of file diff --git a/jmeter/src/main/java/io/github/tcdl/msb/jmeter/sampler/gui/RequesterConfigForm.java b/jmeter/src/main/java/io/github/tcdl/msb/jmeter/sampler/gui/RequesterConfigForm.java new file mode 100644 index 00000000..c397a4c4 --- /dev/null +++ b/jmeter/src/main/java/io/github/tcdl/msb/jmeter/sampler/gui/RequesterConfigForm.java @@ -0,0 +1,213 @@ +package io.github.tcdl.msb.jmeter.sampler.gui; + +import io.github.tcdl.msb.jmeter.sampler.RequesterConfig; +import io.github.tcdl.msb.jmeter.sampler.gui.validation.IntegerVerifier; +import io.github.tcdl.msb.jmeter.sampler.gui.validation.JsonVerifier; +import io.github.tcdl.msb.jmeter.sampler.gui.validation.NotBlankVerifier; +import io.github.tcdl.msb.jmeter.sampler.gui.validation.PatternVerifier; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; + +/** + * Created by rdro-tc on 28.07.16. + */ +public class RequesterConfigForm { + + private JPanel configPanel; + private JPanel brokerConfigPanel; + private JPanel requesterConfigPanel; + private JScrollPane requestPayloadPanel; + + private JLabel hostLabel; + private JLabel portLabel; + private JLabel virtualHostLabel; + private JLabel userNameLabel; + private JLabel passwordLabel; + private JTextField hostField; + private JTextField portField; + private JTextField virtualHostField; + private JTextField userNameField; + private JTextField passwordField; + + private JLabel namespaceLabel; + private JLabel forwardNamespaceLabel; + private JLabel numberOfResponsesLabel; + private JLabel timeoutLabel; + private JTextField namespaceField; + private JTextField forwardNamespaceField; + private JTextField numberOfResponsesField; + private JTextField timeoutField; + private JCheckBox waitForResponsesCheckBox; + + private JTextArea requestPayloadField; + + public RequesterConfigForm(RequesterConfig config) { + setupUI(); + setConfig(config); + waitForResponsesCheckBox.addItemListener(new ItemListener() { + public void itemStateChanged(ItemEvent e) { + onResponseEnabled(e.getStateChange() == ItemEvent.SELECTED); + } + }); + + hostField.setInputVerifier(new NotBlankVerifier()); + portField.setInputVerifier(new IntegerVerifier(true)); + virtualHostField.setInputVerifier(new NotBlankVerifier()); + userNameField.setInputVerifier(new NotBlankVerifier()); + passwordField.setInputVerifier(new NotBlankVerifier()); + + namespaceField.setInputVerifier(new PatternVerifier(true, "^_?([a-z0-9\\-]+\\:)+([a-z0-9\\-]+)$")); + forwardNamespaceField.setInputVerifier(new PatternVerifier(false, "^_?([a-z0-9\\-]+\\:)+([a-z0-9\\-]+)$")); + numberOfResponsesField.setInputVerifier(new IntegerVerifier(true)); + timeoutField.setInputVerifier(new IntegerVerifier(true)); + requestPayloadField.setInputVerifier(new JsonVerifier(true)); + } + + public JComponent getUIComponent() { + return configPanel; + } + + private void onResponseEnabled(boolean enabled) { + numberOfResponsesField.setEnabled(enabled); + timeoutField.setEnabled(enabled); + } + + public void setConfig(RequesterConfig config) { + hostField.setText(config.getHost()); + portField.setText(config.getPort() != null ? config.getPort().toString() : ""); + virtualHostField.setText(config.getVirtualHost()); + userNameField.setText(config.getUserName()); + passwordField.setText(config.getPassword()); + + namespaceField.setText(config.getNamespace()); + forwardNamespaceField.setText(config.getForwardNamespace()); + numberOfResponsesField.setText(config.getNumberOfResponses() != null ? config.getNumberOfResponses().toString() : ""); + timeoutField.setText(config.getTimeout() != null ? config.getTimeout().toString() : ""); + requestPayloadField.setText(config.getRequestPayload()); + waitForResponsesCheckBox.setSelected(config.getWaitForResponses()); + } + + public RequesterConfig getConfig() { + RequesterConfig config = new RequesterConfig(); + + config.setHost(hostField.getText()); + config.setPort(Integer.valueOf(portField.getText())); + config.setVirtualHost(virtualHostField.getText()); + config.setUserName(userNameField.getText()); + config.setPassword(passwordField.getText()); + + config.setNamespace(namespaceField.getText()); + config.setForwardNamespace(forwardNamespaceField.getText()); + config.setTimeout(Integer.valueOf(timeoutField.getText())); + config.setRequestPayload(requestPayloadField.getText()); + config.setNumberOfResponses(Integer.valueOf(numberOfResponsesField.getText())); + config.setWaitForResponses(waitForResponsesCheckBox.isSelected()); + + return config; + } + + private void setupUI() { + configPanel = new JPanel(); + configPanel.setLayout(new GridBagLayout()); + + brokerConfigPanel = new JPanel(); + brokerConfigPanel.setLayout(new GridBagLayout()); + brokerConfigPanel.setBorder(BorderFactory.createTitledBorder("Broker configuration")); + + hostLabel = new JLabel(); + hostLabel.setText("host"); + brokerConfigPanel.add(hostLabel, constraints(0, 0)); + hostField = new JTextField(); + hostField.setName("host"); + brokerConfigPanel.add(hostField, constraints(1, 0)); + + portLabel = new JLabel(); + portLabel.setText("port"); + brokerConfigPanel.add(portLabel, constraints(0, 1)); + portField = new JTextField(); + portField.setName("port"); + brokerConfigPanel.add(portField, constraints(1, 1)); + + virtualHostLabel = new JLabel(); + virtualHostLabel.setText("virtual host"); + brokerConfigPanel.add(virtualHostLabel, constraints(0, 2)); + virtualHostField = new JTextField(); + virtualHostField.setName("virtual host"); + brokerConfigPanel.add(virtualHostField, constraints(1, 2)); + + userNameLabel = new JLabel(); + userNameLabel.setText("user name"); + brokerConfigPanel.add(userNameLabel, constraints(0, 3)); + userNameField = new JTextField(); + userNameField.setName("user name"); + brokerConfigPanel.add(userNameField, constraints(1, 3)); + + passwordLabel = new JLabel(); + passwordLabel.setText("password"); + passwordLabel.setName("password"); + brokerConfigPanel.add(passwordLabel, constraints(0, 4)); + passwordField = new JTextField(); + passwordField.setName("password"); + brokerConfigPanel.add(passwordField, constraints(1, 4)); + configPanel.add(brokerConfigPanel, constraints(0, 0)); + + requesterConfigPanel = new JPanel(); + requesterConfigPanel.setLayout(new GridBagLayout()); + requesterConfigPanel.setBorder(BorderFactory.createTitledBorder("Requester configuration")); + + namespaceLabel = new JLabel("namespace"); + requesterConfigPanel.add(namespaceLabel, constraints(0, 0)); + namespaceField = new JTextField(); + namespaceField.setName("namespace"); + requesterConfigPanel.add(namespaceField, constraints(1, 0)); + + forwardNamespaceLabel = new JLabel("forward namespace"); + requesterConfigPanel.add(forwardNamespaceLabel, constraints(0, 1)); + forwardNamespaceField = new JTextField(); + forwardNamespaceField.setName("forward namespace"); + requesterConfigPanel.add(forwardNamespaceField, constraints(1, 1)); + + numberOfResponsesLabel = new JLabel(); + numberOfResponsesLabel.setText("number of responses"); + requesterConfigPanel.add(numberOfResponsesLabel, constraints(0, 2)); + numberOfResponsesField = new JTextField(); + numberOfResponsesField.setName("number of responses"); + requesterConfigPanel.add(numberOfResponsesField, constraints(1, 2)); + + timeoutLabel = new JLabel(); + timeoutLabel.setText("timeout, ms"); + requesterConfigPanel.add(timeoutLabel, constraints(0, 3)); + timeoutField = new JTextField(); + timeoutField.setName("timeout"); + requesterConfigPanel.add(timeoutField, constraints(1, 3)); + + waitForResponsesCheckBox = new JCheckBox(); + waitForResponsesCheckBox.setEnabled(true); + waitForResponsesCheckBox.setSelected(true); + waitForResponsesCheckBox.setText("wait for responses"); + requesterConfigPanel.add(waitForResponsesCheckBox, constraints(0, 4)); + configPanel.add(requesterConfigPanel, constraints(0, 1)); + + requestPayloadPanel = new JScrollPane(); + configPanel.add(requestPayloadPanel, constraints(0, 2)); + requestPayloadPanel.setBorder(BorderFactory.createTitledBorder("Request payload")); + requestPayloadPanel.setPreferredSize(new Dimension(500, 300)); + requestPayloadField = new JTextArea(); + requestPayloadField.setName("request payload"); + requestPayloadPanel.setViewportView(requestPayloadField); + } + + private GridBagConstraints constraints(int gridx, int gridy) { + GridBagConstraints c = new GridBagConstraints(); + c.fill = GridBagConstraints.HORIZONTAL; + c.anchor = GridBagConstraints.LINE_START; + c.gridx = gridx; + c.gridy = gridy; + c.weightx = 1; + c.weighty = 1; + return c; + } +} diff --git a/jmeter/src/main/java/io/github/tcdl/msb/jmeter/sampler/gui/validation/IntegerVerifier.java b/jmeter/src/main/java/io/github/tcdl/msb/jmeter/sampler/gui/validation/IntegerVerifier.java new file mode 100644 index 00000000..46026bc6 --- /dev/null +++ b/jmeter/src/main/java/io/github/tcdl/msb/jmeter/sampler/gui/validation/IntegerVerifier.java @@ -0,0 +1,39 @@ +package io.github.tcdl.msb.jmeter.sampler.gui.validation; + +import org.apache.commons.lang3.StringUtils; + +import javax.swing.*; + +/** + * Created by rdro-tc on 28.07.16. + */ +public class IntegerVerifier extends InputVerifier { + + private boolean required = false; + + public IntegerVerifier(boolean required) { + this.required = required; + } + + public boolean verify(JComponent input) { + JTextField textField = (JTextField) input; + + if (required && StringUtils.isBlank(textField.getText())) { + JOptionPane.showMessageDialog(input, + "Required field: " + textField.getName(), "Validation Error", + JOptionPane.ERROR_MESSAGE); + return false; + } + + try { + Integer.valueOf(textField.getText()); + } catch (NumberFormatException e) { + JOptionPane.showMessageDialog(input, + "Invalid number: " + textField.getText(), "Validation Error", + JOptionPane.ERROR_MESSAGE); + return false; + } + + return true; + } +} diff --git a/jmeter/src/main/java/io/github/tcdl/msb/jmeter/sampler/gui/validation/JsonVerifier.java b/jmeter/src/main/java/io/github/tcdl/msb/jmeter/sampler/gui/validation/JsonVerifier.java new file mode 100644 index 00000000..609fccb2 --- /dev/null +++ b/jmeter/src/main/java/io/github/tcdl/msb/jmeter/sampler/gui/validation/JsonVerifier.java @@ -0,0 +1,51 @@ +package io.github.tcdl.msb.jmeter.sampler.gui.validation; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import io.github.tcdl.msb.api.exception.JsonConversionException; +import io.github.tcdl.msb.support.Utils; +import org.apache.commons.lang3.StringUtils; + +import javax.swing.*; +import java.util.Map; + +/** + * Created by rdro-tc on 02.08.16. + */ +public class JsonVerifier extends InputVerifier { + + private ObjectMapper objectMapper = new ObjectMapper() + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .setSerializationInclusion(JsonInclude.Include.NON_NULL); + + private boolean required = false; + + public JsonVerifier(boolean required) { + this.required = required; + } + + public boolean verify(JComponent input) { + JTextArea textField = (JTextArea) input; + + if (required && StringUtils.isBlank(textField.getText())) { + JOptionPane.showMessageDialog(input, + "Required field: " + textField.getName(), "Validation Error", + JOptionPane.ERROR_MESSAGE); + return false; + } + + try { + Utils.fromJson(textField.getText(), Map.class, objectMapper); + } catch (JsonConversionException e) { + JOptionPane.showMessageDialog(input, + "Invalid json: " + textField.getText(), "Validation Error", + JOptionPane.ERROR_MESSAGE); + return false; + } + + return true; + } +} diff --git a/jmeter/src/main/java/io/github/tcdl/msb/jmeter/sampler/gui/validation/NotBlankVerifier.java b/jmeter/src/main/java/io/github/tcdl/msb/jmeter/sampler/gui/validation/NotBlankVerifier.java new file mode 100644 index 00000000..ade6d233 --- /dev/null +++ b/jmeter/src/main/java/io/github/tcdl/msb/jmeter/sampler/gui/validation/NotBlankVerifier.java @@ -0,0 +1,25 @@ +package io.github.tcdl.msb.jmeter.sampler.gui.validation; + +import org.apache.commons.lang3.StringUtils; + +import javax.swing.*; + +/** + * Created by rdro-tc on 02.08.16. + */ +public class NotBlankVerifier extends InputVerifier { + + public boolean verify(JComponent input) { + JTextField textField = (JTextField) input; + + if (StringUtils.isBlank(textField.getText())) { + JOptionPane.showMessageDialog(input, + "Required field: " + textField.getName(), "Validation Error", + JOptionPane.ERROR_MESSAGE); + + return false; + } + + return true; + } +} diff --git a/jmeter/src/main/java/io/github/tcdl/msb/jmeter/sampler/gui/validation/PatternVerifier.java b/jmeter/src/main/java/io/github/tcdl/msb/jmeter/sampler/gui/validation/PatternVerifier.java new file mode 100644 index 00000000..a7f87b17 --- /dev/null +++ b/jmeter/src/main/java/io/github/tcdl/msb/jmeter/sampler/gui/validation/PatternVerifier.java @@ -0,0 +1,42 @@ +package io.github.tcdl.msb.jmeter.sampler.gui.validation; + +import org.apache.commons.lang3.StringUtils; + +import javax.swing.*; +import java.util.regex.Pattern; + +/** + * Created by rdro-tc on 02.08.16. + */ +public class PatternVerifier extends IntegerVerifier { + + private boolean required = false; + private Pattern pattern; + + public PatternVerifier(boolean required, String pattern) { + super(required); + this.pattern = Pattern.compile(pattern); + } + + public boolean verify(JComponent input) { + JTextField textField = (JTextField) input; + + if (required && StringUtils.isBlank(textField.getText())) { + JOptionPane.showMessageDialog(input, + "Required field: " + textField.getName(), "Validation Error", + JOptionPane.ERROR_MESSAGE); + + return false; + } + + if (StringUtils.isNotBlank(textField.getText()) && !pattern.matcher(textField.getText()).matches()) { + JOptionPane.showMessageDialog(input, + "Invalid " + textField.getName(), "Validation Error", + JOptionPane.ERROR_MESSAGE); + + return false; + } + + return true; + } +} diff --git a/jmeter/src/main/resources/application.conf b/jmeter/src/main/resources/application.conf new file mode 100644 index 00000000..d6d15d37 --- /dev/null +++ b/jmeter/src/main/resources/application.conf @@ -0,0 +1,20 @@ +msbConfig { + + serviceDetails = { + name = "jmeter" + version = "1.0.0" + } + + brokerAdapterFactory = "io.github.tcdl.msb.adapters.amqp.AmqpAdapterFactory" + + threadingConfig = { + consumerThreadPoolSize = 2 + } + + validateMessage = false + + brokerConfig = { + durable = false + } + +} \ No newline at end of file diff --git a/jmeter/src/main/resources/logback.xml b/jmeter/src/main/resources/logback.xml new file mode 100644 index 00000000..bff65f54 --- /dev/null +++ b/jmeter/src/main/resources/logback.xml @@ -0,0 +1,27 @@ + + + + true + + UTF-8 + %yellow(%d{HH:mm:ss.SSS}) %highlight(%-5level) %blue(%c{1}) %blue([%t]) %green(tags:%X{msbTags}) - %m%n + + + + + logs/micro-services.log + + UTF-8 + %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %c{1} [%t] tags:%X{msbTags} - %m%n + + + + + + + + + + + + \ No newline at end of file diff --git a/jmeter/src/test/java/io/github/tcdl/msb/jmeter/sampler/UiTest.java b/jmeter/src/test/java/io/github/tcdl/msb/jmeter/sampler/UiTest.java new file mode 100644 index 00000000..5f582a43 --- /dev/null +++ b/jmeter/src/test/java/io/github/tcdl/msb/jmeter/sampler/UiTest.java @@ -0,0 +1,18 @@ +package io.github.tcdl.msb.jmeter.sampler; + +import io.github.tcdl.msb.jmeter.sampler.gui.RequesterConfigForm; + +import javax.swing.*; + +/** + * Created by rdro-tc on 28.07.16. + */ +public class UiTest { + + public static void main(String[] args) { + JFrame frame = new JFrame("Test"); + frame.setContentPane(new RequesterConfigForm(new RequesterConfig()).getUIComponent()); + frame.pack(); + frame.setVisible(true); + } +} diff --git a/jmeter/src/test/resources/examples/MSB Requester Sampler.jmx b/jmeter/src/test/resources/examples/MSB Requester Sampler.jmx new file mode 100644 index 00000000..ee9a681c --- /dev/null +++ b/jmeter/src/test/resources/examples/MSB Requester Sampler.jmx @@ -0,0 +1,90 @@ + + + + + + false + false + + + + + + + + continue + + false + 1 + + 1 + 1 + 1470142254000 + 1470142254000 + false + + + + + + true + 100 + + + + + TestElement.msb_requester + + localhost + 5672 + / + guest + guest + jmeter:test + true + 1 + 3000 + {} + + + + + + + + false + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + false + true + false + false + false + true + 0 + true + true + true + + + + + + + + diff --git a/jmeter/SearchForYear_100Users.jmx b/jmeter/src/test/resources/examples/SearchForYear_100Users.jmx similarity index 100% rename from jmeter/SearchForYear_100Users.jmx rename to jmeter/src/test/resources/examples/SearchForYear_100Users.jmx diff --git a/jmeter/SearchForYear_10Users.jmx b/jmeter/src/test/resources/examples/SearchForYear_10Users.jmx similarity index 100% rename from jmeter/SearchForYear_10Users.jmx rename to jmeter/src/test/resources/examples/SearchForYear_10Users.jmx diff --git a/pom.xml b/pom.xml index 671b4d47..d4c6d7ae 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 io.github.tcdl.msb msb-java - 1.3.0-SNAPSHOT + 1.6.7-SNAPSHOT msb java msb java pom @@ -30,10 +30,13 @@ core + activemq amqp cli acceptance + jmeter examples + spring-boot-starter @@ -48,6 +51,12 @@ msb-java-core ${project.version} + + io.github.tcdl.msb + msb-java-core + ${project.version} + test-jar + io.github.tcdl.msb msb-java-amqp @@ -68,6 +77,11 @@ commons-io 2.4 + + commons-collections + commons-collections + 3.2.1 + com.github.fge json-schema-validator @@ -76,17 +90,17 @@ com.typesafe config - 1.2.1 + 1.3.0 org.slf4j slf4j-api - 1.7.10 + 1.7.13 - org.slf4j - slf4j-log4j12 - 1.7.10 + ch.qos.logback + logback-classic + 1.1.3 junit @@ -94,14 +108,14 @@ 4.12 - org.jbehave - jbehave-core - 4.0.1 + org.assertj + assertj-core + 3.3.0 com.fasterxml.jackson.datatype jackson-datatype-jsr310 - 2.4.0 + 2.7.0 org.mockito @@ -111,12 +125,22 @@ com.rabbitmq amqp-client - 3.5.1 + 5.4.0 + + + org.apache.activemq + activemq-client + 5.15.9 + + + org.apache.activemq + activemq-pool + 5.15.9 com.googlecode.junit-toolbox junit-toolbox - 2.1 + 2.2 @@ -137,14 +161,19 @@ junit-toolbox test + + org.assertj + assertj-core + test + org.slf4j slf4j-api - org.slf4j - slf4j-log4j12 - runtime + ch.qos.logback + logback-classic + test @@ -169,6 +198,10 @@ false release true + + pom.xml + settings.xml + @@ -223,6 +256,9 @@ **/examples/* + **/acceptance/* + **/doc/* + **/jmeter/* diff --git a/release-notes.html b/release-notes.html new file mode 100644 index 00000000..0bc1a1cd --- /dev/null +++ b/release-notes.html @@ -0,0 +1,114 @@ + + + + + + +

Welcome to MSB-Java version 1.6.6

+ +

November 7, 2018

+ +
+Changes in MSB-Java version 1.6.6:
+   - Updated AMQP client version from 5.2.0 to 5.4.0; 
+   - Updated java.lang.Error handling with auto-retry if auto-acknowledgement is enabled;
+   - Added 'msb-config.enabled' disable msb-java using property for testing purposes;
+
+Changes in MSB-Java version 1.6.5:
+   - Updated AMQP client version from 3.6.0 to 5.2.0; 
+   - Added check consumer connection isConnected() method;
+   - Upgraded msb-spring-boot-starter to Spring Boot 2 release;
+
+Changes in MSB-Java version 1.6.4:
+   - Exposed Message Count Metric for ResponderServer
+
+Changes in MSB-Java version 1.6.3:
+   - Added separate TRACE level log record when message body was put to error;
+   - Extended AcknowledgementHandler interface with retryMessageFirstTime() method. Message should be requeued only
+     if it was not delivered before;
+     
+Changes in MSB-Java version 1.6.2:
+   - Manual message retry is not limited to a single attempt anymore. It is completely up to client code to decide 
+     when message should not be retried anymore;
+   - Restored CLI tool. Supported 'fanout' and 'topic' exchange types;
+   - Full message logging was replaced with separated TRACE level message logging;
+
+Features of MSB-Java version 1.6.1:
+   - Improved message processing during context shutdown;
+   - Improved AMQP exception handling and channel automatic recovery;
+   - Removed JBehave tests as deprecated;
+   
+Features of MSB-Java version 1.6.0:
+   - Broken backward compatibility with 1.5.2 version. Now exchange type for AMQ is not determined by routing key
+     presence/absence. It can be set explicitly using AmqpRequestOptions. Default value 'fanout' is used if exchange
+     type is not specified. ResponderServer mirrors this behavior using AmqpResponderOptions (see examples for routing key);
+   - Fixed possible deadlock in AMQP channel automatic recovery logic;
+   - Added Spring Boot auto-configuration spring-boot-starter module;
+   - MSB_BROKER_VIRTUAL_HOST environment variable was deprecated. MSB_BROKER_AMQP_VHOST environment variable is used for 
+     RabbitMQ Virutal host specification. The both variable are supported now. Deprecated will be removed in next release;
+   - Removed ChannelMonitorAgent and ChannelMonitorAggregator;
+   - Removed CLI module.
+
+Features of MSB-Java version 1.5.2:
+   - Added routing key support to API and AMQP adapter;
+   - Added "routingKey" field to message envelope "topics" section;
+   - Removed confusing response topic for cases when no responses/acks are expected.
+
+Features of MSB-Java version 1.5.1:
+   - Added stop() method to ResponderServer;
+   - Add request object and map into MsbThreadContext;
+   - Fixed broken graceful shutdown of TimeoutManager on MsbContextImpl shutdown.
+
+Features of MSB-Java version 1.5.0:
+   - Multithreading settings parameters consumerThreadPoolSize, consumerThreadPoolQueueCapacity moved
+     from "brokerConfig" to "threadingConfig" config section;
+   - Now it is possible to process a group of messages in a single-threaded mode using
+     MsbContextBuilder.withMessageGroupStrategy();
+   - Added Requester.request() method that is similar to Requester.publish() but expects exactly 
+     one response and returns CompletableFuture.
+
+Features of MSB-Java version 1.4.6:
+   - Updated messages schema validation;
+   - Fixed issue with unresolved host name;
+   - Replaced Rest like response on incorrect structure request with zero Ack; 
+   - Added custom error handler for messages parsing or schema validation errors;
+   - Added MsbThreadContext to support flows that involve multiple conversations between microservices.
+   
+Features of MSB-Java version 1.4.4:
+   - MsbContext shutdown callbacks support;
+   - Incoming messages schema validation is disabled by default;
+   - AMQP default heartbeat interval increased to 30s;
+   - Performance improvements;
+   - MSB mocking support included, see MSB-TEST-MOCKING.md for details.
+
+Features of MSB-Java version 1.4.3:
+   - Added logging support: all message tags concatenated into one string can be added to consumers thread
+     MDC (see http://www.slf4j.org/api/org/slf4j/MDC.html) in 'msbTags' field. Message tags that have key-value format
+     (e.g. 'requestId:123456' or 'requestId-123456') can be added to consumers thread MDC as separate properties where
+     part before delimiter is a key and part after delimiter is a value. Delimiter is configurable. MDC is cleared
+     after message handler invocation return;
+   - it is possible to define a messages forwarding namespace (added topics.forward field to the msb envelope);
+   - Requester API now allows to publish message with multiple tags.
+
+Features of MSB-Java version 1.4.2:
+   - the library uses only slf4j API for logging
+     so log4j dependency is no longer included;
+   - logback dependency is included for testing only;
+   - dependencies updates.
+
+Features of MSB-Java version 1.4.0:
+   - Added prefetch count for AMQP adapter
+     (for details see https://www.rabbitmq.com/consumer-prefetch.html);
+   - Implemented explicit confirm/reject delivered messages;
+   - Fixed an issue when `onEnd` callback could be invoked by timeout while not all incoming responses are processed yet;
+   - Fixed an issue when time-consuming `onResponse` callbacks could block incoming responses processing;
+   - Acceptance testing is enabled by default during a build.
+
+Features of MSB-Java version 1.3.0:
+   - Removed REST-style payload constraint on parameters type;
+   - Updated Requester behavior. It waits for acks even if configured to
+     `waitForResponses: 0`.
+
+    
+ + \ No newline at end of file diff --git a/spring-boot-starter/pom.xml b/spring-boot-starter/pom.xml new file mode 100644 index 00000000..648bb27f --- /dev/null +++ b/spring-boot-starter/pom.xml @@ -0,0 +1,93 @@ + + + + io.github.tcdl.msb + msb-java + 1.6.7-SNAPSHOT + ../pom.xml + + 4.0.0 + msb-spring-boot-starter + msb spring boot starter + jar + + + scm:git:https://github.com/tcdl/msb-java.git + scm:git:git@github.com:tcdl/msb-java.git + https://github.com/tcdl/msb-java + HEAD + + + + tcdl + https://github.com/tcdl + + + + UTF-8 + UTF-8 + + 1.8 + 1.8 + + + + + org.springframework.boot + spring-boot-autoconfigure + + + + org.springframework.boot + spring-boot-starter-test + test + + + + io.github.tcdl.msb + msb-java-core + ${project.version} + + + + io.github.tcdl.msb + msb-java-amqp + ${project.version} + + + + + org.slf4j + jcl-over-slf4j + 1.7.12 + test + + + + + ch.qos.logback + logback-core + 1.1.3 + test + + + + + org.springframework.boot + spring-boot-configuration-processor + + + + + + + org.springframework.boot + spring-boot-dependencies + 2.0.0.RELEASE + pom + import + + + + + diff --git a/spring-boot-starter/src/main/java/io/github/tcdl/msb/autoconfigure/MsbConfigAutoConfiguration.java b/spring-boot-starter/src/main/java/io/github/tcdl/msb/autoconfigure/MsbConfigAutoConfiguration.java new file mode 100644 index 00000000..4c8866f5 --- /dev/null +++ b/spring-boot-starter/src/main/java/io/github/tcdl/msb/autoconfigure/MsbConfigAutoConfiguration.java @@ -0,0 +1,109 @@ +package io.github.tcdl.msb.autoconfigure; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import com.typesafe.config.ConfigValueFactory; +import io.github.tcdl.msb.config.MsbConfig; +import io.github.tcdl.msb.support.Utils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@ConditionalOnProperty(name = "msb-config.enabled", havingValue = "true", matchIfMissing = true) +@Configuration +@AutoConfigureBefore(MsbContextAutoConfiguration.class) +@EnableConfigurationProperties(MsbProperties.class) +public class MsbConfigAutoConfiguration { + + public static final String DEFAULT_VERSION = "1.0.0"; + public static final String DEFAULT_APP_NAME = Utils.generateId(); + public static final String DEFAULT_ADAPTER_FACTORY = "io.github.tcdl.msb.adapters.amqp.AmqpAdapterFactory"; + public static final int DEFAULT_TIMER_THREAD_POOL_SIZE = 2; + private static final boolean DEFAULT_BROKER_DURABLE = true; + @Autowired + MsbProperties msbProperties; + + @Bean + public MsbConfig config() { + Config config = ConfigFactory.load("reference"); + String appName = StringUtils.isNotBlank(msbProperties.serviceDetails.name) ? msbProperties.serviceDetails.name : DEFAULT_APP_NAME; + config = config.withValue("msbConfig.serviceDetails.name", ConfigValueFactory.fromAnyRef(appName)); + config = config.withValue("msbConfig.serviceDetails.instanceId", ConfigValueFactory.fromAnyRef(msbProperties.serviceDetails.instanceId)); + + // Service Details + String version = StringUtils.isNotBlank(msbProperties.serviceDetails.version) ? msbProperties.serviceDetails.version : DEFAULT_VERSION; + config = config.withValue("msbConfig.serviceDetails.version", ConfigValueFactory.fromAnyRef(version)); + if (StringUtils.isNotBlank(msbProperties.serviceDetails.hostname)) + config = config.withValue("msbConfig.serviceDetails.hostname", ConfigValueFactory.fromAnyRef(msbProperties.serviceDetails.hostname)); + if (StringUtils.isNotBlank(msbProperties.serviceDetails.ip)) + config = config.withValue("msbConfig.serviceDetails.ip", ConfigValueFactory.fromAnyRef(msbProperties.serviceDetails.ip)); + if (msbProperties.serviceDetails.pid != null) + config = config.withValue("msbConfig.serviceDetails.pid", ConfigValueFactory.fromAnyRef(msbProperties.serviceDetails.pid)); + + // Broker Adapter Factory + String brokerAdapterFactory = StringUtils.isNotBlank(msbProperties.brokerAdapterFactory) ? msbProperties.brokerAdapterFactory : DEFAULT_ADAPTER_FACTORY; + config = config.withValue("msbConfig.brokerAdapterFactory", ConfigValueFactory.fromAnyRef(brokerAdapterFactory)); + + // Thread pool used for scheduling ack and response timeout tasks + Integer timerThreadPoolSize = msbProperties.timerThreadPoolSize != null ? msbProperties.timerThreadPoolSize : DEFAULT_TIMER_THREAD_POOL_SIZE; + config = config.withValue("msbConfig.timerThreadPoolSize", ConfigValueFactory.fromAnyRef(timerThreadPoolSize)); + + // Threading Config for Clients + if (msbProperties.threadingConfig.consumerThreadPoolSize != null) + config = config.withValue("msbConfig.threadingConfig.consumerThreadPoolSize", ConfigValueFactory.fromAnyRef(msbProperties.threadingConfig.consumerThreadPoolSize)); + if (msbProperties.threadingConfig.consumerThreadPoolQueueCapacity != null) + config = config.withValue("msbConfig.threadingConfig.consumerThreadPoolQueueCapacity", ConfigValueFactory.fromAnyRef(msbProperties.threadingConfig.consumerThreadPoolQueueCapacity)); + + // Enable/disable message validation against json schema + if (msbProperties.validateMessage != null) + config = config.withValue("msbConfig.validateMessage", ConfigValueFactory.fromAnyRef(msbProperties.validateMessage)); + + //MDC logging + if (msbProperties.mdcLogging.enabled != null) + config = config.withValue("msbConfig.mdcLogging.enabled", ConfigValueFactory.fromAnyRef(msbProperties.mdcLogging.enabled)); + if (StringUtils.isNotBlank(msbProperties.mdcLogging.splitTagsBy)) + config = config.withValue("msbConfig.mdcLogging.splitTagsBy", ConfigValueFactory.fromAnyRef(msbProperties.mdcLogging.splitTagsBy)); + if (StringUtils.isNotBlank(msbProperties.mdcLogging.messageKeys.messageTags)) + config = config.withValue("msbConfig.mdcLogging.messageKeys.messageTags", ConfigValueFactory.fromAnyRef(msbProperties.mdcLogging.messageKeys.messageTags)); + if (StringUtils.isNotBlank(msbProperties.mdcLogging.messageKeys.correlationId)) + config = config.withValue("msbConfig.mdcLogging.messageKeys.correlationId", ConfigValueFactory.fromAnyRef(msbProperties.mdcLogging.messageKeys.correlationId)); + + //requestOptions + if (msbProperties.requestOptions.responseTimeout != null) + config = config.withValue("msbConfig.requestOptions.responseTimeout", ConfigValueFactory.fromAnyRef(msbProperties.requestOptions.responseTimeout)); + + //Broker Adapter Defaults + if (StringUtils.isNotBlank(msbProperties.brokerConfig.host)) + config = config.withValue("msbConfig.brokerConfig.host", ConfigValueFactory.fromAnyRef(msbProperties.brokerConfig.host)); + if (StringUtils.isNotBlank(msbProperties.brokerConfig.port)) + config = config.withValue("msbConfig.brokerConfig.port", ConfigValueFactory.fromAnyRef(msbProperties.brokerConfig.port)); + if (StringUtils.isNotBlank(msbProperties.brokerConfig.userName)) + config = config.withValue("msbConfig.brokerConfig.username", ConfigValueFactory.fromAnyRef(msbProperties.brokerConfig.userName)); + if (StringUtils.isNotBlank(msbProperties.brokerConfig.password)) + config = config.withValue("msbConfig.brokerConfig.password", ConfigValueFactory.fromAnyRef(msbProperties.brokerConfig.password)); + if (StringUtils.isNotBlank(msbProperties.brokerConfig.virtualHost)) + config = config.withValue("msbConfig.brokerConfig.virtualHost", ConfigValueFactory.fromAnyRef(msbProperties.brokerConfig.virtualHost)); + if (msbProperties.brokerConfig.useSSL != null) + config = config.withValue("msbConfig.brokerConfig.useSSL", ConfigValueFactory.fromAnyRef(msbProperties.brokerConfig.useSSL)); + boolean durable = msbProperties.brokerConfig.durable != null ? msbProperties.brokerConfig.durable : DEFAULT_BROKER_DURABLE; + config = config.withValue("msbConfig.brokerConfig.durable", ConfigValueFactory.fromAnyRef(durable)); + if (msbProperties.brokerConfig.charset != null) + config = config.withValue("msbConfig.brokerConfig.charset", ConfigValueFactory.fromAnyRef(msbProperties.brokerConfig.charset)); + if (StringUtils.isNotBlank(msbProperties.brokerConfig.groupId)) + config = config.withValue("msbConfig.brokerConfig.groupId", ConfigValueFactory.fromAnyRef(msbProperties.brokerConfig.groupId)); + if (msbProperties.brokerConfig.heartbeatIntervalSec != null) + config = config.withValue("msbConfig.brokerConfig.heartbeatIntervalSec", ConfigValueFactory.fromAnyRef(msbProperties.brokerConfig.heartbeatIntervalSec)); + if (msbProperties.brokerConfig.networkRecoveryIntervalMs != null) + config = config.withValue("msbConfig.brokerConfig.networkRecoveryIntervalMs", ConfigValueFactory.fromAnyRef(msbProperties.brokerConfig.networkRecoveryIntervalMs)); + if (StringUtils.isNotBlank(msbProperties.brokerConfig.defaultExchangeType)) + config = config.withValue("msbConfig.brokerConfig.defaultExchangeType", ConfigValueFactory.fromAnyRef(msbProperties.brokerConfig.defaultExchangeType)); + if (msbProperties.brokerConfig.prefetchCount != null) + config = config.withValue("msbConfig.brokerConfig.prefetchCount", ConfigValueFactory.fromAnyRef(msbProperties.brokerConfig.prefetchCount)); + + return new MsbConfig(config); + } +} diff --git a/spring-boot-starter/src/main/java/io/github/tcdl/msb/autoconfigure/MsbContextAutoConfiguration.java b/spring-boot-starter/src/main/java/io/github/tcdl/msb/autoconfigure/MsbContextAutoConfiguration.java new file mode 100644 index 00000000..504600b4 --- /dev/null +++ b/spring-boot-starter/src/main/java/io/github/tcdl/msb/autoconfigure/MsbContextAutoConfiguration.java @@ -0,0 +1,42 @@ +package io.github.tcdl.msb.autoconfigure; + +import io.github.tcdl.msb.api.MessageTemplate; +import io.github.tcdl.msb.api.MsbContext; +import io.github.tcdl.msb.api.MsbContextBuilder; +import io.github.tcdl.msb.config.MsbConfig; +import io.github.tcdl.msb.threading.MessageGroupStrategy; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@ConditionalOnProperty(name = "msb-config.enabled", havingValue = "true", matchIfMissing = true) +@Configuration +public class MsbContextAutoConfiguration { + + @Autowired + MsbConfig msbConfig; + + @Autowired(required = false) + MessageGroupStrategy messageGroupStrategy; + + @Bean + @ConditionalOnMissingBean(MessageTemplate.class) + public MessageTemplate messageTemplate() { + return new MessageTemplate(); + } + + @Bean + @ConditionalOnMissingBean(MsbContext.class) + public MsbContext msbContext() { + + MsbContextBuilder builder = new MsbContextBuilder() + .withMsbConfig(msbConfig); + + if (messageGroupStrategy != null) + builder = builder.withMessageGroupStrategy(messageGroupStrategy); + + return builder.build(); + } +} diff --git a/spring-boot-starter/src/main/java/io/github/tcdl/msb/autoconfigure/MsbProperties.java b/spring-boot-starter/src/main/java/io/github/tcdl/msb/autoconfigure/MsbProperties.java new file mode 100644 index 00000000..bb5d739e --- /dev/null +++ b/spring-boot-starter/src/main/java/io/github/tcdl/msb/autoconfigure/MsbProperties.java @@ -0,0 +1,344 @@ +package io.github.tcdl.msb.autoconfigure; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.nio.charset.Charset; + +@ConfigurationProperties("msb-config") +public class MsbProperties { + + ServiceDetails serviceDetails = new ServiceDetails(); + String brokerAdapterFactory; + Integer timerThreadPoolSize; + Boolean validateMessage; + ThreadingConfig threadingConfig = new ThreadingConfig(); + BrokerConfig brokerConfig = new BrokerConfig(); + MdcLogging mdcLogging = new MdcLogging(); + RequestOptions requestOptions = new RequestOptions(); + + public ServiceDetails getServiceDetails() { + return serviceDetails; + } + + public void setServiceDetails(ServiceDetails serviceDetails) { + this.serviceDetails = serviceDetails; + } + + public String getBrokerAdapterFactory() { + return brokerAdapterFactory; + } + + public void setBrokerAdapterFactory(String brokerAdapterFactory) { + this.brokerAdapterFactory = brokerAdapterFactory; + } + + public Integer getTimerThreadPoolSize() { + return timerThreadPoolSize; + } + + public void setTimerThreadPoolSize(Integer timerThreadPoolSize) { + this.timerThreadPoolSize = timerThreadPoolSize; + } + + public Boolean getValidateMessage() { + return validateMessage; + } + + public void setValidateMessage(Boolean validateMessage) { + this.validateMessage = validateMessage; + } + + public BrokerConfig getBrokerConfig() { + return brokerConfig; + } + + public void setBrokerConfig(BrokerConfig brokerConfig) { + this.brokerConfig = brokerConfig; + } + + public ThreadingConfig getThreadingConfig() { + return threadingConfig; + } + + public void setThreadingConfig(ThreadingConfig threadingConfig) { + this.threadingConfig = threadingConfig; + } + + public MdcLogging getMdcLogging() { + return mdcLogging; + } + + public void setMdcLogging(MdcLogging mdcLogging) { + this.mdcLogging = mdcLogging; + } + + public RequestOptions getRequestOptions() { + return requestOptions; + } + + public void setRequestOptions(RequestOptions requestOptions) { + this.requestOptions = requestOptions; + } + + public class ThreadingConfig { + Integer consumerThreadPoolSize; + Integer consumerThreadPoolQueueCapacity; + + public void setConsumerThreadPoolSize(Integer consumerThreadPoolSize) { + this.consumerThreadPoolSize = consumerThreadPoolSize; + } + + public Integer getConsumerThreadPoolSize() { + return consumerThreadPoolSize; + } + + public Integer getConsumerThreadPoolQueueCapacity() { + return consumerThreadPoolQueueCapacity; + } + + public void setConsumerThreadPoolQueueCapacity(Integer consumerThreadPoolQueueCapacity) { + this.consumerThreadPoolQueueCapacity = consumerThreadPoolQueueCapacity; + } + } + + public class ServiceDetails { + String name; + String version; + String instanceId; + String hostname; + String ip; + Long pid; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getInstanceId() { + return instanceId; + } + + public void setInstanceId(String instanceId) { + this.instanceId = instanceId; + } + + public String getHostname() { + return hostname; + } + + public void setHostname(String hostname) { + this.hostname = hostname; + } + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public Long getPid() { + return pid; + } + + public void setPid(Long pid) { + this.pid = pid; + } + } + + public class MdcLogging { + Boolean enabled; + String splitTagsBy; + MessageKeys messageKeys = new MessageKeys(); + + public Boolean getEnabled() { + return enabled; + } + + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + public String getSplitTagsBy() { + return splitTagsBy; + } + + public void setSplitTagsBy(String splitTagsBy) { + this.splitTagsBy = splitTagsBy; + } + + public MessageKeys getMessageKeys() { + return messageKeys; + } + + public void setMessageKeys(MessageKeys messageKeys) { + this.messageKeys = messageKeys; + } + } + + public class MessageKeys { + String messageTags; + String correlationId; + + public String getMessageTags() { + return messageTags; + } + + public void setMessageTags(String messageTags) { + this.messageTags = messageTags; + } + + public String getCorrelationId() { + return correlationId; + } + + public void setCorrelationId(String correlationId) { + this.correlationId = correlationId; + } + } + + public class RequestOptions { + Integer responseTimeout; + + public Integer getResponseTimeout() { + return responseTimeout; + } + + public void setResponseTimeout(Integer responseTimeout) { + this.responseTimeout = responseTimeout; + } + } + + public class BrokerConfig { + Charset charset; + String host; + String port; + String userName; + String password; + String virtualHost; + Boolean useSSL; + String groupId; + Boolean durable; + String defaultExchangeType; + Integer heartbeatIntervalSec; + Long networkRecoveryIntervalMs; + Integer prefetchCount; + + public Charset getCharset() { + return charset; + } + + public void setCharset(Charset charset) { + this.charset = charset; + } + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public String getPort() { + return port; + } + + public void setPort(String port) { + this.port = port; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getVirtualHost() { + return virtualHost; + } + + public void setVirtualHost(String virtualHost) { + this.virtualHost = virtualHost; + } + + public Boolean getUseSSL() { + return useSSL; + } + + public void setUseSSL(Boolean useSSL) { + this.useSSL = useSSL; + } + + public String getGroupId() { + return groupId; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } + + public Boolean getDurable() { + return durable; + } + + public void setDurable(Boolean durable) { + this.durable = durable; + } + + public Integer getHeartbeatIntervalSec() { + return heartbeatIntervalSec; + } + + public void setHeartbeatIntervalSec(Integer heartbeatIntervalSec) { + this.heartbeatIntervalSec = heartbeatIntervalSec; + } + + public Long getNetworkRecoveryIntervalMs() { + return networkRecoveryIntervalMs; + } + + public void setNetworkRecoveryIntervalMs(Long networkRecoveryIntervalMs) { + this.networkRecoveryIntervalMs = networkRecoveryIntervalMs; + } + + public String getDefaultExchangeType() { + return defaultExchangeType; + } + + public void setDefaultExchangeType(String defaultExchangeType) { + this.defaultExchangeType = defaultExchangeType; + } + + public Integer getPrefetchCount() { + return prefetchCount; + } + + public void setPrefetchCount(Integer prefetchCount) { + this.prefetchCount = prefetchCount; + } + } + +} diff --git a/spring-boot-starter/src/main/resources/META-INF/spring.factories b/spring-boot-starter/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..29c77b83 --- /dev/null +++ b/spring-boot-starter/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=io.github.tcdl.msb.autoconfigure.MsbContextAutoConfiguration,\ +io.github.tcdl.msb.autoconfigure.MsbConfigAutoConfiguration \ No newline at end of file diff --git a/spring-boot-starter/src/test/java/io/github/tcdl/msb/autoconfigure/MsbAutoConfigurationDisableTest.java b/spring-boot-starter/src/test/java/io/github/tcdl/msb/autoconfigure/MsbAutoConfigurationDisableTest.java new file mode 100644 index 00000000..a8088a87 --- /dev/null +++ b/spring-boot-starter/src/test/java/io/github/tcdl/msb/autoconfigure/MsbAutoConfigurationDisableTest.java @@ -0,0 +1,27 @@ +package io.github.tcdl.msb.autoconfigure; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.junit.Assert.assertTrue; + +@RunWith(SpringRunner.class) +@ContextConfiguration(classes = {MsbConfigAutoConfiguration.class, MsbContextAutoConfiguration.class}) +@TestPropertySource(properties = "msb-config.enabled=false") +public class MsbAutoConfigurationDisableTest { + + @Autowired + private ApplicationContext context; + + @Test + public void shouldDisableAutoConfigurationByFeatureFlag() { + assertTrue(context.getBeansOfType(MsbConfigAutoConfiguration.class).isEmpty()); + assertTrue(context.getBeansOfType(MsbContextAutoConfiguration.class).isEmpty()); + } + +} diff --git a/spring-boot-starter/src/test/java/io/github/tcdl/msb/autoconfigure/MsbAutoConfigurationTest.java b/spring-boot-starter/src/test/java/io/github/tcdl/msb/autoconfigure/MsbAutoConfigurationTest.java new file mode 100644 index 00000000..d8e48dc5 --- /dev/null +++ b/spring-boot-starter/src/test/java/io/github/tcdl/msb/autoconfigure/MsbAutoConfigurationTest.java @@ -0,0 +1,101 @@ +package io.github.tcdl.msb.autoconfigure; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import io.github.tcdl.msb.api.MessageTemplate; +import io.github.tcdl.msb.api.MsbContext; +import io.github.tcdl.msb.config.MsbConfig; +import org.junit.After; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.boot.test.util.EnvironmentTestUtils; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class MsbAutoConfigurationTest { + + private static final Integer TTL = 1000000; + private AnnotationConfigApplicationContext context; + + @After + public void tearDown() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + public void testDefaultMsbConfig() { + Config config = ConfigFactory.load("reference.conf"); + load(EmptyConfiguration.class); + MsbConfig msbConfig = this.context.getBean(MsbConfig.class); + + assertNotNull(msbConfig); + assertNotNull(msbConfig.getServiceDetails().getName()); + assertEquals(config.getString("msbConfig.brokerAdapterFactory"), msbConfig.getBrokerAdapterFactory()); + assertEquals(MsbConfigAutoConfiguration.DEFAULT_TIMER_THREAD_POOL_SIZE, msbConfig.getTimerThreadPoolSize()); + assertEquals(config.getInt("msbConfig.threadingConfig.consumerThreadPoolSize"), msbConfig.getConsumerThreadPoolSize()); + assertEquals(config.getInt("msbConfig.threadingConfig.consumerThreadPoolQueueCapacity"), msbConfig.getConsumerThreadPoolQueueCapacity()); + assertEquals(config.getBoolean("msbConfig.validateMessage"), msbConfig.isValidateMessage()); + } + + @Test + public void testOverrideMsbConfigParams() { + load(EmptyConfiguration.class, "msbConfig.serviceDetails.name=test-name", "msbConfig.threadingConfig.consumerThreadPoolSize=100", "msbConfig.brokerConfig.host=192.168.0.1"); + MsbConfig msbConfig = this.context.getBean(MsbConfig.class); + + assertEquals("test-name", msbConfig.getServiceDetails().getName()); + assertEquals(100, msbConfig.getConsumerThreadPoolSize()); + assertEquals("192.168.0.1", msbConfig.getBrokerConfig().getString("host")); + } + + @Test + public void testDefaultMessageTemplate() { + load(EmptyConfiguration.class); + MessageTemplate messageTemplate = this.context.getBean(MessageTemplate.class); + + assertNotNull(messageTemplate); + } + + @Test + public void testOverrideMessageTemplate() { + load(ConfigurationWithCustomMessageTemplate.class); + MessageTemplate messageTemplate = this.context.getBean(MessageTemplate.class); + + assertNotNull(messageTemplate); + assertEquals(TTL, messageTemplate.getTtl()); + } + + + + + + static class ConfigurationWithCustomMessageTemplate extends EmptyConfiguration{ + @Bean + MessageTemplate messageTemplate() { + return MessageTemplate.copyOf(new MessageTemplate()).withTtl(TTL); + } + } + + @Configuration + static class EmptyConfiguration { + //override bean to prevent establishing connection to real message queue + @Bean + MsbContext msbContext(){ + return Mockito.mock(MsbContext.class); + } + } + + private void load(Class config, String... environment) { + AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(); + EnvironmentTestUtils.addEnvironment(applicationContext, environment); + applicationContext.register(config); + applicationContext.register(MsbConfigAutoConfiguration.class, MsbContextAutoConfiguration.class); + applicationContext.refresh(); + this.context = applicationContext; + } +}