From 7cf0aec51b534cf0e7350e7508be06e3b9d9186e Mon Sep 17 00:00:00 2001 From: Rekish Date: Tue, 14 Jun 2022 16:24:59 +0400 Subject: [PATCH 01/51] [TH2-2212] fixed increasing recovery delay, added delay time deviation, added logging for hard errors --- .../connection/ConnectionManager.java | 128 +++++++++++------- .../configuration/RabbitMQConfiguration.kt | 1 + 2 files changed, 80 insertions(+), 49 deletions(-) diff --git a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java index 656f6d34e..a0e37402c 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 Exactpro (Exactpro Systems Limited) + * Copyright 2022 Exactpro (Exactpro Systems Limited) * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -14,36 +14,6 @@ */ package com.exactpro.th2.common.schema.message.impl.rabbitmq.connection; -import java.io.IOException; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; -import java.util.function.BiConsumer; -import java.util.function.Supplier; - -import javax.annotation.concurrent.GuardedBy; - -import org.apache.commons.lang3.ObjectUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.builder.EqualsBuilder; -import org.apache.commons.lang3.builder.HashCodeBuilder; -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; -import org.jetbrains.annotations.NotNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import com.exactpro.th2.common.metrics.HealthMetrics; import com.exactpro.th2.common.schema.message.ManualAckDeliveryCallback; import com.exactpro.th2.common.schema.message.ManualAckDeliveryCallback.Confirmation; @@ -61,10 +31,41 @@ import com.rabbitmq.client.Consumer; import com.rabbitmq.client.Envelope; import com.rabbitmq.client.ExceptionHandler; +import com.rabbitmq.client.Method; import com.rabbitmq.client.Recoverable; import com.rabbitmq.client.RecoveryListener; import com.rabbitmq.client.ShutdownNotifier; import com.rabbitmq.client.TopologyRecoveryException; +import com.rabbitmq.client.impl.AMQImpl; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.concurrent.GuardedBy; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.BiConsumer; +import java.util.function.Supplier; public class ConnectionManager implements AutoCloseable { @@ -189,7 +190,6 @@ private void turnOffReadiness(Throwable exception){ if (connectionIsClosed.get()) { return false; } - int tmpCountTriesToRecovery = connectionRecoveryAttempts.get(); if (tmpCountTriesToRecovery < connectionManagerConfiguration.getMaxRecoveryAttempts()) { @@ -208,13 +208,19 @@ private void turnOffReadiness(Throwable exception){ factory.setRecoveryDelayHandler(recoveryAttempts -> { int tmpCountTriesToRecovery = connectionRecoveryAttempts.getAndIncrement(); - - int recoveryDelay = connectionManagerConfiguration.getMinConnectionRecoveryTimeout() - + (connectionManagerConfiguration.getMaxRecoveryAttempts() > 1 - ? (connectionManagerConfiguration.getMaxConnectionRecoveryTimeout() - connectionManagerConfiguration.getMinConnectionRecoveryTimeout()) - / (connectionManagerConfiguration.getMaxRecoveryAttempts() - 1) - * tmpCountTriesToRecovery - : 0); + int minTime = connectionManagerConfiguration.getMinConnectionRecoveryTimeout(); + int maxTime = connectionManagerConfiguration.getMaxConnectionRecoveryTimeout(); + int maxRecoveryAttempts = connectionManagerConfiguration.getMaxRecoveryAttempts(); + int deviationPercent = connectionManagerConfiguration.getRetryTimeDeviationPercent(); + + LOGGER.info("Try to recovery connection to RabbitMQ. Count tries = {}", tmpCountTriesToRecovery); + int recoveryDelay; + if (tmpCountTriesToRecovery < maxRecoveryAttempts) { + recoveryDelay = minTime + (((maxTime - minTime) / maxRecoveryAttempts) * tmpCountTriesToRecovery); + } else { + int deviation = maxTime * deviationPercent / 100; + recoveryDelay = ThreadLocalRandom.current().nextInt(maxTime - deviation, maxTime + deviation + 1); + } LOGGER.info("Recovery delay for '{}' try = {}", tmpCountTriesToRecovery, recoveryDelay); return recoveryDelay; @@ -227,6 +233,7 @@ private void turnOffReadiness(Throwable exception){ try { this.connection = factory.newConnection(); + addShutdownListenerToConnection(this.connection); metrics.getReadinessMonitor().enable(); LOGGER.debug("Set RabbitMQ readiness to true"); } catch (IOException | TimeoutException e) { @@ -235,20 +242,29 @@ private void turnOffReadiness(Throwable exception){ throw new IllegalStateException("Failed to create RabbitMQ connection using configuration", e); } - this.connection.addBlockedListener(new BlockedListener() { - @Override - public void handleBlocked(String reason) throws IOException { - LOGGER.warn("RabbitMQ blocked connection: {}", reason); - } + addBlockedListenersToConnection(this.connection); + addRecoveryListenerToConnection(this.connection); + } - @Override - public void handleUnblocked() throws IOException { - LOGGER.warn("RabbitMQ unblocked connection"); + private void addShutdownListenerToConnection(Connection conn) { + conn.addShutdownListener(cause -> { + if (cause.isHardError()) { + Connection connectionCause = (Connection) cause.getReference(); + Method reason = cause.getReason(); + if (reason instanceof AMQImpl.Connection.Close && ((AMQImpl.Connection.Close) reason).getReplyCode() != 200) { + StringBuilder errorBuilder = new StringBuilder("RabbitMQ hard error occupied: "); + ((AMQImpl.Connection.Close) reason).appendArgumentDebugStringTo(errorBuilder); + errorBuilder.append(" on connection "); + errorBuilder.append(connectionCause); + LOGGER.warn(errorBuilder.toString()); + } } }); + } - if (this.connection instanceof Recoverable) { - Recoverable recoverableConnection = (Recoverable) this.connection; + private void addRecoveryListenerToConnection(Connection conn) { + if (conn instanceof Recoverable) { + Recoverable recoverableConnection = (Recoverable) conn; recoverableConnection.addRecoveryListener(recoveryListener); LOGGER.debug("Recovery listener was added to connection."); } else { @@ -256,6 +272,20 @@ public void handleUnblocked() throws IOException { } } + private void addBlockedListenersToConnection(Connection conn) { + conn.addBlockedListener(new BlockedListener() { + @Override + public void handleBlocked(String reason) { + LOGGER.warn("RabbitMQ blocked connection: {}", reason); + } + + @Override + public void handleUnblocked() { + LOGGER.warn("RabbitMQ unblocked connection"); + } + }); + } + public boolean isOpen() { return connection.isOpen() && !connectionIsClosed.get(); } diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/configuration/RabbitMQConfiguration.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/configuration/RabbitMQConfiguration.kt index a8d2813be..da02194f4 100644 --- a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/configuration/RabbitMQConfiguration.kt +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/configuration/RabbitMQConfiguration.kt @@ -36,6 +36,7 @@ data class ConnectionManagerConfiguration( var maxRecoveryAttempts: Int = 5, var minConnectionRecoveryTimeout: Int = 10000, var maxConnectionRecoveryTimeout: Int = 60000, + var retryTimeDeviationPercent: Int = 10, val prefetchCount: Int = 10, val messageRecursionLimit: Int = 100, val workingThreads: Int = 1, From d4949c1d085a936a4e26ad3b1800fbd8f190480e Mon Sep 17 00:00:00 2001 From: Rekish Date: Wed, 15 Jun 2022 14:38:45 +0400 Subject: [PATCH 02/51] [TH2-2212] fixed configurations tests --- .../impl/rabbitmq/configuration/RabbitMQConfiguration.kt | 2 +- .../resources/test_json_configurations/connection_manager.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/configuration/RabbitMQConfiguration.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/configuration/RabbitMQConfiguration.kt index da02194f4..17591f171 100644 --- a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/configuration/RabbitMQConfiguration.kt +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/configuration/RabbitMQConfiguration.kt @@ -36,8 +36,8 @@ data class ConnectionManagerConfiguration( var maxRecoveryAttempts: Int = 5, var minConnectionRecoveryTimeout: Int = 10000, var maxConnectionRecoveryTimeout: Int = 60000, - var retryTimeDeviationPercent: Int = 10, val prefetchCount: Int = 10, + var retryTimeDeviationPercent: Int = 10, val messageRecursionLimit: Int = 100, val workingThreads: Int = 1, val confirmationTimeout: Duration = Duration.ofMinutes(5) diff --git a/src/test/resources/test_json_configurations/connection_manager.json b/src/test/resources/test_json_configurations/connection_manager.json index 4711383fb..fe8dee838 100644 --- a/src/test/resources/test_json_configurations/connection_manager.json +++ b/src/test/resources/test_json_configurations/connection_manager.json @@ -5,5 +5,6 @@ "maxRecoveryAttempts": 8, "minConnectionRecoveryTimeout": 8888, "maxConnectionRecoveryTimeout": 88888, - "prefetchCount": 1 + "prefetchCount": 1, + "retryTimeDeviationPercent": 10 } \ No newline at end of file From fa2f55fb8796ad7b431a09ad5263d24ca93699d5 Mon Sep 17 00:00:00 2001 From: Rekish Date: Thu, 16 Jun 2022 17:36:17 +0400 Subject: [PATCH 03/51] [TH2-2212] simplified logic of connection recovery triggering --- .../schema/factory/AbstractCommonFactory.java | 2 +- .../connection/ConnectionManager.java | 47 ++++++++----------- .../connection/TestConnectionManager.kt | 4 +- 3 files changed, 22 insertions(+), 31 deletions(-) diff --git a/src/main/java/com/exactpro/th2/common/schema/factory/AbstractCommonFactory.java b/src/main/java/com/exactpro/th2/common/schema/factory/AbstractCommonFactory.java index ba074c6be..24b9f8896 100644 --- a/src/main/java/com/exactpro/th2/common/schema/factory/AbstractCommonFactory.java +++ b/src/main/java/com/exactpro/th2/common/schema/factory/AbstractCommonFactory.java @@ -654,7 +654,7 @@ protected PrometheusConfiguration loadPrometheusConfiguration() { } protected ConnectionManager createRabbitMQConnectionManager() { - return new ConnectionManager(getRabbitMqConfiguration(), getConnectionManagerConfiguration(), livenessMonitor::disable); + return new ConnectionManager(getRabbitMqConfiguration(), getConnectionManagerConfiguration()); } protected ConnectionManager getRabbitMqConnectionManager() { diff --git a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java index a0e37402c..ea64d6833 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java @@ -92,6 +92,10 @@ public void handleRecovery(Recoverable recoverable) { connectionRecoveryAttempts.set(0); metrics.getReadinessMonitor().enable(); LOGGER.debug("Set RabbitMQ readiness to true"); + if (!metrics.getLivenessMonitor().isEnabled()) { + metrics.getLivenessMonitor().enable(); + LOGGER.debug("Set RabbitMQ liveness to true"); + } } @Override @@ -102,7 +106,7 @@ public ConnectionManagerConfiguration getConfiguration() { return configuration; } - public ConnectionManager(@NotNull RabbitMQConfiguration rabbitMQConfiguration, @NotNull ConnectionManagerConfiguration connectionManagerConfiguration, Runnable onFailedRecoveryConnection) { + public ConnectionManager(@NotNull RabbitMQConfiguration rabbitMQConfiguration, @NotNull ConnectionManagerConfiguration connectionManagerConfiguration) { Objects.requireNonNull(rabbitMQConfiguration, "RabbitMQ configuration cannot be null"); this.configuration = Objects.requireNonNull(connectionManagerConfiguration, "Connection manager configuration can not be null"); @@ -186,25 +190,7 @@ private void turnOffReadiness(Throwable exception){ }); factory.setAutomaticRecoveryEnabled(true); - factory.setConnectionRecoveryTriggeringCondition(shutdownSignal -> { - if (connectionIsClosed.get()) { - return false; - } - int tmpCountTriesToRecovery = connectionRecoveryAttempts.get(); - - if (tmpCountTriesToRecovery < connectionManagerConfiguration.getMaxRecoveryAttempts()) { - LOGGER.info("Try to recovery connection to RabbitMQ. Count tries = {}", tmpCountTriesToRecovery + 1); - return true; - } - LOGGER.error("Can not connect to RabbitMQ. Count tries = {}", tmpCountTriesToRecovery); - if (onFailedRecoveryConnection != null) { - onFailedRecoveryConnection.run(); - } else { - // TODO: we should stop the execution of the application. Don't use System.exit!!! - throw new IllegalStateException("Cannot recover connection to RabbitMQ"); - } - return false; - }); + factory.setConnectionRecoveryTriggeringCondition(shutdownSignal -> !connectionIsClosed.get()); factory.setRecoveryDelayHandler(recoveryAttempts -> { int tmpCountTriesToRecovery = connectionRecoveryAttempts.getAndIncrement(); @@ -218,6 +204,10 @@ private void turnOffReadiness(Throwable exception){ if (tmpCountTriesToRecovery < maxRecoveryAttempts) { recoveryDelay = minTime + (((maxTime - minTime) / maxRecoveryAttempts) * tmpCountTriesToRecovery); } else { + if (metrics.getLivenessMonitor().isEnabled()) { + LOGGER.debug("Set RabbitMQ liveness to false. Can't recover connection"); + metrics.getLivenessMonitor().disable(); + } int deviation = maxTime * deviationPercent / 100; recoveryDelay = ThreadLocalRandom.current().nextInt(maxTime - deviation, maxTime + deviation + 1); } @@ -248,15 +238,18 @@ private void turnOffReadiness(Throwable exception){ private void addShutdownListenerToConnection(Connection conn) { conn.addShutdownListener(cause -> { - if (cause.isHardError()) { + if (cause.isHardError() && cause.getReference() instanceof Connection) { Connection connectionCause = (Connection) cause.getReference(); Method reason = cause.getReason(); - if (reason instanceof AMQImpl.Connection.Close && ((AMQImpl.Connection.Close) reason).getReplyCode() != 200) { - StringBuilder errorBuilder = new StringBuilder("RabbitMQ hard error occupied: "); - ((AMQImpl.Connection.Close) reason).appendArgumentDebugStringTo(errorBuilder); - errorBuilder.append(" on connection "); - errorBuilder.append(connectionCause); - LOGGER.warn(errorBuilder.toString()); + if (reason instanceof AMQImpl.Connection.Close) { + var castedReason = (AMQImpl.Connection.Close) reason; + if (castedReason.getReplyCode() != 200) { + StringBuilder errorBuilder = new StringBuilder("RabbitMQ hard error occupied: "); + castedReason.appendArgumentDebugStringTo(errorBuilder); + errorBuilder.append(" on connection "); + errorBuilder.append(connectionCause); + LOGGER.warn(errorBuilder.toString()); + } } } }); diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt index fe805a0af..8221a915f 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt @@ -65,9 +65,7 @@ class TestConnectionManager { prefetchCount = prefetchCount, confirmationTimeout = confirmationTimeout, ), - ) { - LOGGER.error { "Fatal connection problem" } - }.use { manager -> + ).use { manager -> manager.basicConsume(queueName, { _, delivery, ack -> LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from ${delivery.envelope.routingKey}" } queue += ack From 16d1cf248aa802d20db4c1006856b5589629f554 Mon Sep 17 00:00:00 2001 From: Rekish Date: Tue, 21 Jun 2022 19:10:07 +0400 Subject: [PATCH 04/51] [TH2-2212] added retry for channel level errors --- .../connection/ConnectionManager.java | 80 +++++++++++++++---- 1 file changed, 65 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java index ea64d6833..e0320ea26 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java @@ -37,6 +37,7 @@ import com.rabbitmq.client.ShutdownNotifier; import com.rabbitmq.client.TopologyRecoveryException; import com.rabbitmq.client.impl.AMQImpl; +import com.rabbitmq.client.impl.recovery.AutorecoveringChannel; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.builder.EqualsBuilder; @@ -49,6 +50,7 @@ import javax.annotation.concurrent.GuardedBy; import java.io.IOException; +import java.text.MessageFormat; import java.util.List; import java.util.Map; import java.util.Objects; @@ -200,16 +202,10 @@ private void turnOffReadiness(Throwable exception){ int deviationPercent = connectionManagerConfiguration.getRetryTimeDeviationPercent(); LOGGER.info("Try to recovery connection to RabbitMQ. Count tries = {}", tmpCountTriesToRecovery); - int recoveryDelay; - if (tmpCountTriesToRecovery < maxRecoveryAttempts) { - recoveryDelay = minTime + (((maxTime - minTime) / maxRecoveryAttempts) * tmpCountTriesToRecovery); - } else { - if (metrics.getLivenessMonitor().isEnabled()) { - LOGGER.debug("Set RabbitMQ liveness to false. Can't recover connection"); - metrics.getLivenessMonitor().disable(); - } - int deviation = maxTime * deviationPercent / 100; - recoveryDelay = ThreadLocalRandom.current().nextInt(maxTime - deviation, maxTime + deviation + 1); + int recoveryDelay = getRecoveryDelay(tmpCountTriesToRecovery, minTime, maxTime, maxRecoveryAttempts, deviationPercent); + if (tmpCountTriesToRecovery >= maxRecoveryAttempts && metrics.getLivenessMonitor().isEnabled()) { + LOGGER.debug("Set RabbitMQ liveness to false. Can't recover connection"); + metrics.getLivenessMonitor().disable(); } LOGGER.info("Recovery delay for '{}' try = {}", tmpCountTriesToRecovery, recoveryDelay); @@ -236,6 +232,28 @@ private void turnOffReadiness(Throwable exception){ addRecoveryListenerToConnection(this.connection); } + /** + * @param numberOfTries zero based + */ + private static int getRecoveryDelay(int numberOfTries, int minTime, int maxTime, int maxRecoveryAttempts, int deviationPercent) { + if (numberOfTries < maxRecoveryAttempts) { + return getRecoveryDelayWithIncrement(numberOfTries, minTime, maxTime, maxRecoveryAttempts); + } + return getRecoveryDelayWithDeviation(maxTime, deviationPercent); + } + + private static int getRecoveryDelayWithDeviation(int maxTime, int deviationPercent) { + int recoveryDelay; + int deviation = maxTime * deviationPercent / 100; + recoveryDelay = ThreadLocalRandom.current().nextInt(maxTime - deviation, maxTime + deviation + 1); + return recoveryDelay; + } + + + private static int getRecoveryDelayWithIncrement(int numberOfTries, int minTime, int maxTime, int maxRecoveryAttempts) { + return minTime + (((maxTime - minTime) / maxRecoveryAttempts) * numberOfTries); + } + private void addShutdownListenerToConnection(Connection conn) { conn.addShutdownListener(cause -> { if (cause.isHardError() && cause.getReference() instanceof Connection) { @@ -314,7 +332,7 @@ public void basicPublish(String exchange, String routingKey, BasicProperties pro public SubscriberMonitor basicConsume(String queue, ManualAckDeliveryCallback deliverCallback, CancelCallback cancelCallback) throws IOException { ChannelHolder holder = getChannelFor(PinId.forQueue(queue)); - String tag = holder.mapWithLock(channel -> + String tag = holder.retryingConsumeWithLock(channel -> channel.basicConsume(queue, false, subscriberName + "_" + nextSubscriberId.getAndIncrement(), (tagTmp, delivery) -> { try { Envelope envelope = delivery.getEnvelope(); @@ -346,7 +364,7 @@ public SubscriberMonitor basicConsume(String queue, ManualAckDeliveryCallback de } catch (IOException | RuntimeException e) { LOGGER.error("Cannot handle delivery for tag {}: {}", tagTmp, e.getMessage(), e); } - }, cancelCallback)); + }, cancelCallback), configuration); return new RabbitMqSubscriberMonitor(holder, tag, this::basicCancel); } @@ -430,7 +448,7 @@ private void waitForRecovery(ShutdownNotifier notifier) { } private boolean isConnectionRecovery(ShutdownNotifier notifier) { - return !notifier.isOpen() && !connectionIsClosed.get(); + return !(notifier instanceof AutorecoveringChannel) && !notifier.isOpen() && !connectionIsClosed.get(); } /** @@ -554,15 +572,41 @@ public void withLock(boolean waitForRecovery, ChannelConsumer consumer) throws I } } - public T mapWithLock(ChannelMapper mapper) throws IOException { + + + public T retryingConsumeWithLock(ChannelMapper mapper, ConnectionManagerConfiguration configuration) { lock.lock(); try { - return mapper.map(getChannel()); + int retryCount = 0; + Channel tempChannel = getChannel(); + while (true) { + try { + return mapper.map(tempChannel); + } catch (Exception e) { + int recoveryDelay = getRecoveryDelay(retryCount++, + configuration.getMinConnectionRecoveryTimeout(), + configuration.getMaxConnectionRecoveryTimeout(), + configuration.getMaxRecoveryAttempts(), + configuration.getRetryTimeDeviationPercent()); + LOGGER.warn(MessageFormat.format("Retrying consume №{0}, waiting for {1}ms, then recreating channel...", retryCount, recoveryDelay), e); + sleepFor(recoveryDelay); + tempChannel = recreateChannel(); + } + } } finally { lock.unlock(); } } + private void sleepFor(long ms) { + try { + TimeUnit.MILLISECONDS.sleep(ms); + } catch (InterruptedException e) { + LOGGER.error("Wait for connection recovery was interrupted", e); + Thread.currentThread().interrupt(); + } + } + /** * Decreases the number of unacked messages. * If the number of unacked messages is less than {@link #maxCount} @@ -618,6 +662,12 @@ private Channel getChannel() { return getChannel(true); } + private Channel recreateChannel() { + channel = supplier.get(); + reconnectionChecker.accept(channel, true); + return channel; + } + private Channel getChannel(boolean waitForRecovery) { if (channel == null) { channel = supplier.get(); From 6b85ae23d430e4a84d2a053dae2c139d578a071a Mon Sep 17 00:00:00 2001 From: Rekish Date: Tue, 21 Jun 2022 20:55:40 +0400 Subject: [PATCH 05/51] [TH2-2212] minor logger fixes, readme update --- README.md | 2 ++ .../impl/rabbitmq/connection/ConnectionManager.java | 8 +++----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 32d4c2cca..d6acdc774 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ The `CommonFactory` reads a RabbitMQ configuration from the rabbitMQ.json file. The `th2_readiness` probe is set to false and publishers are blocked after a lost connection to RabbitMQ. The `th2_readiness` probe is reverted to true if the connection will be recovered during specified attempts otherwise the `th2_liveness` probe will be set to false. * minConnectionRecoveryTimeout - this option defines a minimal interval in milliseconds between reconnect attempts, with its default value set to 10000. Common factory increases the reconnect interval values from minConnectionRecoveryTimeout to maxConnectionRecoveryTimeout. * maxConnectionRecoveryTimeout - this option defines a maximum interval in milliseconds between reconnect attempts, with its default value set to 60000. Common factory increases the reconnect interval values from minConnectionRecoveryTimeout to maxConnectionRecoveryTimeout. +* retryTimeDeviationPercent - if the current number of retry attempts is more than maxRecoveryAttempts, then following intervals will be in range `[maxConnectionRecoveryTimeout - deviationPercent%, maxConnectionRecoveryTimeout + deviationPercent%]`. Default value is 10%. * prefetchCount - this option is the maximum number of messages that the server will deliver, with its value set to 0 if unlimited, the default value is set to 10. * messageRecursionLimit - an integer number denotes how deep nested protobuf message might be, default = 100 @@ -94,6 +95,7 @@ The `CommonFactory` reads a RabbitMQ configuration from the rabbitMQ.json file. "maxRecoveryAttempts": 5, "minConnectionRecoveryTimeout": 10000, "maxConnectionRecoveryTimeout": 60000, + "retryTimeDeviationPercent": 10, "prefetchCount": 10, "messageRecursionLimit": 100 } diff --git a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java index e0320ea26..a1a5939da 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java @@ -94,10 +94,7 @@ public void handleRecovery(Recoverable recoverable) { connectionRecoveryAttempts.set(0); metrics.getReadinessMonitor().enable(); LOGGER.debug("Set RabbitMQ readiness to true"); - if (!metrics.getLivenessMonitor().isEnabled()) { - metrics.getLivenessMonitor().enable(); - LOGGER.debug("Set RabbitMQ liveness to true"); - } + metrics.getLivenessMonitor().enable(); } @Override @@ -201,7 +198,7 @@ private void turnOffReadiness(Throwable exception){ int maxRecoveryAttempts = connectionManagerConfiguration.getMaxRecoveryAttempts(); int deviationPercent = connectionManagerConfiguration.getRetryTimeDeviationPercent(); - LOGGER.info("Try to recovery connection to RabbitMQ. Count tries = {}", tmpCountTriesToRecovery); + LOGGER.debug("Try to recovery connection to RabbitMQ. Count tries = {}", tmpCountTriesToRecovery); int recoveryDelay = getRecoveryDelay(tmpCountTriesToRecovery, minTime, maxTime, maxRecoveryAttempts, deviationPercent); if (tmpCountTriesToRecovery >= maxRecoveryAttempts && metrics.getLivenessMonitor().isEnabled()) { LOGGER.debug("Set RabbitMQ liveness to false. Can't recover connection"); @@ -257,6 +254,7 @@ private static int getRecoveryDelayWithIncrement(int numberOfTries, int minTime, private void addShutdownListenerToConnection(Connection conn) { conn.addShutdownListener(cause -> { if (cause.isHardError() && cause.getReference() instanceof Connection) { + LOGGER.trace("Closing the connection: ", cause); Connection connectionCause = (Connection) cause.getReference(); Method reason = cause.getReason(); if (reason instanceof AMQImpl.Connection.Close) { From dad038c4b3c379df6a5da4573e3d61b823d7157f Mon Sep 17 00:00:00 2001 From: Rekish Date: Thu, 23 Jun 2022 16:21:01 +0400 Subject: [PATCH 06/51] [TH2-2212] channel level errors logging --- .../connection/ConnectionManager.java | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java index a1a5939da..ec607b0e4 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java @@ -50,7 +50,6 @@ import javax.annotation.concurrent.GuardedBy; import java.io.IOException; -import java.text.MessageFormat; import java.util.List; import java.util.Map; import java.util.Objects; @@ -251,6 +250,26 @@ private static int getRecoveryDelayWithIncrement(int numberOfTries, int minTime, return minTime + (((maxTime - minTime) / maxRecoveryAttempts) * numberOfTries); } + private void addShutdownListenerToChannel(Channel channel) { + channel.addShutdownListener(cause -> { + if (!cause.isHardError() && cause.getReference() instanceof Channel) { + LOGGER.trace("Closing the channel: ", cause); + Channel channelCause = (Channel) cause.getReference(); + Method reason = cause.getReason(); + if (reason instanceof AMQImpl.Channel.Close) { + var castedReason = (AMQImpl.Channel.Close) reason; + if (castedReason.getReplyCode() != 200) { + StringBuilder errorBuilder = new StringBuilder("RabbitMQ soft error occupied: "); + castedReason.appendArgumentDebugStringTo(errorBuilder); + errorBuilder.append(" on channel "); + errorBuilder.append(channelCause); + LOGGER.warn(errorBuilder.toString()); + } + } + } + }); + } + private void addShutdownListenerToConnection(Connection conn) { conn.addShutdownListener(cause -> { if (cause.isHardError() && cause.getReference() instanceof Connection) { @@ -328,7 +347,7 @@ public void basicPublish(String exchange, String routingKey, BasicProperties pro holder.withLock(channel -> channel.basicPublish(exchange, routingKey, props, body)); } - public SubscriberMonitor basicConsume(String queue, ManualAckDeliveryCallback deliverCallback, CancelCallback cancelCallback) throws IOException { + public SubscriberMonitor basicConsume(String queue, ManualAckDeliveryCallback deliverCallback, CancelCallback cancelCallback) { ChannelHolder holder = getChannelFor(PinId.forQueue(queue)); String tag = holder.retryingConsumeWithLock(channel -> channel.basicConsume(queue, false, subscriberName + "_" + nextSubscriberId.getAndIncrement(), (tagTmp, delivery) -> { @@ -341,6 +360,8 @@ public SubscriberMonitor basicConsume(String queue, ManualAckDeliveryCallback de Confirmation confirmation = OnlyOnceConfirmation.wrap("from " + routingKey + " to " + queue, () -> holder.withLock(ch -> { try { basicAck(ch, deliveryTag); + } catch (Exception e) { + LOGGER.warn("Error during basicAck of message with deliveryTag = {} inside channel #{}: {}", deliveryTag, ch.getChannelNumber(), e); } finally { holder.release(() -> metrics.getReadinessMonitor().enable()); } @@ -406,6 +427,7 @@ private Channel createChannel() { Channel channel = connection.createChannel(); Objects.requireNonNull(channel, () -> "No channels are available in the connection. Max channel number: " + connection.getChannelMax()); channel.basicQos(configuration.getPrefetchCount()); + addShutdownListenerToChannel(channel); channel.addReturnListener(ret -> LOGGER.warn("Can not router message to exchange '{}', routing key '{}'. Reply code '{}' and text = {}", ret.getExchange(), ret.getRoutingKey(), ret.getReplyCode(), ret.getReplyText())); return channel; @@ -586,7 +608,7 @@ public T retryingConsumeWithLock(ChannelMapper mapper, ConnectionManagerC configuration.getMaxConnectionRecoveryTimeout(), configuration.getMaxRecoveryAttempts(), configuration.getRetryTimeDeviationPercent()); - LOGGER.warn(MessageFormat.format("Retrying consume №{0}, waiting for {1}ms, then recreating channel...", retryCount, recoveryDelay), e); + LOGGER.warn("Retrying consume #{}, waiting for {}ms, then recreating channel. Reason: {}", retryCount, recoveryDelay, e); sleepFor(recoveryDelay); tempChannel = recreateChannel(); } From 0261f2fa3ace01fd5d5a76f2f3ef7a47524d96e3 Mon Sep 17 00:00:00 2001 From: Rekish Date: Thu, 23 Jun 2022 18:05:42 +0400 Subject: [PATCH 07/51] [TH2-2212] fix: added throws --- .../message/impl/rabbitmq/connection/ConnectionManager.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java index ec607b0e4..fe7d253b3 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java @@ -347,7 +347,7 @@ public void basicPublish(String exchange, String routingKey, BasicProperties pro holder.withLock(channel -> channel.basicPublish(exchange, routingKey, props, body)); } - public SubscriberMonitor basicConsume(String queue, ManualAckDeliveryCallback deliverCallback, CancelCallback cancelCallback) { + public SubscriberMonitor basicConsume(String queue, ManualAckDeliveryCallback deliverCallback, CancelCallback cancelCallback) throws IOException { ChannelHolder holder = getChannelFor(PinId.forQueue(queue)); String tag = holder.retryingConsumeWithLock(channel -> channel.basicConsume(queue, false, subscriberName + "_" + nextSubscriberId.getAndIncrement(), (tagTmp, delivery) -> { @@ -592,8 +592,6 @@ public void withLock(boolean waitForRecovery, ChannelConsumer consumer) throws I } } - - public T retryingConsumeWithLock(ChannelMapper mapper, ConnectionManagerConfiguration configuration) { lock.lock(); try { From 3ca6dddda154dc0ecf7667051b70f3fef4bcfb29 Mon Sep 17 00:00:00 2001 From: Rekish Date: Thu, 23 Jun 2022 18:14:36 +0400 Subject: [PATCH 08/51] [TH2-2212] version bump, readme edited --- README.md | 6 ++++++ gradle.properties | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 16fa86e47..34dcdd668 100644 --- a/README.md +++ b/README.md @@ -300,6 +300,12 @@ dependencies { ## Release notes +### 3.40.0 + ++ Added retry in case of a RabbitMQ channel or connection error (when possible). ++ Added additional logging for RabbitMQ errors. ++ Fixed connection recovery delay time. + ### 3.39.2 + Fixed: diff --git a/gradle.properties b/gradle.properties index d68e349b1..970584b98 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ # limitations under the License. # -release_version=3.39.2 +release_version=3.40.0 description = 'th2 common library (Java)' From eb394c225edb871a1c681c98cdfc06c9c0a23106 Mon Sep 17 00:00:00 2001 From: Rekish Date: Fri, 24 Jun 2022 15:15:48 +0400 Subject: [PATCH 09/51] [TH2-2212] added interrupted exception to basicConsume --- README.md | 1 + .../rabbitmq/AbstractRabbitSubscriber.java | 3 +++ .../connection/ConnectionManager.java | 19 +++++-------------- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 34dcdd668..7ec84968b 100644 --- a/README.md +++ b/README.md @@ -303,6 +303,7 @@ dependencies { ### 3.40.0 + Added retry in case of a RabbitMQ channel or connection error (when possible). ++ Added InterruptedException to basicConsume method signature. + Added additional logging for RabbitMQ errors. + Fixed connection recovery delay time. diff --git a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitSubscriber.java b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitSubscriber.java index fc22f9a82..2e1ea8c10 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitSubscriber.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitSubscriber.java @@ -148,6 +148,9 @@ public void start() throws Exception { LOGGER.info("Start listening queue name='{}'", queue); } catch (IOException e) { throw new IllegalStateException("Can not start subscribe to queue = " + queue, e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Thread was interrupted while consuming", e); } } diff --git a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java index fe7d253b3..1391da972 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java @@ -252,8 +252,8 @@ private static int getRecoveryDelayWithIncrement(int numberOfTries, int minTime, private void addShutdownListenerToChannel(Channel channel) { channel.addShutdownListener(cause -> { + LOGGER.debug("Closing the channel: ", cause); if (!cause.isHardError() && cause.getReference() instanceof Channel) { - LOGGER.trace("Closing the channel: ", cause); Channel channelCause = (Channel) cause.getReference(); Method reason = cause.getReason(); if (reason instanceof AMQImpl.Channel.Close) { @@ -272,8 +272,8 @@ private void addShutdownListenerToChannel(Channel channel) { private void addShutdownListenerToConnection(Connection conn) { conn.addShutdownListener(cause -> { + LOGGER.debug("Closing the connection: ", cause); if (cause.isHardError() && cause.getReference() instanceof Connection) { - LOGGER.trace("Closing the connection: ", cause); Connection connectionCause = (Connection) cause.getReference(); Method reason = cause.getReason(); if (reason instanceof AMQImpl.Connection.Close) { @@ -347,7 +347,7 @@ public void basicPublish(String exchange, String routingKey, BasicProperties pro holder.withLock(channel -> channel.basicPublish(exchange, routingKey, props, body)); } - public SubscriberMonitor basicConsume(String queue, ManualAckDeliveryCallback deliverCallback, CancelCallback cancelCallback) throws IOException { + public SubscriberMonitor basicConsume(String queue, ManualAckDeliveryCallback deliverCallback, CancelCallback cancelCallback) throws IOException, InterruptedException { ChannelHolder holder = getChannelFor(PinId.forQueue(queue)); String tag = holder.retryingConsumeWithLock(channel -> channel.basicConsume(queue, false, subscriberName + "_" + nextSubscriberId.getAndIncrement(), (tagTmp, delivery) -> { @@ -592,7 +592,7 @@ public void withLock(boolean waitForRecovery, ChannelConsumer consumer) throws I } } - public T retryingConsumeWithLock(ChannelMapper mapper, ConnectionManagerConfiguration configuration) { + public T retryingConsumeWithLock(ChannelMapper mapper, ConnectionManagerConfiguration configuration) throws InterruptedException { lock.lock(); try { int retryCount = 0; @@ -607,7 +607,7 @@ public T retryingConsumeWithLock(ChannelMapper mapper, ConnectionManagerC configuration.getMaxRecoveryAttempts(), configuration.getRetryTimeDeviationPercent()); LOGGER.warn("Retrying consume #{}, waiting for {}ms, then recreating channel. Reason: {}", retryCount, recoveryDelay, e); - sleepFor(recoveryDelay); + TimeUnit.MILLISECONDS.sleep(recoveryDelay); tempChannel = recreateChannel(); } } @@ -616,15 +616,6 @@ public T retryingConsumeWithLock(ChannelMapper mapper, ConnectionManagerC } } - private void sleepFor(long ms) { - try { - TimeUnit.MILLISECONDS.sleep(ms); - } catch (InterruptedException e) { - LOGGER.error("Wait for connection recovery was interrupted", e); - Thread.currentThread().interrupt(); - } - } - /** * Decreases the number of unacked messages. * If the number of unacked messages is less than {@link #maxCount} From 0547a17f92d316461191a162a6e4dcf1224a8fb4 Mon Sep 17 00:00:00 2001 From: "fiodar.rekish" Date: Fri, 24 Jun 2022 14:54:14 +0000 Subject: [PATCH 10/51] [TH2-2212] title version bump --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7ec84968b..91d5cb710 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# th2 common library (Java) (3.39.2) +# th2 common library (Java) (3.40.0) ## Usage From 7ed4122e34db0602b87411b3c425da67156acada Mon Sep 17 00:00:00 2001 From: "fiodar.rekish" Date: Mon, 7 Nov 2022 16:34:24 +0400 Subject: [PATCH 11/51] [TH2-2212] added integration tests --- .../connection/TestConnectionManager.kt | 196 +++++++++++++++++- .../common/util/RabbitTestContainerUtil.kt | 54 +++++ 2 files changed, 244 insertions(+), 6 deletions(-) create mode 100644 src/test/kotlin/com/exactpro/th2/common/util/RabbitTestContainerUtil.kt diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt index 8221a915f..e7c3db281 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt @@ -19,16 +19,19 @@ import com.exactpro.th2.common.annotations.IntegrationTest import com.exactpro.th2.common.schema.message.ManualAckDeliveryCallback import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.ConnectionManagerConfiguration import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.RabbitMQConfiguration +import com.exactpro.th2.common.util.RabbitTestContainerUtil +import com.rabbitmq.client.AlreadyClosedException import com.rabbitmq.client.BuiltinExchangeType +import java.time.Duration +import java.util.concurrent.ArrayBlockingQueue +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger import mu.KotlinLogging import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import org.testcontainers.containers.RabbitMQContainer import org.testcontainers.utility.DockerImageName -import java.time.Duration -import java.util.concurrent.ArrayBlockingQueue -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit private val LOGGER = KotlinLogging.logger { } @@ -78,7 +81,12 @@ class TestConnectionManager { manager.basicPublish(exchange, routingKey, null, "Hello $index".toByteArray(Charsets.UTF_8)) } - Assertions.assertTrue(countDown.await(1L, TimeUnit.SECONDS)) { "Not all messages were received: ${countDown.count}" } + Assertions.assertTrue( + countDown.await( + 1L, + TimeUnit.SECONDS + ) + ) { "Not all messages were received: ${countDown.count}" } Assertions.assertTrue(manager.isAlive) { "Manager should still be alive" } Assertions.assertTrue(manager.isReady) { "Manager should be ready until the confirmation timeout expires" } @@ -100,4 +108,180 @@ class TestConnectionManager { } } } -} \ No newline at end of file + + @Test + fun `connection manager receives a message from a queue that did not exist at the time of subscription`() { + val routingKey = "routingKey" + val queueName = "queue" + val exchange = "test-exchange" + val prefetchCount = 10 + RabbitMQContainer(DockerImageName.parse("rabbitmq:3.8-management-alpine")) + .withExchange(exchange, BuiltinExchangeType.FANOUT.type, false, false, true, emptyMap()) + .withQueue(queueName) + .withBinding(exchange, queueName, emptyMap(), routingKey, "queue") + .use { + it.start() + LOGGER.info { "Started with port ${it.amqpPort}" } + val counter = AtomicInteger(0) + val confirmationTimeout = Duration.ofSeconds(1) + ConnectionManager( + RabbitMQConfiguration( + host = it.host, + vHost = "", + port = it.amqpPort, + username = it.adminUsername, + password = it.adminPassword, + ), + ConnectionManagerConfiguration( + subscriberName = "test", + prefetchCount = prefetchCount, + confirmationTimeout = confirmationTimeout, + minConnectionRecoveryTimeout = 100, + maxConnectionRecoveryTimeout = 200, + maxRecoveryAttempts = 5 + ), + ).use { connectionManager -> + Thread { + connectionManager.basicConsume("wrong-queue", { _, delivery, ack -> + LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from ${delivery.envelope.routingKey}" } + counter.incrementAndGet() + ack.confirm() + }) { + LOGGER.info { "Canceled $it" } + } + }.start() + + Thread.sleep(500) + + LOGGER.info { "creating the queue..." } + RabbitTestContainerUtil.declareQueue(it, "wrong-queue") + LOGGER.info { RabbitTestContainerUtil.putMessageInQueue(it, "wrong-queue") } + LOGGER.info { "queues list: \n ${it.execInContainer("rabbitmqctl", "list_queues")}" } + + Thread.sleep(500) + + Assertions.assertEquals(1, counter.get()) + Assertions.assertTrue(connectionManager.isAlive) + Assertions.assertTrue(connectionManager.isReady) + } + } + } + + @Test + fun `connection manager sends a message to wrong exchange`() { + val queueName = "queue" + val exchange = "test-exchange" + val prefetchCount = 10 + RabbitMQContainer(DockerImageName.parse("rabbitmq:3.8-management-alpine")) + .withQueue(queueName) + .use { + it.start() + LOGGER.info { "Started with port ${it.amqpPort}" } + val confirmationTimeout = Duration.ofSeconds(1) + val counter = AtomicInteger(0) + ConnectionManager( + RabbitMQConfiguration( + host = it.host, + vHost = "", + port = it.amqpPort, + username = it.adminUsername, + password = it.adminPassword, + ), + ConnectionManagerConfiguration( + subscriberName = "test", + prefetchCount = prefetchCount, + confirmationTimeout = confirmationTimeout, + minConnectionRecoveryTimeout = 100, + maxConnectionRecoveryTimeout = 200, + maxRecoveryAttempts = 5 + ), + ).use { connectionManager -> + Thread { + connectionManager.basicConsume(queueName, { _, delivery, ack -> + counter.incrementAndGet() + LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from ${delivery.envelope.routingKey}" } + }) { + LOGGER.info { "Canceled $it" } + } + }.start() + + Thread.sleep(5000) + + LOGGER.info { "Starting publishing..." } + connectionManager.basicPublish(exchange, "", null, "Hello1".toByteArray(Charsets.UTF_8)) + Thread.sleep(1000) + LOGGER.info { "Publication finished!" } + RabbitTestContainerUtil.declareFanoutExchangeWithBinding(it, exchange, queueName) + Thread.sleep(1000) + Assertions.assertThrows(AlreadyClosedException::class.java) { + connectionManager.basicPublish(exchange, "", null, "Hello2".toByteArray(Charsets.UTF_8)) + } + Assertions.assertEquals(0, counter.get()) + + } + } + } +} + +// @Test +// @Disabled +// fun `connection manager receives a messages after container restart`() { +// val routingKey = "routingKey" +// val queueName = "queue" +// val exchange = "test-exchange" +// val prefetchCount = 10 +// RabbitMQContainer(DockerImageName.parse("rabbitmq:3.8-management-alpine")) +//// .withExchange(exchange, BuiltinExchangeType.FANOUT.type, false, false, true, emptyMap()) +// .withQueue(queueName) +//// .withBinding(exchange, queueName, emptyMap(), routingKey, "queue") +// .use { +// it.start() +// LOGGER.info { "Started with port ${it.amqpPort}" } +// val counter = AtomicInteger(0) +// val confirmationTimeout = Duration.ofSeconds(1) +// ConnectionManager( +// RabbitMQConfiguration( +// host = it.host, +// vHost = "", +// port = it.amqpPort, +// username = it.adminUsername, +// password = it.adminPassword, +// ), +// ConnectionManagerConfiguration( +// subscriberName = "test", +// prefetchCount = prefetchCount, +// confirmationTimeout = confirmationTimeout, +// minConnectionRecoveryTimeout = 100, +// maxConnectionRecoveryTimeout = 200, +// maxRecoveryAttempts = 5 +// ), +// ).use { connectionManager -> +// Thread { +// connectionManager.basicConsume(queueName, { _, delivery, ack -> +// LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from ${delivery.envelope.routingKey}" } +// counter.incrementAndGet() +// }) { +// LOGGER.info { "Canceled $it" } +// } +// }.start() +// +// LOGGER.info { it.host + " " + it.httpPort + " " + it.amqpPort } +// +// +// LOGGER.info { it.host + " " + it.httpPort + " " + it.amqpPort } +// Thread.sleep(10000) +// +// LOGGER.info { "Starting publishing..." } +// RabbitTestContainerUtil.putMessageInQueue(it, queueName) +// LOGGER.info { "Publication finished!" } +// +// Assertions.assertEquals(1, counter.get()) +// Thread.sleep(1000) +// +// } +// } +// } +//} +// + + diff --git a/src/test/kotlin/com/exactpro/th2/common/util/RabbitTestContainerUtil.kt b/src/test/kotlin/com/exactpro/th2/common/util/RabbitTestContainerUtil.kt new file mode 100644 index 000000000..d54577c20 --- /dev/null +++ b/src/test/kotlin/com/exactpro/th2/common/util/RabbitTestContainerUtil.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2020-2022 Exactpro (Exactpro Systems Limited) + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.exactpro.th2.common.util + +import org.testcontainers.containers.Container +import org.testcontainers.containers.RabbitMQContainer + +class RabbitTestContainerUtil { + companion object { + fun declareQueue(rabbit: RabbitMQContainer, queueName: String): Container.ExecResult? { + return execCommandWithSplit(rabbit, "rabbitmqadmin declare queue name=$queueName durable=false") + + } + + fun declareFanoutExchangeWithBinding( + rabbit: RabbitMQContainer, + exchangeName: String, + destinationQueue: String + ) { + execCommandWithSplit(rabbit, "rabbitmqadmin declare exchange name=$exchangeName type=fanout") + execCommandWithSplit( + rabbit, + """rabbitmqadmin declare binding source="$exchangeName" destination_type="queue" destination="$destinationQueue"""" + ) + } + + fun putMessageInQueue(rabbit: RabbitMQContainer, queueName: String): Container.ExecResult? { + return execCommandWithSplit( + rabbit, + """rabbitmqadmin publish exchange=amq.default routing_key=$queueName payload="hello"""" + ) + } + + private fun execCommandWithSplit(rabbit: RabbitMQContainer, command: String): Container.ExecResult? { + return rabbit.execInContainer( + *command.split(" ").toTypedArray() + ) + } + + } +} \ No newline at end of file From e4a9ad6bac5a86c9a2e270d928428418adf337ed Mon Sep 17 00:00:00 2001 From: "fiodar.rekish" Date: Thu, 10 Nov 2022 17:40:27 +0400 Subject: [PATCH 12/51] [TH2-2212] recoverable subscriptions, added integrations tests --- .../connection/ConnectionManager.java | 90 ++++++- .../connection/TestConnectionManager.kt | 243 ++++++++++++------ .../common/util/RabbitTestContainerUtil.kt | 28 +- src/test/resources/rabbitmq_it.conf | 2 + 4 files changed, 281 insertions(+), 82 deletions(-) create mode 100644 src/test/resources/rabbitmq_it.conf diff --git a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java index b46d78b1b..5aaf75ba5 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java @@ -67,6 +67,7 @@ import java.util.concurrent.locks.ReentrantLock; import java.util.function.BiConsumer; import java.util.function.Supplier; +import java.util.stream.Collectors; public class ConnectionManager implements AutoCloseable { @@ -74,6 +75,7 @@ public class ConnectionManager implements AutoCloseable { private final Connection connection; private final Map channelsByPin = new ConcurrentHashMap<>(); + private final Map subscriptionBackupMap = new ConcurrentHashMap<>(); private final AtomicInteger connectionRecoveryAttempts = new AtomicInteger(0); private final AtomicBoolean connectionIsClosed = new AtomicBoolean(false); private final ConnectionManagerConfiguration configuration; @@ -251,7 +253,7 @@ private static int getRecoveryDelayWithIncrement(int numberOfTries, int minTime, } private void addShutdownListenerToChannel(Channel channel) { - channel.addShutdownListener(cause -> { + channel.addShutdownListener(cause -> new Thread(() -> { LOGGER.debug("Closing the channel: ", cause); if (!cause.isHardError() && cause.getReference() instanceof Channel) { Channel channelCause = (Channel) cause.getReference(); @@ -263,11 +265,45 @@ private void addShutdownListenerToChannel(Channel channel) { castedReason.appendArgumentDebugStringTo(errorBuilder); errorBuilder.append(" on channel "); errorBuilder.append(channelCause); - LOGGER.warn(errorBuilder.toString()); + String errorString = errorBuilder.toString(); + LOGGER.warn(errorString); + if (errorString.contains("PRECONDITION_FAILED")) { + recoverSubscriptionsOfChannel(channel); + } } } } - }); + }).start()); + } + + private void recoverSubscriptionsOfChannel(Channel channel) { + var mapEntries = + channelsByPin + .entrySet() + .stream() + .filter(entry -> channel.equals(entry.getValue().channel)) + .collect(Collectors.toList()); + for (Map.Entry mapEntry : mapEntries) { + PinId pinId = mapEntry.getKey(); + SubscriptionCallbacks subscriptionCallbacks = subscriptionBackupMap.get(pinId); + if (subscriptionCallbacks != null) { + + + try { + LOGGER.info("Changing channel for holder with pin id: " + pinId.toString()); + + channelsByPin.remove(pinId); +// mapEntry.getValue().lock.lock(); +// var newChannel = getNewChannelFor(pinId); +// mapEntry.setValue(newChannel); +// mapEntry.getValue().lock.unlock(); + channel.abort(); // todo ????????? + basicConsume(pinId.queue, subscriptionCallbacks.deliverCallback, subscriptionCallbacks.cancelCallback); + } catch (IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + } } private void addShutdownListenerToConnection(Connection conn) { @@ -342,14 +378,15 @@ public void close() { shutdownExecutor(channelChecker, closeTimeout, "channel-checker"); } - public void basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body) throws IOException { + public void basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body) throws InterruptedException { ChannelHolder holder = getChannelFor(PinId.forRoutingKey(routingKey)); - - holder.withLock(channel -> channel.basicPublish(exchange, routingKey, props, body)); + holder.retryingPublishWithLock(channel -> channel.basicPublish(exchange, routingKey, props, body), configuration); } public SubscriberMonitor basicConsume(String queue, ManualAckDeliveryCallback deliverCallback, CancelCallback cancelCallback) throws IOException, InterruptedException { - ChannelHolder holder = getChannelFor(PinId.forQueue(queue)); + PinId pinId = PinId.forQueue(queue); + ChannelHolder holder = getChannelFor(pinId); + backupSubscription(pinId, deliverCallback, cancelCallback); String tag = holder.retryingConsumeWithLock(channel -> channel.basicConsume(queue, false, subscriberName + "_" + nextSubscriberId.getAndIncrement(), (tagTmp, delivery) -> { try { @@ -414,6 +451,20 @@ private void shutdownExecutor(ExecutorService executor, int closeTimeout, String } } + private void backupSubscription(PinId pinId, ManualAckDeliveryCallback deliverCallback, CancelCallback cancelCallback) { + subscriptionBackupMap.put(pinId, new SubscriptionCallbacks(deliverCallback, cancelCallback)); + } + + private static final class SubscriptionCallbacks { + private final ManualAckDeliveryCallback deliverCallback; + private final CancelCallback cancelCallback; + + public SubscriptionCallbacks(ManualAckDeliveryCallback deliverCallback, CancelCallback cancelCallback) { + this.deliverCallback = deliverCallback; + this.cancelCallback = cancelCallback; + } + } + private ChannelHolder getChannelFor(PinId pinId) { return channelsByPin.computeIfAbsent(pinId, ignore -> { LOGGER.trace("Creating channel holder for {}", pinId); @@ -593,6 +644,31 @@ public void withLock(boolean waitForRecovery, ChannelConsumer consumer) throws I } } + public void retryingPublishWithLock(ChannelConsumer consumer, ConnectionManagerConfiguration configuration) throws InterruptedException { + lock.lock(); + try { + int retryCount = 0; + Channel tempChannel = getChannel(true); + while (true) { + try { + consumer.consume(tempChannel); + break; + } catch (Exception e) { + int recoveryDelay = getRecoveryDelay(retryCount++, + configuration.getMinConnectionRecoveryTimeout(), + configuration.getMaxConnectionRecoveryTimeout(), + configuration.getMaxRecoveryAttempts(), + configuration.getRetryTimeDeviationPercent()); + LOGGER.warn("Retrying publishing #{}, waiting for {}ms, then recreating channel. Reason: {}", retryCount, recoveryDelay, e); + TimeUnit.MILLISECONDS.sleep(recoveryDelay); + tempChannel = recreateChannel(); + } + } + } finally { + lock.unlock(); + } + } + public T retryingConsumeWithLock(ChannelMapper mapper, ConnectionManagerConfiguration configuration) throws InterruptedException { lock.lock(); try { diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt index e1240b3ef..fa86a3344 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt @@ -20,7 +20,9 @@ import com.exactpro.th2.common.schema.message.ManualAckDeliveryCallback import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.ConnectionManagerConfiguration import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.RabbitMQConfiguration import com.exactpro.th2.common.util.RabbitTestContainerUtil -import com.rabbitmq.client.AlreadyClosedException +import com.exactpro.th2.common.util.RabbitTestContainerUtil.Companion.getQueuesInfo +import com.exactpro.th2.common.util.RabbitTestContainerUtil.Companion.putMessageInQueue +import com.exactpro.th2.common.util.RabbitTestContainerUtil.Companion.restartContainer import com.rabbitmq.client.BuiltinExchangeType import java.time.Duration import java.util.concurrent.ArrayBlockingQueue @@ -32,6 +34,7 @@ import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import org.testcontainers.containers.RabbitMQContainer import org.testcontainers.utility.DockerImageName +import org.testcontainers.utility.MountableFile private val LOGGER = KotlinLogging.logger { } @@ -155,13 +158,19 @@ class TestConnectionManager { LOGGER.info { "creating the queue..." } RabbitTestContainerUtil.declareQueue(it, "wrong-queue") - LOGGER.info { RabbitTestContainerUtil.putMessageInQueue(it, "wrong-queue") } + LOGGER.info { + "Adding message to the queue: \n" + putMessageInQueue( + it, + "wrong-queue" + ) + } LOGGER.info { "queues list: \n ${it.execInContainer("rabbitmqctl", "list_queues")}" } - Thread.sleep(500) - // todo check isReady and isAlive, it should be false at some point - Assertions.assertEquals(1, counter.get()) + Assertions.assertEquals( + 1, + counter.get() + ) { "Unexpected number of messages received. The message should be received" } Assertions.assertTrue(connectionManager.isAlive) Assertions.assertTrue(connectionManager.isReady) } @@ -198,92 +207,180 @@ class TestConnectionManager { ), ).use { connectionManager -> Thread { - connectionManager.basicConsume(queueName, { _, delivery, ack -> + connectionManager.basicConsume(queueName, { _, delivery, _ -> counter.incrementAndGet() - LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from ${delivery.envelope.routingKey}" } + LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from \"${delivery.envelope.routingKey}\"" } }) { LOGGER.info { "Canceled $it" } } }.start() - Thread.sleep(5000) - - LOGGER.info { "Starting publishing..." } + LOGGER.info { "Starting first publishing..." } connectionManager.basicPublish(exchange, "", null, "Hello1".toByteArray(Charsets.UTF_8)) Thread.sleep(1000) LOGGER.info { "Publication finished!" } + Assertions.assertEquals( + 0, + counter.get() + ) { "Unexpected number of messages received. The first message shouldn't be received" } + Thread.sleep(1000) + LOGGER.info { "Creating the correct exchange..." } RabbitTestContainerUtil.declareFanoutExchangeWithBinding(it, exchange, queueName) Thread.sleep(1000) - Assertions.assertThrows(AlreadyClosedException::class.java) { - //todo there should be retry + LOGGER.info { "Exchange created!" } + + Assertions.assertDoesNotThrow { connectionManager.basicPublish(exchange, "", null, "Hello2".toByteArray(Charsets.UTF_8)) } - Assertions.assertEquals(0, counter.get()) + + Thread.sleep(500) + Assertions.assertEquals( + 1, + counter.get() + ) { "Unexpected number of messages received. The second message should be received" } + + } + } + } + + @Test + fun `connection manager handles ack timeout`() { + val queueName = "queue" + val prefetchCount = 10 + RabbitMQContainer(DockerImageName.parse("rabbitmq:3.8-management-alpine")) + .withRabbitMQConfig(MountableFile.forClasspathResource("rabbitmq_it.conf")) + .withQueue(queueName) + .use { + it.start() + LOGGER.info { "Started with port ${it.amqpPort}" } + val confirmationTimeout = Duration.ofSeconds(1) + val counter = AtomicInteger(0) + ConnectionManager( + RabbitMQConfiguration( + host = it.host, + vHost = "", + port = it.amqpPort, + username = it.adminUsername, + password = it.adminPassword, + ), + ConnectionManagerConfiguration( + subscriberName = "test", + prefetchCount = prefetchCount, + confirmationTimeout = confirmationTimeout, + minConnectionRecoveryTimeout = 100, + maxConnectionRecoveryTimeout = 200, + maxRecoveryAttempts = 5 + ), + ).use { connectionManager -> + Thread { + connectionManager.basicConsume(queueName, { _, delivery, ack -> + LOGGER.info { "Received 1 ${delivery.body.toString(Charsets.UTF_8)} from \"${delivery.envelope.routingKey}\"" } + if (counter.get() != 0) { + ack.confirm() + LOGGER.info { "Confirmed!" } + } else { + LOGGER.info { "Left this message unacked" } + } + counter.incrementAndGet() + }) { + LOGGER.info { "Canceled $it" } + } + }.start() + + LOGGER.info { "Sending first message" } + putMessageInQueue(it, queueName) + + LOGGER.info { "queues list: \n ${getQueuesInfo(it)}" } + LOGGER.info { "Sleeping..." } + Thread.sleep(63000) + + LOGGER.info { "Sending second message" } + putMessageInQueue(it, queueName) + + val queuesListExecResult = getQueuesInfo(it) + LOGGER.info { "queues list: \n $queuesListExecResult" } + + Assertions.assertEquals(3, counter.get()) { "Wrong number of received messages" } + Assertions.assertTrue( + queuesListExecResult.toString().contains("$queueName\t0") + ) { "There should be no messages left in the queue" } + + } + } + } + + @Test + fun `connection manager receives a messages after container restart`() { + val queueName = "queue" + val prefetchCount = 10 + val amqpPort = 5672 + + val container = object : RabbitMQContainer(DockerImageName.parse("rabbitmq:3.8-management-alpine")) { + fun addFixedPort(hostPort: Int, containerPort: Int) { + super.addFixedExposedPort(hostPort, containerPort) + } + } + + container + .addFixedPort(amqpPort, amqpPort) + container + .withQueue(queueName) + .use { + it.start() + LOGGER.info { "Started with port ${it.amqpPort}" } + val counter = AtomicInteger(0) + val confirmationTimeout = Duration.ofSeconds(1) + ConnectionManager( + RabbitMQConfiguration( + host = it.host, + vHost = "", + port = amqpPort, + username = it.adminUsername, + password = it.adminPassword, + ), + ConnectionManagerConfiguration( + subscriberName = "test", + prefetchCount = prefetchCount, + confirmationTimeout = confirmationTimeout, + minConnectionRecoveryTimeout = 10000, + maxConnectionRecoveryTimeout = 20000, + connectionTimeout = 10000, + maxRecoveryAttempts = 5 + ), + ).use { connectionManager -> + + Thread { + connectionManager.basicConsume(queueName, { _, delivery, ack -> + LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from ${delivery.envelope.routingKey}" } + counter.incrementAndGet() + ack.confirm() + }) { + LOGGER.info { "Canceled $it" } + } + }.start() + LOGGER.info { "Rabbit address- ${it.host}:${it.amqpPort}" } + + LOGGER.info { "Restarting the container" } + restartContainer(it) + Thread.sleep(10000) + + LOGGER.info { "Rabbit address after restart - ${it.host}:${it.amqpPort}" } + LOGGER.info { getQueuesInfo(it) } + + LOGGER.info { "Starting publishing..." } + putMessageInQueue(it, queueName) + LOGGER.info { "Publication finished!" } + LOGGER.info { getQueuesInfo(it) } + + Assertions.assertEquals(1, counter.get()) { "Wrong number of received messages" } + Assertions.assertTrue( + getQueuesInfo(it).toString().contains("$queueName\t0") + ) { "There should be no messages left in the queue" } } } } } -// @Test -// @Disabled -// fun `connection manager receives a messages after container restart`() { -// val routingKey = "routingKey" -// val queueName = "queue" -// val exchange = "test-exchange" -// val prefetchCount = 10 -// RabbitMQContainer(DockerImageName.parse("rabbitmq:3.8-management-alpine")) -//// .withExchange(exchange, BuiltinExchangeType.FANOUT.type, false, false, true, emptyMap()) -// .withQueue(queueName) -//// .withBinding(exchange, queueName, emptyMap(), routingKey, "queue") -// .use { -// it.start() -// LOGGER.info { "Started with port ${it.amqpPort}" } -// val counter = AtomicInteger(0) -// val confirmationTimeout = Duration.ofSeconds(1) -// ConnectionManager( -// RabbitMQConfiguration( -// host = it.host, -// vHost = "", -// port = it.amqpPort, -// username = it.adminUsername, -// password = it.adminPassword, -// ), -// ConnectionManagerConfiguration( -// subscriberName = "test", -// prefetchCount = prefetchCount, -// confirmationTimeout = confirmationTimeout, -// minConnectionRecoveryTimeout = 100, -// maxConnectionRecoveryTimeout = 200, -// maxRecoveryAttempts = 5 -// ), -// ).use { connectionManager -> -// Thread { -// connectionManager.basicConsume(queueName, { _, delivery, ack -> -// LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from ${delivery.envelope.routingKey}" } -// counter.incrementAndGet() -// }) { -// LOGGER.info { "Canceled $it" } -// } -// }.start() -// -// LOGGER.info { it.host + " " + it.httpPort + " " + it.amqpPort } -// -// -// LOGGER.info { it.host + " " + it.httpPort + " " + it.amqpPort } -// Thread.sleep(10000) -// -// LOGGER.info { "Starting publishing..." } -// RabbitTestContainerUtil.putMessageInQueue(it, queueName) -// LOGGER.info { "Publication finished!" } -// -// Assertions.assertEquals(1, counter.get()) -// Thread.sleep(1000) -// -// } -// } -// } -//} -// diff --git a/src/test/kotlin/com/exactpro/th2/common/util/RabbitTestContainerUtil.kt b/src/test/kotlin/com/exactpro/th2/common/util/RabbitTestContainerUtil.kt index d54577c20..24ace57a8 100644 --- a/src/test/kotlin/com/exactpro/th2/common/util/RabbitTestContainerUtil.kt +++ b/src/test/kotlin/com/exactpro/th2/common/util/RabbitTestContainerUtil.kt @@ -15,6 +15,7 @@ package com.exactpro.th2.common.util +import kotlin.random.Random import org.testcontainers.containers.Container import org.testcontainers.containers.RabbitMQContainer @@ -25,6 +26,10 @@ class RabbitTestContainerUtil { } + fun removeQueue(rabbit: RabbitMQContainer, queueName: String): Container.ExecResult? { + return execCommandWithSplit(rabbit, "rabbitmqadmin delete queue name=$queueName") + } + fun declareFanoutExchangeWithBinding( rabbit: RabbitMQContainer, exchangeName: String, @@ -33,17 +38,36 @@ class RabbitTestContainerUtil { execCommandWithSplit(rabbit, "rabbitmqadmin declare exchange name=$exchangeName type=fanout") execCommandWithSplit( rabbit, - """rabbitmqadmin declare binding source="$exchangeName" destination_type="queue" destination="$destinationQueue"""" + "rabbitmqadmin declare binding source=$exchangeName destination_type=queue destination=$destinationQueue" ) } fun putMessageInQueue(rabbit: RabbitMQContainer, queueName: String): Container.ExecResult? { return execCommandWithSplit( rabbit, - """rabbitmqadmin publish exchange=amq.default routing_key=$queueName payload="hello"""" + """rabbitmqadmin publish exchange=amq.default routing_key=$queueName payload="hello-${ + Random.nextInt( + 0, + 1000 + ) + }"""" ) } + fun getQueuesInfo(rabbit: RabbitMQContainer): Container.ExecResult? { + return execCommandWithSplit(rabbit, "rabbitmqctl list_queues") + } + + fun restartContainer(rabbit: RabbitMQContainer) { + val tag: String = rabbit.containerId + val snapshotId: String = rabbit.dockerClient.commitCmd(tag) + .withRepository("temp-rabbit") + .withTag(tag).exec() + rabbit.stop() + rabbit.dockerImageName = snapshotId + rabbit.start() + } + private fun execCommandWithSplit(rabbit: RabbitMQContainer, command: String): Container.ExecResult? { return rabbit.execInContainer( *command.split(" ").toTypedArray() diff --git a/src/test/resources/rabbitmq_it.conf b/src/test/resources/rabbitmq_it.conf new file mode 100644 index 000000000..e6ef5a8e0 --- /dev/null +++ b/src/test/resources/rabbitmq_it.conf @@ -0,0 +1,2 @@ +consumer_timeout = 50000 +loopback_users.guest = false \ No newline at end of file From c22cef65ee21c2fdf2ef80382b30322ff1b7f41c Mon Sep 17 00:00:00 2001 From: "fiodar.rekish" Date: Fri, 11 Nov 2022 16:12:33 +0400 Subject: [PATCH 13/51] [TH2-2212] publish-consume test, refactoring --- .../connection/TestConnectionManager.kt | 100 +++++++++++++++--- 1 file changed, 83 insertions(+), 17 deletions(-) diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt index fa86a3344..7cf994af8 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt @@ -19,7 +19,8 @@ import com.exactpro.th2.common.annotations.IntegrationTest import com.exactpro.th2.common.schema.message.ManualAckDeliveryCallback import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.ConnectionManagerConfiguration import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.RabbitMQConfiguration -import com.exactpro.th2.common.util.RabbitTestContainerUtil +import com.exactpro.th2.common.util.RabbitTestContainerUtil.Companion.declareFanoutExchangeWithBinding +import com.exactpro.th2.common.util.RabbitTestContainerUtil.Companion.declareQueue import com.exactpro.th2.common.util.RabbitTestContainerUtil.Companion.getQueuesInfo import com.exactpro.th2.common.util.RabbitTestContainerUtil.Companion.putMessageInQueue import com.exactpro.th2.common.util.RabbitTestContainerUtil.Companion.restartContainer @@ -42,13 +43,15 @@ private val LOGGER = KotlinLogging.logger { } @IntegrationTest class TestConnectionManager { + private val RABBIT_IMAGE_NAME = "rabbitmq:3.8-management-alpine" + @Test fun `connection manager reports unacked messages when confirmation timeout elapsed`() { val routingKey = "routingKey" val queueName = "queue" val exchange = "test-exchange" val prefetchCount = 10 - RabbitMQContainer(DockerImageName.parse("rabbitmq:3.8-management-alpine")) + RabbitMQContainer(DockerImageName.parse(RABBIT_IMAGE_NAME)) .withExchange(exchange, BuiltinExchangeType.FANOUT.type, false, false, true, emptyMap()) .withQueue(queueName) .withBinding(exchange, queueName, emptyMap(), routingKey, "queue") @@ -117,8 +120,9 @@ class TestConnectionManager { val routingKey = "routingKey" val queueName = "queue" val exchange = "test-exchange" + val wrongQueue = "wrong-queue" val prefetchCount = 10 - RabbitMQContainer(DockerImageName.parse("rabbitmq:3.8-management-alpine")) + RabbitMQContainer(DockerImageName.parse(RABBIT_IMAGE_NAME)) .withExchange(exchange, BuiltinExchangeType.FANOUT.type, false, false, true, emptyMap()) .withQueue(queueName) .withBinding(exchange, queueName, emptyMap(), routingKey, "queue") @@ -145,7 +149,7 @@ class TestConnectionManager { ), ).use { connectionManager -> Thread { - connectionManager.basicConsume("wrong-queue", { _, delivery, ack -> + connectionManager.basicConsume(wrongQueue, { _, delivery, ack -> LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from ${delivery.envelope.routingKey}" } counter.incrementAndGet() ack.confirm() @@ -154,14 +158,14 @@ class TestConnectionManager { } }.start() - Thread.sleep(500) +// Thread.sleep(500) LOGGER.info { "creating the queue..." } - RabbitTestContainerUtil.declareQueue(it, "wrong-queue") + declareQueue(it, wrongQueue) LOGGER.info { "Adding message to the queue: \n" + putMessageInQueue( it, - "wrong-queue" + wrongQueue ) } LOGGER.info { "queues list: \n ${it.execInContainer("rabbitmqctl", "list_queues")}" } @@ -182,7 +186,7 @@ class TestConnectionManager { val queueName = "queue" val exchange = "test-exchange" val prefetchCount = 10 - RabbitMQContainer(DockerImageName.parse("rabbitmq:3.8-management-alpine")) + RabbitMQContainer(DockerImageName.parse(RABBIT_IMAGE_NAME)) .withQueue(queueName) .use { it.start() @@ -217,23 +221,23 @@ class TestConnectionManager { LOGGER.info { "Starting first publishing..." } connectionManager.basicPublish(exchange, "", null, "Hello1".toByteArray(Charsets.UTF_8)) - Thread.sleep(1000) + Thread.sleep(200) LOGGER.info { "Publication finished!" } Assertions.assertEquals( 0, counter.get() ) { "Unexpected number of messages received. The first message shouldn't be received" } - Thread.sleep(1000) + Thread.sleep(200) LOGGER.info { "Creating the correct exchange..." } - RabbitTestContainerUtil.declareFanoutExchangeWithBinding(it, exchange, queueName) - Thread.sleep(1000) + declareFanoutExchangeWithBinding(it, exchange, queueName) + Thread.sleep(200) LOGGER.info { "Exchange created!" } Assertions.assertDoesNotThrow { connectionManager.basicPublish(exchange, "", null, "Hello2".toByteArray(Charsets.UTF_8)) } - Thread.sleep(500) + Thread.sleep(200) Assertions.assertEquals( 1, counter.get() @@ -245,10 +249,12 @@ class TestConnectionManager { @Test fun `connection manager handles ack timeout`() { + val configFilename = "rabbitmq_it.conf" val queueName = "queue" val prefetchCount = 10 - RabbitMQContainer(DockerImageName.parse("rabbitmq:3.8-management-alpine")) - .withRabbitMQConfig(MountableFile.forClasspathResource("rabbitmq_it.conf")) + + RabbitMQContainer(DockerImageName.parse(RABBIT_IMAGE_NAME)) + .withRabbitMQConfig(MountableFile.forClasspathResource(configFilename)) .withQueue(queueName) .use { it.start() @@ -315,7 +321,7 @@ class TestConnectionManager { val prefetchCount = 10 val amqpPort = 5672 - val container = object : RabbitMQContainer(DockerImageName.parse("rabbitmq:3.8-management-alpine")) { + val container = object : RabbitMQContainer(DockerImageName.parse(RABBIT_IMAGE_NAME)) { fun addFixedPort(hostPort: Int, containerPort: Int) { super.addFixedExposedPort(hostPort, containerPort) } @@ -362,7 +368,7 @@ class TestConnectionManager { LOGGER.info { "Restarting the container" } restartContainer(it) - Thread.sleep(10000) + Thread.sleep(5000) LOGGER.info { "Rabbit address after restart - ${it.host}:${it.amqpPort}" } LOGGER.info { getQueuesInfo(it) } @@ -380,6 +386,66 @@ class TestConnectionManager { } } } + + @Test + fun `connection manager publish a message and receives it`() { + val queueName = "queue" + val prefetchCount = 10 + val exchange = "test-exchange" + val routingKey = "routingKey" + + RabbitMQContainer(DockerImageName.parse(RABBIT_IMAGE_NAME)) + .use { + it.start() + LOGGER.info { "Started with port ${it.amqpPort}" } + val counter = AtomicInteger(0) + val confirmationTimeout = Duration.ofSeconds(1) + ConnectionManager( + RabbitMQConfiguration( + host = it.host, + vHost = "", + port = it.amqpPort, + username = it.adminUsername, + password = it.adminPassword, + ), + ConnectionManagerConfiguration( + subscriberName = "test", + prefetchCount = prefetchCount, + confirmationTimeout = confirmationTimeout, + minConnectionRecoveryTimeout = 10000, + maxConnectionRecoveryTimeout = 20000, + connectionTimeout = 10000, + maxRecoveryAttempts = 5 + ), + ).use { connectionManager -> + + declareQueue(it, queueName) + declareFanoutExchangeWithBinding(it, exchange, queueName) + + connectionManager.basicPublish(exchange, routingKey, null, "Hello1".toByteArray(Charsets.UTF_8)) + + Thread.sleep(500) + Thread { + connectionManager.basicConsume(queueName, { _, delivery, ack -> + LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from ${delivery.envelope.routingKey}" } + counter.incrementAndGet() + ack.confirm() + }) { + LOGGER.info { "Canceled $it" } + } + }.start() + Thread.sleep(500) + + Assertions.assertEquals(1, counter.get()) { "Wrong number of received messages" } + Assertions.assertTrue( + getQueuesInfo(it).toString().contains("$queueName\t0") + ) { "There should be no messages left in the queue" } + + } + } + } + + } From 8be1815f5a6aeb071dfdbdcf8c5eacdc109bc01a Mon Sep 17 00:00:00 2001 From: "fiodar.rekish" Date: Mon, 14 Nov 2022 12:06:34 +0400 Subject: [PATCH 14/51] [TH2-2212] rabbit container reusing in the tests --- .../connection/TestConnectionManager.kt | 87 ++++++++++--------- 1 file changed, 48 insertions(+), 39 deletions(-) diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt index 7cf994af8..3af8a648f 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt @@ -24,14 +24,15 @@ import com.exactpro.th2.common.util.RabbitTestContainerUtil.Companion.declareQue import com.exactpro.th2.common.util.RabbitTestContainerUtil.Companion.getQueuesInfo import com.exactpro.th2.common.util.RabbitTestContainerUtil.Companion.putMessageInQueue import com.exactpro.th2.common.util.RabbitTestContainerUtil.Companion.restartContainer -import com.rabbitmq.client.BuiltinExchangeType import java.time.Duration import java.util.concurrent.ArrayBlockingQueue import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger import mu.KotlinLogging +import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test import org.testcontainers.containers.RabbitMQContainer import org.testcontainers.utility.DockerImageName @@ -43,20 +44,16 @@ private val LOGGER = KotlinLogging.logger { } @IntegrationTest class TestConnectionManager { - private val RABBIT_IMAGE_NAME = "rabbitmq:3.8-management-alpine" - @Test fun `connection manager reports unacked messages when confirmation timeout elapsed`() { - val routingKey = "routingKey" - val queueName = "queue" - val exchange = "test-exchange" + val routingKey = "routingKey1" + val queueName = "queue1" + val exchange = "test-exchange1" val prefetchCount = 10 - RabbitMQContainer(DockerImageName.parse(RABBIT_IMAGE_NAME)) - .withExchange(exchange, BuiltinExchangeType.FANOUT.type, false, false, true, emptyMap()) - .withQueue(queueName) - .withBinding(exchange, queueName, emptyMap(), routingKey, "queue") - .use { - it.start() + rabbit + .let { + declareQueue(rabbit, queueName) + declareFanoutExchangeWithBinding(rabbit, exchange, queueName) LOGGER.info { "Started with port ${it.amqpPort}" } val queue = ArrayBlockingQueue(prefetchCount) val countDown = CountDownLatch(prefetchCount) @@ -117,17 +114,14 @@ class TestConnectionManager { @Test fun `connection manager receives a message from a queue that did not exist at the time of subscription`() { - val routingKey = "routingKey" - val queueName = "queue" - val exchange = "test-exchange" - val wrongQueue = "wrong-queue" + val queueName = "queue2" + val exchange = "test-exchange2" + val wrongQueue = "wrong-queue2" val prefetchCount = 10 - RabbitMQContainer(DockerImageName.parse(RABBIT_IMAGE_NAME)) - .withExchange(exchange, BuiltinExchangeType.FANOUT.type, false, false, true, emptyMap()) - .withQueue(queueName) - .withBinding(exchange, queueName, emptyMap(), routingKey, "queue") - .use { - it.start() + rabbit + .let { + declareQueue(rabbit, queueName) + declareFanoutExchangeWithBinding(rabbit, exchange, queueName) LOGGER.info { "Started with port ${it.amqpPort}" } val counter = AtomicInteger(0) val confirmationTimeout = Duration.ofSeconds(1) @@ -158,8 +152,6 @@ class TestConnectionManager { } }.start() -// Thread.sleep(500) - LOGGER.info { "creating the queue..." } declareQueue(it, wrongQueue) LOGGER.info { @@ -183,13 +175,12 @@ class TestConnectionManager { @Test fun `connection manager sends a message to wrong exchange`() { - val queueName = "queue" - val exchange = "test-exchange" + val queueName = "queue3" + val exchange = "test-exchange3" val prefetchCount = 10 - RabbitMQContainer(DockerImageName.parse(RABBIT_IMAGE_NAME)) - .withQueue(queueName) - .use { - it.start() + rabbit + .let { + declareQueue(rabbit, queueName) LOGGER.info { "Started with port ${it.amqpPort}" } val confirmationTimeout = Duration.ofSeconds(1) val counter = AtomicInteger(0) @@ -250,7 +241,7 @@ class TestConnectionManager { @Test fun `connection manager handles ack timeout`() { val configFilename = "rabbitmq_it.conf" - val queueName = "queue" + val queueName = "queue4" val prefetchCount = 10 RabbitMQContainer(DockerImageName.parse(RABBIT_IMAGE_NAME)) @@ -317,10 +308,9 @@ class TestConnectionManager { @Test fun `connection manager receives a messages after container restart`() { - val queueName = "queue" + val queueName = "queue5" val prefetchCount = 10 val amqpPort = 5672 - val container = object : RabbitMQContainer(DockerImageName.parse(RABBIT_IMAGE_NAME)) { fun addFixedPort(hostPort: Int, containerPort: Int) { super.addFixedExposedPort(hostPort, containerPort) @@ -389,14 +379,13 @@ class TestConnectionManager { @Test fun `connection manager publish a message and receives it`() { - val queueName = "queue" + val queueName = "queue6" val prefetchCount = 10 - val exchange = "test-exchange" - val routingKey = "routingKey" + val exchange = "test-exchange6" + val routingKey = "routingKey6" - RabbitMQContainer(DockerImageName.parse(RABBIT_IMAGE_NAME)) - .use { - it.start() + rabbit + .let { LOGGER.info { "Started with port ${it.amqpPort}" } val counter = AtomicInteger(0) val confirmationTimeout = Duration.ofSeconds(1) @@ -445,6 +434,26 @@ class TestConnectionManager { } } + companion object { + + private const val RABBIT_IMAGE_NAME = "rabbitmq:3.8-management-alpine" + private lateinit var rabbit: RabbitMQContainer + + @BeforeAll + @JvmStatic + fun initRabbit() { + rabbit = + RabbitMQContainer(DockerImageName.parse(RABBIT_IMAGE_NAME)) + rabbit.start() + } + + @AfterAll + @JvmStatic + fun closeRabbit() { + rabbit.close() + } + } + } From aaf58d1b79c047a34a95cc679c39bb76bdef5d78 Mon Sep 17 00:00:00 2001 From: "fiodar.rekish" Date: Mon, 14 Nov 2022 15:59:24 +0400 Subject: [PATCH 15/51] [TH2-2212] added test for ack timeout among several channels --- .../connection/ConnectionManager.java | 12 +- .../connection/TestConnectionManager.kt | 105 ++++++++++++++++++ 2 files changed, 110 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java index 5aaf75ba5..13ae5bb65 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java @@ -263,7 +263,9 @@ private void addShutdownListenerToChannel(Channel channel) { if (castedReason.getReplyCode() != 200) { StringBuilder errorBuilder = new StringBuilder("RabbitMQ soft error occupied: "); castedReason.appendArgumentDebugStringTo(errorBuilder); - errorBuilder.append(" on channel "); + errorBuilder.append(" on channel with hash ") + .append(channel.hashCode()) + .append(" "); errorBuilder.append(channelCause); String errorString = errorBuilder.toString(); LOGGER.warn(errorString); @@ -281,7 +283,7 @@ private void recoverSubscriptionsOfChannel(Channel channel) { channelsByPin .entrySet() .stream() - .filter(entry -> channel.equals(entry.getValue().channel)) + .filter(entry -> Objects.nonNull(entry.getValue().channel) && channel.getChannelNumber() == entry.getValue().channel.getChannelNumber()) .collect(Collectors.toList()); for (Map.Entry mapEntry : mapEntries) { PinId pinId = mapEntry.getKey(); @@ -293,11 +295,7 @@ private void recoverSubscriptionsOfChannel(Channel channel) { LOGGER.info("Changing channel for holder with pin id: " + pinId.toString()); channelsByPin.remove(pinId); -// mapEntry.getValue().lock.lock(); -// var newChannel = getNewChannelFor(pinId); -// mapEntry.setValue(newChannel); -// mapEntry.getValue().lock.unlock(); - channel.abort(); // todo ????????? + channel.abort(400, "Aborted because of the recovery"); basicConsume(pinId.queue, subscriptionCallbacks.deliverCallback, subscriptionCallbacks.cancelCallback); } catch (IOException | InterruptedException e) { throw new RuntimeException(e); diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt index 3af8a648f..d720ac159 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt @@ -306,6 +306,111 @@ class TestConnectionManager { } } + @Test + fun `connection manager handles ack timeout on a single channel`() { + val configFilename = "rabbitmq_it.conf" + val queueNames = arrayOf("separate_queues1", "separate_queues2", "separate_queues3") + val prefetchCount = 10 + + RabbitMQContainer(DockerImageName.parse(RABBIT_IMAGE_NAME)) + .withRabbitMQConfig(MountableFile.forClasspathResource(configFilename)) + .withQueue(queueNames[0]) + .withQueue(queueNames[1]) + .withQueue(queueNames[2]) + .use { + it.start() + LOGGER.info { "Started with port ${it.amqpPort}" } + val confirmationTimeout = Duration.ofSeconds(1) + val counters = mapOf( + queueNames[0] to AtomicInteger(), // this subscriber won't ack the first delivery + queueNames[1] to AtomicInteger(1), + queueNames[2] to AtomicInteger(1) + ) + ConnectionManager( + RabbitMQConfiguration( + host = it.host, + vHost = "", + port = it.amqpPort, + username = it.adminUsername, + password = it.adminPassword, + ), + ConnectionManagerConfiguration( + subscriberName = "test", + prefetchCount = prefetchCount, + confirmationTimeout = confirmationTimeout, + minConnectionRecoveryTimeout = 100, + maxConnectionRecoveryTimeout = 200, + maxRecoveryAttempts = 5 + ), + ).use { connectionManager -> + + fun createThreadAndSubscribe( + queue: String + ) { + Thread { + connectionManager.basicConsume(queue, { _, delivery, ack -> + LOGGER.info { "Received from queue $queue ${delivery.body.toString(Charsets.UTF_8)}" } + if (counters[queue]!!.get() != 0) { + ack.confirm() + LOGGER.info { "Confirmed message form $queue" } + } else { + LOGGER.info { "Left this message from $queue unacked" } + } + counters[queue]!!.incrementAndGet() + }, { + LOGGER.info { "Canceled message form queue $queue" } + }) + }.start() + } + + createThreadAndSubscribe(queueNames[0]) + createThreadAndSubscribe(queueNames[1]) + createThreadAndSubscribe(queueNames[2]) + + LOGGER.info { "Sending the first message batch" } + putMessageInQueue(it, queueNames[0]) + putMessageInQueue(it, queueNames[1]) + putMessageInQueue(it, queueNames[2]) + + LOGGER.info { "queues list: \n ${getQueuesInfo(it)}" } + LOGGER.info { "Sleeping..." } + Thread.sleep(33000) + + LOGGER.info { "Sending the second message batch" } + putMessageInQueue(it, queueNames[0]) + putMessageInQueue(it, queueNames[1]) + putMessageInQueue(it, queueNames[2]) + + LOGGER.info { "Still sleeping. Waiting for PRECONDITION_FAILED..." } + Thread.sleep(30000) + + LOGGER.info { "Sending the third message batch" } + putMessageInQueue(it, queueNames[0]) + putMessageInQueue(it, queueNames[1]) + putMessageInQueue(it, queueNames[2]) + + val queuesListExecResult = getQueuesInfo(it) + LOGGER.info { "queues list: \n $queuesListExecResult" } + + for (queueName in queueNames) { + Assertions.assertTrue(queuesListExecResult.toString().contains("$queueName\t0")) + { "There should be no messages left in queue $queueName" } + } + + + // 0 + 1 failed ack + 2 successful ack + 1 ack of requeued message + Assertions.assertEquals(4, counters[queueNames[0]]!!.get()) + { "Wrong number of received messages from queue ${queueNames[0]}" } + // 1 + 3 successful ack + Assertions.assertEquals(4, counters[queueNames[1]]!!.get()) + { "Wrong number of received messages from queue ${queueNames[1]}" } + Assertions.assertEquals(4, counters[queueNames[2]]!!.get()) + { "Wrong number of received messages from queue ${queueNames[2]}" } + + } + } + } + @Test fun `connection manager receives a messages after container restart`() { val queueName = "queue5" From 1e9de98434e92a001e3c61b294dc40e6b167415e Mon Sep 17 00:00:00 2001 From: "fiodar.rekish" Date: Mon, 14 Nov 2022 16:15:01 +0400 Subject: [PATCH 16/51] [TH2-2212] added one more test --- .../connection/TestConnectionManager.kt | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt index d720ac159..887573f6b 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt @@ -24,6 +24,7 @@ import com.exactpro.th2.common.util.RabbitTestContainerUtil.Companion.declareQue import com.exactpro.th2.common.util.RabbitTestContainerUtil.Companion.getQueuesInfo import com.exactpro.th2.common.util.RabbitTestContainerUtil.Companion.putMessageInQueue import com.exactpro.th2.common.util.RabbitTestContainerUtil.Companion.restartContainer +import com.rabbitmq.client.BuiltinExchangeType import java.time.Duration import java.util.concurrent.ArrayBlockingQueue import java.util.concurrent.CountDownLatch @@ -539,6 +540,86 @@ class TestConnectionManager { } } + @Test + fun `connection manager handles ack timeout on queue with publishing by the manager`() { + val configFilename = "rabbitmq_it.conf" + val queueName = "queue7" + val exchange = "test-exchange7" + val routingKey = "routingKey7" + + val prefetchCount = 10 + + RabbitMQContainer(DockerImageName.parse(RABBIT_IMAGE_NAME)) + .withRabbitMQConfig(MountableFile.forClasspathResource(configFilename)) + .withExchange(exchange, BuiltinExchangeType.FANOUT.type, false, false, true, emptyMap()) + .withQueue(queueName) + .withBinding(exchange, queueName, emptyMap(), routingKey, "queue") + .use { + it.start() + LOGGER.info { "Started with port ${it.amqpPort}" } + val confirmationTimeout = Duration.ofSeconds(1) + val counter = AtomicInteger(0) + ConnectionManager( + RabbitMQConfiguration( + host = it.host, + vHost = "", + port = it.amqpPort, + username = it.adminUsername, + password = it.adminPassword, + ), + ConnectionManagerConfiguration( + subscriberName = "test", + prefetchCount = prefetchCount, + confirmationTimeout = confirmationTimeout, + minConnectionRecoveryTimeout = 100, + maxConnectionRecoveryTimeout = 200, + maxRecoveryAttempts = 5 + ), + ).use { connectionManager -> + Thread { + connectionManager.basicConsume(queueName, { _, delivery, ack -> + LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} " } + if (counter.get() != 0) { + ack.confirm() + LOGGER.info { "Confirmed!" } + } else { + LOGGER.info { "Left this message unacked" } + } + counter.incrementAndGet() + }) { + LOGGER.info { "Canceled $it" } + } + }.start() + + + LOGGER.info { "Sending the first message" } + connectionManager.basicPublish(exchange, routingKey, null, "Hello1".toByteArray(Charsets.UTF_8)) + + LOGGER.info { "queues list: \n ${getQueuesInfo(it)}" } + LOGGER.info { "Sleeping..." } + Thread.sleep(33000) + + + LOGGER.info { "Sending the second message" } + connectionManager.basicPublish(exchange, routingKey, null, "Hello2".toByteArray(Charsets.UTF_8)) + + Thread.sleep(30000) + + LOGGER.info { "Sending the third message" } + connectionManager.basicPublish(exchange, routingKey, null, "Hello3".toByteArray(Charsets.UTF_8)) + + val queuesListExecResult = getQueuesInfo(it) + LOGGER.info { "queues list: \n $queuesListExecResult" } + + Assertions.assertEquals(4, counter.get()) { "Wrong number of received messages" } + Assertions.assertTrue( + queuesListExecResult.toString().contains("$queueName\t0") + ) { "There should be no messages left in the queue" } + + } + } + } + companion object { private const val RABBIT_IMAGE_NAME = "rabbitmq:3.8-management-alpine" From 23bc5105764839f5968030bddc6111f714c21e85 Mon Sep 17 00:00:00 2001 From: "fiodar.rekish" Date: Tue, 15 Nov 2022 13:18:43 +0400 Subject: [PATCH 17/51] [TH2-2212] added more complexity to the several channels test --- .../rabbitmq/connection/TestConnectionManager.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt index 887573f6b..6b911be06 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt @@ -308,7 +308,7 @@ class TestConnectionManager { } @Test - fun `connection manager handles ack timeout on a single channel`() { + fun `connection manager handles ack timeout with several channels`() { val configFilename = "rabbitmq_it.conf" val queueNames = arrayOf("separate_queues1", "separate_queues2", "separate_queues3") val prefetchCount = 10 @@ -324,7 +324,7 @@ class TestConnectionManager { val confirmationTimeout = Duration.ofSeconds(1) val counters = mapOf( queueNames[0] to AtomicInteger(), // this subscriber won't ack the first delivery - queueNames[1] to AtomicInteger(1), + queueNames[1] to AtomicInteger(-1), // this subscriber won't ack two first deliveries queueNames[2] to AtomicInteger(1) ) ConnectionManager( @@ -351,7 +351,7 @@ class TestConnectionManager { Thread { connectionManager.basicConsume(queue, { _, delivery, ack -> LOGGER.info { "Received from queue $queue ${delivery.body.toString(Charsets.UTF_8)}" } - if (counters[queue]!!.get() != 0) { + if (counters[queue]!!.get() > 0) { ack.confirm() LOGGER.info { "Confirmed message form $queue" } } else { @@ -375,7 +375,7 @@ class TestConnectionManager { LOGGER.info { "queues list: \n ${getQueuesInfo(it)}" } LOGGER.info { "Sleeping..." } - Thread.sleep(33000) + Thread.sleep(30000) LOGGER.info { "Sending the second message batch" } putMessageInQueue(it, queueNames[0]) @@ -383,7 +383,7 @@ class TestConnectionManager { putMessageInQueue(it, queueNames[2]) LOGGER.info { "Still sleeping. Waiting for PRECONDITION_FAILED..." } - Thread.sleep(30000) + Thread.sleep(32000) LOGGER.info { "Sending the third message batch" } putMessageInQueue(it, queueNames[0]) @@ -402,7 +402,7 @@ class TestConnectionManager { // 0 + 1 failed ack + 2 successful ack + 1 ack of requeued message Assertions.assertEquals(4, counters[queueNames[0]]!!.get()) { "Wrong number of received messages from queue ${queueNames[0]}" } - // 1 + 3 successful ack + // -1 + 2 failed ack + 2 ack of requeued message + 1 successful ack Assertions.assertEquals(4, counters[queueNames[1]]!!.get()) { "Wrong number of received messages from queue ${queueNames[1]}" } Assertions.assertEquals(4, counters[queueNames[2]]!!.get()) From 3bd042c5e3c7f9011bee59677cf232197d2f6b6a Mon Sep 17 00:00:00 2001 From: "fiodar.rekish" Date: Wed, 16 Nov 2022 15:33:21 +0400 Subject: [PATCH 18/51] [TH2-2212] refactoring --- README.md | 4 +- gradle.properties | 2 +- .../rabbitmq/AbstractRabbitSubscriber.java | 1 + .../connection/ConnectionManager.java | 139 ++++++++---------- .../configuration/RabbitMQConfiguration.kt | 46 ++++++ 5 files changed, 115 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index 084bb926e..ec6416fcb 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# th2 common library (Java) (3.42.0) +# th2 common library (Java) (3.43.0) ## Usage @@ -311,7 +311,7 @@ dependencies { ## Release notes -### 3.42.0 +### 3.43.0 + Added retry in case of a RabbitMQ channel or connection error (when possible). + Added InterruptedException to basicConsume method signature. + Added additional logging for RabbitMQ errors. diff --git a/gradle.properties b/gradle.properties index d9abe0345..bb25870a8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ # limitations under the License. # -release_version=3.42.0 +release_version=3.43.0 description = 'th2 common library (Java)' diff --git a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitSubscriber.java b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitSubscriber.java index a4c873567..bf2723b4e 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitSubscriber.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitSubscriber.java @@ -150,6 +150,7 @@ public void start() throws Exception { throw new IllegalStateException("Can not start subscribe to queue = " + queue, e); } catch (InterruptedException e) { Thread.currentThread().interrupt(); + LOGGER.error("Interrupted exception while consuming from queue '{}'", queue); throw new IllegalStateException("Thread was interrupted while consuming", e); } } diff --git a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java index 13ae5bb65..2a666b799 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java @@ -21,6 +21,7 @@ import com.exactpro.th2.common.schema.message.impl.OnlyOnceConfirmation; import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.ConnectionManagerConfiguration; import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.RabbitMQConfiguration; +import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.RetryingDelay; import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.rabbitmq.client.AMQP.BasicProperties; import com.rabbitmq.client.BlockedListener; @@ -50,6 +51,7 @@ import javax.annotation.concurrent.GuardedBy; import java.io.IOException; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; @@ -58,7 +60,6 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; @@ -75,7 +76,6 @@ public class ConnectionManager implements AutoCloseable { private final Connection connection; private final Map channelsByPin = new ConcurrentHashMap<>(); - private final Map subscriptionBackupMap = new ConcurrentHashMap<>(); private final AtomicInteger connectionRecoveryAttempts = new AtomicInteger(0); private final AtomicBoolean connectionIsClosed = new AtomicBoolean(false); private final ConnectionManagerConfiguration configuration; @@ -200,7 +200,7 @@ private void turnOffReadiness(Throwable exception){ int deviationPercent = connectionManagerConfiguration.getRetryTimeDeviationPercent(); LOGGER.debug("Try to recovery connection to RabbitMQ. Count tries = {}", tmpCountTriesToRecovery); - int recoveryDelay = getRecoveryDelay(tmpCountTriesToRecovery, minTime, maxTime, maxRecoveryAttempts, deviationPercent); + int recoveryDelay = RetryingDelay.Companion.getRecoveryDelay(tmpCountTriesToRecovery, minTime, maxTime, maxRecoveryAttempts, deviationPercent); if (tmpCountTriesToRecovery >= maxRecoveryAttempts && metrics.getLivenessMonitor().isEnabled()) { LOGGER.debug("Set RabbitMQ liveness to false. Can't recover connection"); metrics.getLivenessMonitor().disable(); @@ -230,30 +230,8 @@ private void turnOffReadiness(Throwable exception){ addRecoveryListenerToConnection(this.connection); } - /** - * @param numberOfTries zero based - */ - private static int getRecoveryDelay(int numberOfTries, int minTime, int maxTime, int maxRecoveryAttempts, int deviationPercent) { - if (numberOfTries < maxRecoveryAttempts) { - return getRecoveryDelayWithIncrement(numberOfTries, minTime, maxTime, maxRecoveryAttempts); - } - return getRecoveryDelayWithDeviation(maxTime, deviationPercent); - } - - private static int getRecoveryDelayWithDeviation(int maxTime, int deviationPercent) { - int recoveryDelay; - int deviation = maxTime * deviationPercent / 100; - recoveryDelay = ThreadLocalRandom.current().nextInt(maxTime - deviation, maxTime + deviation + 1); - return recoveryDelay; - } - - - private static int getRecoveryDelayWithIncrement(int numberOfTries, int minTime, int maxTime, int maxRecoveryAttempts) { - return minTime + (((maxTime - minTime) / maxRecoveryAttempts) * numberOfTries); - } - - private void addShutdownListenerToChannel(Channel channel) { - channel.addShutdownListener(cause -> new Thread(() -> { + private void addShutdownListenerToChannel(Channel channel, Boolean withRecovery) { + channel.addShutdownListener(cause -> { LOGGER.debug("Closing the channel: ", cause); if (!cause.isHardError() && cause.getReference() instanceof Channel) { Channel channelCause = (Channel) cause.getReference(); @@ -263,45 +241,44 @@ private void addShutdownListenerToChannel(Channel channel) { if (castedReason.getReplyCode() != 200) { StringBuilder errorBuilder = new StringBuilder("RabbitMQ soft error occupied: "); castedReason.appendArgumentDebugStringTo(errorBuilder); - errorBuilder.append(" on channel with hash ") - .append(channel.hashCode()) - .append(" "); + errorBuilder.append(" on channel "); errorBuilder.append(channelCause); String errorString = errorBuilder.toString(); LOGGER.warn(errorString); - if (errorString.contains("PRECONDITION_FAILED")) { + if (withRecovery && errorString.contains("PRECONDITION_FAILED")) { recoverSubscriptionsOfChannel(channel); } } } } - }).start()); + }); } private void recoverSubscriptionsOfChannel(Channel channel) { - var mapEntries = - channelsByPin - .entrySet() - .stream() - .filter(entry -> Objects.nonNull(entry.getValue().channel) && channel.getChannelNumber() == entry.getValue().channel.getChannelNumber()) - .collect(Collectors.toList()); - for (Map.Entry mapEntry : mapEntries) { - PinId pinId = mapEntry.getKey(); - SubscriptionCallbacks subscriptionCallbacks = subscriptionBackupMap.get(pinId); - if (subscriptionCallbacks != null) { - - - try { - LOGGER.info("Changing channel for holder with pin id: " + pinId.toString()); - - channelsByPin.remove(pinId); - channel.abort(400, "Aborted because of the recovery"); - basicConsume(pinId.queue, subscriptionCallbacks.deliverCallback, subscriptionCallbacks.cancelCallback); - } catch (IOException | InterruptedException e) { - throw new RuntimeException(e); + channelChecker.execute(() -> { + var mapEntries = + channelsByPin + .entrySet() + .stream() + .filter(entry -> Objects.nonNull(entry.getValue().channel) && channel.getChannelNumber() == entry.getValue().channel.getChannelNumber()) + .collect(Collectors.toList()); + for (Map.Entry mapEntry : mapEntries) { + PinId pinId = mapEntry.getKey(); + SubscriptionCallbacks subscriptionCallbacks = mapEntry.getValue().subscriptionCallbacks; + if (subscriptionCallbacks != null) { + + try { + LOGGER.info("Changing channel for holder with pin id: " + pinId.toString()); + + channelsByPin.remove(pinId); + channel.abort(400, "Aborted because of the recovery"); + basicConsume(pinId.queue, subscriptionCallbacks.deliverCallback, subscriptionCallbacks.cancelCallback); + } catch (IOException | InterruptedException e) { + throw new RuntimeException(e); + } } } - } + }); } private void addShutdownListenerToConnection(Connection conn) { @@ -383,8 +360,7 @@ public void basicPublish(String exchange, String routingKey, BasicProperties pro public SubscriberMonitor basicConsume(String queue, ManualAckDeliveryCallback deliverCallback, CancelCallback cancelCallback) throws IOException, InterruptedException { PinId pinId = PinId.forQueue(queue); - ChannelHolder holder = getChannelFor(pinId); - backupSubscription(pinId, deliverCallback, cancelCallback); + ChannelHolder holder = getChannelFor(pinId, new SubscriptionCallbacks(deliverCallback, cancelCallback)); String tag = holder.retryingConsumeWithLock(channel -> channel.basicConsume(queue, false, subscriberName + "_" + nextSubscriberId.getAndIncrement(), (tagTmp, delivery) -> { try { @@ -449,10 +425,6 @@ private void shutdownExecutor(ExecutorService executor, int closeTimeout, String } } - private void backupSubscription(PinId pinId, ManualAckDeliveryCallback deliverCallback, CancelCallback cancelCallback) { - subscriptionBackupMap.put(pinId, new SubscriptionCallbacks(deliverCallback, cancelCallback)); - } - private static final class SubscriptionCallbacks { private final ManualAckDeliveryCallback deliverCallback; private final CancelCallback cancelCallback; @@ -470,14 +442,25 @@ private ChannelHolder getChannelFor(PinId pinId) { }); } + private ChannelHolder getChannelFor(PinId pinId, SubscriptionCallbacks subscriptionCallbacks) { + return channelsByPin.computeIfAbsent(pinId, ignore -> { + LOGGER.trace("Creating channel holder with callbacks for {}", pinId); + return new ChannelHolder(() -> createChannelWithOptionalRecovery(true), this::waitForConnectionRecovery, configuration.getPrefetchCount(), subscriptionCallbacks); + }); + } + private Channel createChannel() { + return createChannelWithOptionalRecovery(false); + } + + private Channel createChannelWithOptionalRecovery(Boolean withRecovery) { waitForConnectionRecovery(connection); try { Channel channel = connection.createChannel(); Objects.requireNonNull(channel, () -> "No channels are available in the connection. Max channel number: " + connection.getChannelMax()); channel.basicQos(configuration.getPrefetchCount()); - addShutdownListenerToChannel(channel); + addShutdownListenerToChannel(channel, withRecovery); channel.addReturnListener(ret -> LOGGER.warn("Can not router message to exchange '{}', routing key '{}'. Reply code '{}' and text = {}", ret.getExchange(), ret.getRoutingKey(), ret.getReplyCode(), ret.getReplyText())); return channel; @@ -603,6 +586,7 @@ private static class ChannelHolder { private final Supplier supplier; private final BiConsumer reconnectionChecker; private final int maxCount; + private final SubscriptionCallbacks subscriptionCallbacks; @GuardedBy("lock") private int pending; @GuardedBy("lock") @@ -618,6 +602,19 @@ public ChannelHolder( this.supplier = Objects.requireNonNull(supplier, "'Supplier' parameter"); this.reconnectionChecker = Objects.requireNonNull(reconnectionChecker, "'Reconnection checker' parameter"); this.maxCount = maxCount; + this.subscriptionCallbacks = null; + } + public ChannelHolder( + Supplier supplier, + BiConsumer reconnectionChecker, + int maxCount, + SubscriptionCallbacks subscriptionCallbacks + + ) { + this.supplier = Objects.requireNonNull(supplier, "'Supplier' parameter"); + this.reconnectionChecker = Objects.requireNonNull(reconnectionChecker, "'Reconnection checker' parameter"); + this.maxCount = maxCount; + this.subscriptionCallbacks = subscriptionCallbacks; } public void withLock(ChannelConsumer consumer) throws IOException { @@ -643,21 +640,18 @@ public void withLock(boolean waitForRecovery, ChannelConsumer consumer) throws I } public void retryingPublishWithLock(ChannelConsumer consumer, ConnectionManagerConfiguration configuration) throws InterruptedException { + Iterator iterator = configuration.createRetryingDelaySequence().iterator(); lock.lock(); try { - int retryCount = 0; + var currentValue = iterator.next(); Channel tempChannel = getChannel(true); while (true) { try { consumer.consume(tempChannel); break; } catch (Exception e) { - int recoveryDelay = getRecoveryDelay(retryCount++, - configuration.getMinConnectionRecoveryTimeout(), - configuration.getMaxConnectionRecoveryTimeout(), - configuration.getMaxRecoveryAttempts(), - configuration.getRetryTimeDeviationPercent()); - LOGGER.warn("Retrying publishing #{}, waiting for {}ms, then recreating channel. Reason: {}", retryCount, recoveryDelay, e); + int recoveryDelay = currentValue.getDelay(); + LOGGER.warn("Retrying publishing #{}, waiting for {}ms, then recreating channel. Reason: {}", currentValue.getTryNumber(), recoveryDelay, e); TimeUnit.MILLISECONDS.sleep(recoveryDelay); tempChannel = recreateChannel(); } @@ -668,20 +662,17 @@ public void retryingPublishWithLock(ChannelConsumer consumer, ConnectionManagerC } public T retryingConsumeWithLock(ChannelMapper mapper, ConnectionManagerConfiguration configuration) throws InterruptedException { + Iterator iterator = configuration.createRetryingDelaySequence().iterator(); lock.lock(); try { - int retryCount = 0; Channel tempChannel = getChannel(); while (true) { + RetryingDelay currentValue = iterator.next(); try { return mapper.map(tempChannel); } catch (Exception e) { - int recoveryDelay = getRecoveryDelay(retryCount++, - configuration.getMinConnectionRecoveryTimeout(), - configuration.getMaxConnectionRecoveryTimeout(), - configuration.getMaxRecoveryAttempts(), - configuration.getRetryTimeDeviationPercent()); - LOGGER.warn("Retrying consume #{}, waiting for {}ms, then recreating channel. Reason: {}", retryCount, recoveryDelay, e); + int recoveryDelay = currentValue.getDelay(); + LOGGER.warn("Retrying consume #{}, waiting for {}ms, then recreating channel. Reason: {}", currentValue.getTryNumber(), recoveryDelay, e); TimeUnit.MILLISECONDS.sleep(recoveryDelay); tempChannel = recreateChannel(); } diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/configuration/RabbitMQConfiguration.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/configuration/RabbitMQConfiguration.kt index 17591f171..7eac817db 100644 --- a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/configuration/RabbitMQConfiguration.kt +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/configuration/RabbitMQConfiguration.kt @@ -18,6 +18,7 @@ package com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration import com.exactpro.th2.common.schema.configuration.Configuration import com.fasterxml.jackson.annotation.JsonProperty import java.time.Duration +import java.util.concurrent.ThreadLocalRandom data class RabbitMQConfiguration( @JsonProperty(required = true) var host: String, @@ -46,4 +47,49 @@ data class ConnectionManagerConfiguration( check(workingThreads > 0) { "expected 'workingThreads' greater than 0 but was $workingThreads" } check(!confirmationTimeout.run { isNegative || isZero }) { "expected 'confirmationTimeout' greater than 0 but was $confirmationTimeout" } } + + fun createRetryingDelaySequence(): Sequence { + return generateSequence(RetryingDelay(0, minConnectionRecoveryTimeout)) { + RetryingDelay(it.tryNumber + 1, RetryingDelay.getRecoveryDelay( + it.tryNumber + 1, + minConnectionRecoveryTimeout, + maxConnectionRecoveryTimeout, + maxRecoveryAttempts, + retryTimeDeviationPercent + )) + } + } + +} + +data class RetryingDelay(val tryNumber: Int, val delay: Int) { + companion object { + fun getRecoveryDelay( + numberOfTries: Int, + minTime: Int, + maxTime: Int, + maxRecoveryAttempts: Int, + deviationPercent: Int + ): Int { + return if (numberOfTries <= maxRecoveryAttempts) { + getRecoveryDelayWithIncrement(numberOfTries, minTime, maxTime, maxRecoveryAttempts) + } else getRecoveryDelayWithDeviation(maxTime, deviationPercent) + } + + private fun getRecoveryDelayWithDeviation(maxTime: Int, deviationPercent: Int): Int { + val recoveryDelay: Int + val deviation = maxTime * deviationPercent / 100 + recoveryDelay = ThreadLocalRandom.current().nextInt(maxTime - deviation, maxTime + deviation + 1) + return recoveryDelay + } + + private fun getRecoveryDelayWithIncrement( + numberOfTries: Int, + minTime: Int, + maxTime: Int, + maxRecoveryAttempts: Int + ): Int { + return minTime + (maxTime - minTime) / maxRecoveryAttempts * numberOfTries + } + } } \ No newline at end of file From 17dc5a58b7a61ca270a17aeabf7658767c849186 Mon Sep 17 00:00:00 2001 From: "fiodar.rekish" Date: Thu, 17 Nov 2022 18:08:52 +0400 Subject: [PATCH 19/51] [TH2-2212] refactor --- README.md | 4 +- gradle.properties | 2 +- .../connection/ConnectionManager.java | 81 +++--- .../connection/TestConnectionManager.kt | 264 ++++++++++++++++-- 4 files changed, 280 insertions(+), 71 deletions(-) diff --git a/README.md b/README.md index 1ebc440d2..115fad0f9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# th2 common library (Java) (3.43.0) +# th2 common library (Java) (3.42.1) ## Usage @@ -359,7 +359,7 @@ dependencies { ## Release notes -### 3.43.0 +### 3.42.1 + Added retry in case of a RabbitMQ channel or connection error (when possible). + Added InterruptedException to basicConsume method signature. + Added additional logging for RabbitMQ errors. diff --git a/gradle.properties b/gradle.properties index bb25870a8..ed2a9768c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ # limitations under the License. # -release_version=3.43.0 +release_version=3.42.1 description = 'th2 common library (Java)' diff --git a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java index 2a666b799..859648503 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java @@ -36,6 +36,7 @@ import com.rabbitmq.client.Recoverable; import com.rabbitmq.client.RecoveryListener; import com.rabbitmq.client.ShutdownNotifier; +import com.rabbitmq.client.ShutdownSignalException; import com.rabbitmq.client.TopologyRecoveryException; import com.rabbitmq.client.impl.AMQImpl; import com.rabbitmq.client.impl.recovery.AutorecoveringChannel; @@ -68,7 +69,6 @@ import java.util.concurrent.locks.ReentrantLock; import java.util.function.BiConsumer; import java.util.function.Supplier; -import java.util.stream.Collectors; public class ConnectionManager implements AutoCloseable { @@ -76,7 +76,6 @@ public class ConnectionManager implements AutoCloseable { private final Connection connection; private final Map channelsByPin = new ConcurrentHashMap<>(); - private final AtomicInteger connectionRecoveryAttempts = new AtomicInteger(0); private final AtomicBoolean connectionIsClosed = new AtomicBoolean(false); private final ConnectionManagerConfiguration configuration; private final String subscriberName; @@ -91,15 +90,15 @@ public class ConnectionManager implements AutoCloseable { private final RecoveryListener recoveryListener = new RecoveryListener() { @Override public void handleRecovery(Recoverable recoverable) { - LOGGER.debug("Count tries to recovery connection reset to 0"); - connectionRecoveryAttempts.set(0); metrics.getReadinessMonitor().enable(); - LOGGER.debug("Set RabbitMQ readiness to true"); + LOGGER.debug("Recovery finished. Set RabbitMQ readiness to true"); metrics.getLivenessMonitor().enable(); } @Override - public void handleRecoveryStarted(Recoverable recoverable) {} + public void handleRecoveryStarted(Recoverable recoverable) { + LOGGER.debug("Recovery started..."); + } }; public ConnectionManagerConfiguration getConfiguration() { @@ -193,20 +192,19 @@ private void turnOffReadiness(Throwable exception){ factory.setConnectionRecoveryTriggeringCondition(shutdownSignal -> !connectionIsClosed.get()); factory.setRecoveryDelayHandler(recoveryAttempts -> { - int tmpCountTriesToRecovery = connectionRecoveryAttempts.getAndIncrement(); int minTime = connectionManagerConfiguration.getMinConnectionRecoveryTimeout(); int maxTime = connectionManagerConfiguration.getMaxConnectionRecoveryTimeout(); int maxRecoveryAttempts = connectionManagerConfiguration.getMaxRecoveryAttempts(); int deviationPercent = connectionManagerConfiguration.getRetryTimeDeviationPercent(); - LOGGER.debug("Try to recovery connection to RabbitMQ. Count tries = {}", tmpCountTriesToRecovery); - int recoveryDelay = RetryingDelay.Companion.getRecoveryDelay(tmpCountTriesToRecovery, minTime, maxTime, maxRecoveryAttempts, deviationPercent); - if (tmpCountTriesToRecovery >= maxRecoveryAttempts && metrics.getLivenessMonitor().isEnabled()) { - LOGGER.debug("Set RabbitMQ liveness to false. Can't recover connection"); + LOGGER.debug("Try to recovery connection to RabbitMQ. Count tries = {}", recoveryAttempts); + int recoveryDelay = RetryingDelay.Companion.getRecoveryDelay(recoveryAttempts, minTime, maxTime, maxRecoveryAttempts, deviationPercent); + if (recoveryAttempts >= maxRecoveryAttempts && metrics.getLivenessMonitor().isEnabled()) { + LOGGER.info("Set RabbitMQ liveness to false. Can't recover connection"); metrics.getLivenessMonitor().disable(); } - LOGGER.info("Recovery delay for '{}' try = {}", tmpCountTriesToRecovery, recoveryDelay); + LOGGER.info("Recovery delay for '{}' try = {}", recoveryAttempts, recoveryDelay); return recoveryDelay; } ); @@ -218,6 +216,8 @@ private void turnOffReadiness(Throwable exception){ try { this.connection = factory.newConnection(); addShutdownListenerToConnection(this.connection); + addBlockedListenersToConnection(this.connection); + addRecoveryListenerToConnection(this.connection); metrics.getReadinessMonitor().enable(); LOGGER.debug("Set RabbitMQ readiness to true"); } catch (IOException | TimeoutException e) { @@ -225,9 +225,6 @@ private void turnOffReadiness(Throwable exception){ LOGGER.debug("Set RabbitMQ readiness to false. Can not create connection", e); throw new IllegalStateException("Failed to create RabbitMQ connection using configuration", e); } - - addBlockedListenersToConnection(this.connection); - addRecoveryListenerToConnection(this.connection); } private void addShutdownListenerToChannel(Channel channel, Boolean withRecovery) { @@ -254,29 +251,33 @@ private void addShutdownListenerToChannel(Channel channel, Boolean withRecovery) }); } + + private void recoverSubscriptionsOfChannel(Channel channel) { channelChecker.execute(() -> { - var mapEntries = - channelsByPin - .entrySet() - .stream() - .filter(entry -> Objects.nonNull(entry.getValue().channel) && channel.getChannelNumber() == entry.getValue().channel.getChannelNumber()) - .collect(Collectors.toList()); - for (Map.Entry mapEntry : mapEntries) { - PinId pinId = mapEntry.getKey(); - SubscriptionCallbacks subscriptionCallbacks = mapEntry.getValue().subscriptionCallbacks; - if (subscriptionCallbacks != null) { - - try { + try { + var pinToChannelHolderOptional = + channelsByPin + .entrySet() + .stream() + .filter(entry -> Objects.nonNull(entry.getValue().channel) && channel.getChannelNumber() == entry.getValue().channel.getChannelNumber()) + .findAny(); + if (pinToChannelHolderOptional.isPresent()) { + var pinIdToChannelHolder = pinToChannelHolderOptional.get(); + PinId pinId = pinIdToChannelHolder.getKey(); + ChannelHolder channelHolder = pinIdToChannelHolder.getValue(); + + SubscriptionCallbacks subscriptionCallbacks = channelHolder.subscriptionCallbacks; + if (subscriptionCallbacks != null) { LOGGER.info("Changing channel for holder with pin id: " + pinId.toString()); - channelsByPin.remove(pinId); channel.abort(400, "Aborted because of the recovery"); basicConsume(pinId.queue, subscriptionCallbacks.deliverCallback, subscriptionCallbacks.cancelCallback); - } catch (IOException | InterruptedException e) { - throw new RuntimeException(e); } } + } catch (IOException | InterruptedException e) { + LOGGER.warn("Exception during channel's subscriptions recovery", e); + throw new RuntimeException(e); } }); } @@ -354,13 +355,13 @@ public void close() { } public void basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body) throws InterruptedException { - ChannelHolder holder = getChannelFor(PinId.forRoutingKey(routingKey)); + ChannelHolder holder = getOrCreateChannelFor(PinId.forRoutingKey(routingKey)); holder.retryingPublishWithLock(channel -> channel.basicPublish(exchange, routingKey, props, body), configuration); } public SubscriberMonitor basicConsume(String queue, ManualAckDeliveryCallback deliverCallback, CancelCallback cancelCallback) throws IOException, InterruptedException { PinId pinId = PinId.forQueue(queue); - ChannelHolder holder = getChannelFor(pinId, new SubscriptionCallbacks(deliverCallback, cancelCallback)); + ChannelHolder holder = getOrCreateChannelFor(pinId, new SubscriptionCallbacks(deliverCallback, cancelCallback)); String tag = holder.retryingConsumeWithLock(channel -> channel.basicConsume(queue, false, subscriberName + "_" + nextSubscriberId.getAndIncrement(), (tagTmp, delivery) -> { try { @@ -372,8 +373,9 @@ public SubscriberMonitor basicConsume(String queue, ManualAckDeliveryCallback de Confirmation confirmation = OnlyOnceConfirmation.wrap("from " + routingKey + " to " + queue, () -> holder.withLock(ch -> { try { basicAck(ch, deliveryTag); - } catch (Exception e) { + } catch (IOException | ShutdownSignalException e) { LOGGER.warn("Error during basicAck of message with deliveryTag = {} inside channel #{}: {}", deliveryTag, ch.getChannelNumber(), e); + throw e; } finally { holder.release(() -> metrics.getReadinessMonitor().enable()); } @@ -435,14 +437,14 @@ public SubscriptionCallbacks(ManualAckDeliveryCallback deliverCallback, CancelCa } } - private ChannelHolder getChannelFor(PinId pinId) { + private ChannelHolder getOrCreateChannelFor(PinId pinId) { return channelsByPin.computeIfAbsent(pinId, ignore -> { LOGGER.trace("Creating channel holder for {}", pinId); return new ChannelHolder(this::createChannel, this::waitForConnectionRecovery, configuration.getPrefetchCount()); }); } - private ChannelHolder getChannelFor(PinId pinId, SubscriptionCallbacks subscriptionCallbacks) { + private ChannelHolder getOrCreateChannelFor(PinId pinId, SubscriptionCallbacks subscriptionCallbacks) { return channelsByPin.computeIfAbsent(pinId, ignore -> { LOGGER.trace("Creating channel holder with callbacks for {}", pinId); return new ChannelHolder(() -> createChannelWithOptionalRecovery(true), this::waitForConnectionRecovery, configuration.getPrefetchCount(), subscriptionCallbacks); @@ -528,7 +530,10 @@ public RabbitMqSubscriberMonitor(ChannelHolder holder, String tag, @Override public void unsubscribe() throws Exception { - holder.withLock(false, channel -> action.execute(channel, tag)); + holder.withLock(false, channel -> { + holder.subscriptionCallbacks = null; + action.execute(channel, tag); + }); } } @@ -586,7 +591,7 @@ private static class ChannelHolder { private final Supplier supplier; private final BiConsumer reconnectionChecker; private final int maxCount; - private final SubscriptionCallbacks subscriptionCallbacks; + private SubscriptionCallbacks subscriptionCallbacks; @GuardedBy("lock") private int pending; @GuardedBy("lock") @@ -649,7 +654,7 @@ public void retryingPublishWithLock(ChannelConsumer consumer, ConnectionManagerC try { consumer.consume(tempChannel); break; - } catch (Exception e) { + } catch (IOException | ShutdownSignalException e) { int recoveryDelay = currentValue.getDelay(); LOGGER.warn("Retrying publishing #{}, waiting for {}ms, then recreating channel. Reason: {}", currentValue.getTryNumber(), recoveryDelay, e); TimeUnit.MILLISECONDS.sleep(recoveryDelay); diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt index 6b911be06..269ebdc8e 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt @@ -30,6 +30,7 @@ import java.util.concurrent.ArrayBlockingQueue import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger +import kotlin.concurrent.thread import mu.KotlinLogging import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.Assertions @@ -115,14 +116,10 @@ class TestConnectionManager { @Test fun `connection manager receives a message from a queue that did not exist at the time of subscription`() { - val queueName = "queue2" - val exchange = "test-exchange2" val wrongQueue = "wrong-queue2" val prefetchCount = 10 rabbit .let { - declareQueue(rabbit, queueName) - declareFanoutExchangeWithBinding(rabbit, exchange, queueName) LOGGER.info { "Started with port ${it.amqpPort}" } val counter = AtomicInteger(0) val confirmationTimeout = Duration.ofSeconds(1) @@ -143,33 +140,41 @@ class TestConnectionManager { maxRecoveryAttempts = 5 ), ).use { connectionManager -> - Thread { - connectionManager.basicConsume(wrongQueue, { _, delivery, ack -> - LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from ${delivery.envelope.routingKey}" } - counter.incrementAndGet() - ack.confirm() - }) { - LOGGER.info { "Canceled $it" } + var thread: Thread? = null + try { + thread = thread { + connectionManager.basicConsume(wrongQueue, { _, delivery, ack -> + LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from ${delivery.envelope.routingKey}" } + counter.incrementAndGet() + ack.confirm() + }) { + LOGGER.info { "Canceled $it" } + } } - }.start() - LOGGER.info { "creating the queue..." } - declareQueue(it, wrongQueue) - LOGGER.info { - "Adding message to the queue: \n" + putMessageInQueue( - it, - wrongQueue - ) + LOGGER.info { "creating the queue..." } + declareQueue(it, wrongQueue) + LOGGER.info { + "Adding message to the queue: \n" + putMessageInQueue( + it, + wrongQueue + ) + } + LOGGER.info { "queues list: \n ${it.execInContainer("rabbitmqctl", "list_queues")}" } + + // todo check isReady and isAlive, it should be false at some point + Assertions.assertEquals( + 1, + counter.get() + ) { "Unexpected number of messages received. The message should be received" } + Assertions.assertTrue(connectionManager.isAlive) + Assertions.assertTrue(connectionManager.isReady) + } finally { + thread?.interrupt() + thread?.join(100) + // assert fail } - LOGGER.info { "queues list: \n ${it.execInContainer("rabbitmqctl", "list_queues")}" } - // todo check isReady and isAlive, it should be false at some point - Assertions.assertEquals( - 1, - counter.get() - ) { "Unexpected number of messages received. The message should be received" } - Assertions.assertTrue(connectionManager.isAlive) - Assertions.assertTrue(connectionManager.isReady) } } } @@ -444,9 +449,9 @@ class TestConnectionManager { subscriberName = "test", prefetchCount = prefetchCount, confirmationTimeout = confirmationTimeout, - minConnectionRecoveryTimeout = 10000, - maxConnectionRecoveryTimeout = 20000, - connectionTimeout = 10000, + minConnectionRecoveryTimeout = 1000, + maxConnectionRecoveryTimeout = 2000, + connectionTimeout = 1000, maxRecoveryAttempts = 5 ), ).use { connectionManager -> @@ -474,6 +479,9 @@ class TestConnectionManager { LOGGER.info { "Publication finished!" } LOGGER.info { getQueuesInfo(it) } + restartContainer(it) + Thread.sleep(5000) + Assertions.assertEquals(1, counter.get()) { "Wrong number of received messages" } Assertions.assertTrue( getQueuesInfo(it).toString().contains("$queueName\t0") @@ -620,6 +628,202 @@ class TestConnectionManager { } } + @Test + fun `thread interruption test`() { + val queueName = "queue8" + val prefetchCount = 10 + rabbit + .let { + LOGGER.info { "Started with port ${it.amqpPort}" } + val counter = AtomicInteger(0) + val confirmationTimeout = Duration.ofSeconds(1) + ConnectionManager( + RabbitMQConfiguration( + host = it.host, + vHost = "", + port = it.amqpPort, + username = it.adminUsername, + password = it.adminPassword, + ), + ConnectionManagerConfiguration( + subscriberName = "test", + prefetchCount = prefetchCount, + confirmationTimeout = confirmationTimeout, + minConnectionRecoveryTimeout = 100, + maxConnectionRecoveryTimeout = 2000, + connectionTimeout = 1000, + maxRecoveryAttempts = 5 + ), + ).use { connectionManager -> + +// declareQueue(it, queueName) +// declareFanoutExchangeWithBinding(it, exchange, queueName) +// +// connectionManager.basicPublish(exchange, routingKey, null, "Hello1".toByteArray(Charsets.UTF_8)) + + val thread = Thread { + // marker that thread is actually running + connectionManager.basicConsume(queueName, { _, delivery, ack -> + LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from ${delivery.envelope.routingKey}" } + counter.incrementAndGet() + ack.confirm() + }) { + LOGGER.info { "Canceled $it" } + } + } + thread.start() + Thread.sleep(2000) + LOGGER.info { "Interrupting..." } + thread.interrupt() + LOGGER.info { "Interrupted!" } + // todo try join + Thread.sleep(4000) + LOGGER.info { "Sleep done" } + +// todo assert thread.isAlive + +// Assertions.assertEquals(1, counter.get()) { "Wrong number of received messages" } +// Assertions.assertTrue( +// getQueuesInfo(it).toString().contains("$queueName\t0") +// ) { "There should be no messages left in the queue" } + + } + } + } + + @Test + fun `connection manager handles subscription cancel`() { + val queueName = "queue9" + val prefetchCount = 10 + rabbit + .let { + LOGGER.info { "Started with port ${it.amqpPort}" } + val counter = AtomicInteger(0) + val confirmationTimeout = Duration.ofSeconds(1) + ConnectionManager( + RabbitMQConfiguration( + host = it.host, + vHost = "", + port = it.amqpPort, + username = it.adminUsername, + password = it.adminPassword, + ), + ConnectionManagerConfiguration( + subscriberName = "test", + prefetchCount = prefetchCount, + confirmationTimeout = confirmationTimeout, + minConnectionRecoveryTimeout = 100, + maxConnectionRecoveryTimeout = 2000, + connectionTimeout = 1000, + maxRecoveryAttempts = 5 + ), + ).use { connectionManager -> + + declareQueue(it, queueName) + + val thread = Thread { + val subscriberMonitor = connectionManager.basicConsume(queueName, { _, delivery, ack -> + LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from ${delivery.envelope.routingKey}" } + counter.incrementAndGet() + ack.confirm() + }) { + LOGGER.info { "Canceled $it" } + } + + Thread.sleep(3500) + LOGGER.info { "Unsubscribing..." } + subscriberMonitor.unsubscribe() + } + thread.start() + for (i in 1..5) { + putMessageInQueue(it, queueName) + Thread.sleep(1000) + } + + Assertions.assertEquals(3, counter.get()) { "Wrong number of received messages" } + Assertions.assertTrue( + getQueuesInfo(it).toString().contains("$queueName\t2") + ) { "There should be messages in the queue" } + + } + } + } + + @Test + fun `connection manager handles ack timeout and subscription cancel`() { + val configFilename = "rabbitmq_it.conf" + val queueName = "queue" + val prefetchCount = 10 + + RabbitMQContainer(DockerImageName.parse(RABBIT_IMAGE_NAME)) + .withRabbitMQConfig(MountableFile.forClasspathResource(configFilename)) + .withQueue(queueName) + .use { + it.start() + LOGGER.info { "Started with port ${it.amqpPort}" } + val confirmationTimeout = Duration.ofSeconds(1) + val counter = AtomicInteger(0) + ConnectionManager( + RabbitMQConfiguration( + host = it.host, + vHost = "", + port = it.amqpPort, + username = it.adminUsername, + password = it.adminPassword, + ), + ConnectionManagerConfiguration( + subscriberName = "test", + prefetchCount = prefetchCount, + confirmationTimeout = confirmationTimeout, + minConnectionRecoveryTimeout = 100, + maxConnectionRecoveryTimeout = 200, + maxRecoveryAttempts = 5 + ), + ).use { connectionManager -> + Thread { + val subscriberMonitor = connectionManager.basicConsume(queueName, { _, delivery, ack -> + LOGGER.info { "Received 1 ${delivery.body.toString(Charsets.UTF_8)} from \"${delivery.envelope.routingKey}\"" } + if (counter.get() == 0) { + ack.confirm() + LOGGER.info { "Confirmed!" } + } else { + LOGGER.info { "Left this message unacked" } + } + counter.incrementAndGet() + }) { + LOGGER.info { "Canceled $it" } + } + + Thread.sleep(30000) + LOGGER.info { "Unsubscribing..." } + subscriberMonitor.unsubscribe() + }.start() + + LOGGER.info { "Sending first message" } + putMessageInQueue(it, queueName) + + LOGGER.info { "queues list: \n ${getQueuesInfo(it)}" } + + LOGGER.info { "Sending second message" } + putMessageInQueue(it, queueName) + LOGGER.info { "Sleeping..." } + Thread.sleep(63000) + + LOGGER.info { "queues list: \n ${getQueuesInfo(it)}" } + + + val queuesListExecResult = getQueuesInfo(it) + LOGGER.info { "queues list: \n $queuesListExecResult" } + + Assertions.assertEquals(2, counter.get()) { "Wrong number of received messages" } + Assertions.assertTrue( + queuesListExecResult.toString().contains("$queueName\t1") + ) { "There should a message left in the queue" } + + } + } + } + companion object { private const val RABBIT_IMAGE_NAME = "rabbitmq:3.8-management-alpine" From 0cbfb9b090e084132de00592d356f9f9eb19f610 Mon Sep 17 00:00:00 2001 From: "fiodar.rekish" Date: Fri, 18 Nov 2022 12:10:05 +0400 Subject: [PATCH 20/51] [TH2-2212] removed unnecessary threads in the tests --- .../connection/ConnectionManager.java | 7 +- .../connection/TestConnectionManager.kt | 137 ++++++++---------- 2 files changed, 64 insertions(+), 80 deletions(-) diff --git a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java index 859648503..6e3100ec5 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java @@ -515,7 +515,7 @@ private static void basicAck(Channel channel, long deliveryTag) throws IOExcepti channel.basicAck(deliveryTag, false); } - private static class RabbitMqSubscriberMonitor implements SubscriberMonitor { + private class RabbitMqSubscriberMonitor implements SubscriberMonitor { private final ChannelHolder holder; private final String tag; @@ -531,8 +531,9 @@ public RabbitMqSubscriberMonitor(ChannelHolder holder, String tag, @Override public void unsubscribe() throws Exception { holder.withLock(false, channel -> { - holder.subscriptionCallbacks = null; + channelsByPin.values().remove(holder); action.execute(channel, tag); + channel.abort(); }); } } @@ -591,7 +592,7 @@ private static class ChannelHolder { private final Supplier supplier; private final BiConsumer reconnectionChecker; private final int maxCount; - private SubscriptionCallbacks subscriptionCallbacks; + private final SubscriptionCallbacks subscriptionCallbacks; @GuardedBy("lock") private int pending; @GuardedBy("lock") diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt index 269ebdc8e..a2f81ede1 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt @@ -207,14 +207,12 @@ class TestConnectionManager { maxRecoveryAttempts = 5 ), ).use { connectionManager -> - Thread { - connectionManager.basicConsume(queueName, { _, delivery, _ -> - counter.incrementAndGet() - LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from \"${delivery.envelope.routingKey}\"" } - }) { - LOGGER.info { "Canceled $it" } - } - }.start() + connectionManager.basicConsume(queueName, { _, delivery, _ -> + counter.incrementAndGet() + LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from \"${delivery.envelope.routingKey}\"" } + }) { + LOGGER.info { "Canceled $it" } + } LOGGER.info { "Starting first publishing..." } connectionManager.basicPublish(exchange, "", null, "Hello1".toByteArray(Charsets.UTF_8)) @@ -275,20 +273,18 @@ class TestConnectionManager { maxRecoveryAttempts = 5 ), ).use { connectionManager -> - Thread { - connectionManager.basicConsume(queueName, { _, delivery, ack -> - LOGGER.info { "Received 1 ${delivery.body.toString(Charsets.UTF_8)} from \"${delivery.envelope.routingKey}\"" } - if (counter.get() != 0) { - ack.confirm() - LOGGER.info { "Confirmed!" } - } else { - LOGGER.info { "Left this message unacked" } - } - counter.incrementAndGet() - }) { - LOGGER.info { "Canceled $it" } + connectionManager.basicConsume(queueName, { _, delivery, ack -> + LOGGER.info { "Received 1 ${delivery.body.toString(Charsets.UTF_8)} from \"${delivery.envelope.routingKey}\"" } + if (counter.get() != 0) { + ack.confirm() + LOGGER.info { "Confirmed!" } + } else { + LOGGER.info { "Left this message unacked" } } - }.start() + counter.incrementAndGet() + }) { + LOGGER.info { "Canceled $it" } + } LOGGER.info { "Sending first message" } putMessageInQueue(it, queueName) @@ -350,28 +346,26 @@ class TestConnectionManager { ), ).use { connectionManager -> - fun createThreadAndSubscribe( + fun subscribeOnQueue( queue: String ) { - Thread { - connectionManager.basicConsume(queue, { _, delivery, ack -> - LOGGER.info { "Received from queue $queue ${delivery.body.toString(Charsets.UTF_8)}" } - if (counters[queue]!!.get() > 0) { - ack.confirm() - LOGGER.info { "Confirmed message form $queue" } - } else { - LOGGER.info { "Left this message from $queue unacked" } - } - counters[queue]!!.incrementAndGet() - }, { - LOGGER.info { "Canceled message form queue $queue" } - }) - }.start() + connectionManager.basicConsume(queue, { _, delivery, ack -> + LOGGER.info { "Received from queue $queue ${delivery.body.toString(Charsets.UTF_8)}" } + if (counters[queue]!!.get() > 0) { + ack.confirm() + LOGGER.info { "Confirmed message form $queue" } + } else { + LOGGER.info { "Left this message from $queue unacked" } + } + counters[queue]!!.incrementAndGet() + }, { + LOGGER.info { "Canceled message form queue $queue" } + }) } - createThreadAndSubscribe(queueNames[0]) - createThreadAndSubscribe(queueNames[1]) - createThreadAndSubscribe(queueNames[2]) + subscribeOnQueue(queueNames[0]) + subscribeOnQueue(queueNames[1]) + subscribeOnQueue(queueNames[2]) LOGGER.info { "Sending the first message batch" } putMessageInQueue(it, queueNames[0]) @@ -456,15 +450,13 @@ class TestConnectionManager { ), ).use { connectionManager -> - Thread { - connectionManager.basicConsume(queueName, { _, delivery, ack -> - LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from ${delivery.envelope.routingKey}" } - counter.incrementAndGet() - ack.confirm() - }) { - LOGGER.info { "Canceled $it" } - } - }.start() + connectionManager.basicConsume(queueName, { _, delivery, ack -> + LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from ${delivery.envelope.routingKey}" } + counter.incrementAndGet() + ack.confirm() + }) { + LOGGER.info { "Canceled $it" } + } LOGGER.info { "Rabbit address- ${it.host}:${it.amqpPort}" } LOGGER.info { "Restarting the container" } @@ -527,17 +519,15 @@ class TestConnectionManager { connectionManager.basicPublish(exchange, routingKey, null, "Hello1".toByteArray(Charsets.UTF_8)) - Thread.sleep(500) - Thread { - connectionManager.basicConsume(queueName, { _, delivery, ack -> - LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from ${delivery.envelope.routingKey}" } - counter.incrementAndGet() - ack.confirm() - }) { - LOGGER.info { "Canceled $it" } - } - }.start() - Thread.sleep(500) + Thread.sleep(200) + connectionManager.basicConsume(queueName, { _, delivery, ack -> + LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from ${delivery.envelope.routingKey}" } + counter.incrementAndGet() + ack.confirm() + }) { + LOGGER.info { "Canceled $it" } + } + Thread.sleep(200) Assertions.assertEquals(1, counter.get()) { "Wrong number of received messages" } Assertions.assertTrue( @@ -584,20 +574,18 @@ class TestConnectionManager { maxRecoveryAttempts = 5 ), ).use { connectionManager -> - Thread { - connectionManager.basicConsume(queueName, { _, delivery, ack -> - LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} " } - if (counter.get() != 0) { - ack.confirm() - LOGGER.info { "Confirmed!" } - } else { - LOGGER.info { "Left this message unacked" } - } - counter.incrementAndGet() - }) { - LOGGER.info { "Canceled $it" } + connectionManager.basicConsume(queueName, { _, delivery, ack -> + LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} " } + if (counter.get() != 0) { + ack.confirm() + LOGGER.info { "Confirmed!" } + } else { + LOGGER.info { "Left this message unacked" } } - }.start() + counter.incrementAndGet() + }) { + LOGGER.info { "Canceled $it" } + } LOGGER.info { "Sending the first message" } @@ -656,11 +644,6 @@ class TestConnectionManager { ), ).use { connectionManager -> -// declareQueue(it, queueName) -// declareFanoutExchangeWithBinding(it, exchange, queueName) -// -// connectionManager.basicPublish(exchange, routingKey, null, "Hello1".toByteArray(Charsets.UTF_8)) - val thread = Thread { // marker that thread is actually running connectionManager.basicConsume(queueName, { _, delivery, ack -> From 4b75b331cd81414d16d2054901468a18fe7719cd Mon Sep 17 00:00:00 2001 From: "fiodar.rekish" Date: Fri, 18 Nov 2022 12:47:47 +0400 Subject: [PATCH 21/51] [TH2-2212] threads handling --- .../connection/TestConnectionManager.kt | 301 ++++++++++-------- 1 file changed, 174 insertions(+), 127 deletions(-) diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt index a2f81ede1..a856cda5e 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt @@ -17,6 +17,7 @@ package com.exactpro.th2.common.schema.message.impl.rabbitmq.connection import com.exactpro.th2.common.annotations.IntegrationTest import com.exactpro.th2.common.schema.message.ManualAckDeliveryCallback +import com.exactpro.th2.common.schema.message.SubscriberMonitor import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.ConnectionManagerConfiguration import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.RabbitMQConfiguration import com.exactpro.th2.common.util.RabbitTestContainerUtil.Companion.declareFanoutExchangeWithBinding @@ -121,7 +122,7 @@ class TestConnectionManager { rabbit .let { LOGGER.info { "Started with port ${it.amqpPort}" } - val counter = AtomicInteger(0) + val counter = CountDownLatch(1) val confirmationTimeout = Duration.ofSeconds(1) ConnectionManager( RabbitMQConfiguration( @@ -141,11 +142,12 @@ class TestConnectionManager { ), ).use { connectionManager -> var thread: Thread? = null + var monitor: SubscriberMonitor? = null try { thread = thread { - connectionManager.basicConsume(wrongQueue, { _, delivery, ack -> + monitor = connectionManager.basicConsume(wrongQueue, { _, delivery, ack -> LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from ${delivery.envelope.routingKey}" } - counter.incrementAndGet() + counter.countDown() ack.confirm() }) { LOGGER.info { "Canceled $it" } @@ -164,15 +166,20 @@ class TestConnectionManager { // todo check isReady and isAlive, it should be false at some point Assertions.assertEquals( - 1, - counter.get() + 0, + counter.count ) { "Unexpected number of messages received. The message should be received" } Assertions.assertTrue(connectionManager.isAlive) Assertions.assertTrue(connectionManager.isReady) } finally { + counter.await() + Assertions.assertDoesNotThrow { + monitor?.unsubscribe() + } thread?.interrupt() thread?.join(100) - // assert fail + Assertions.assertNotNull(thread) + Assertions.assertFalse(thread!!.isAlive) } } @@ -207,36 +214,44 @@ class TestConnectionManager { maxRecoveryAttempts = 5 ), ).use { connectionManager -> - connectionManager.basicConsume(queueName, { _, delivery, _ -> - counter.incrementAndGet() - LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from \"${delivery.envelope.routingKey}\"" } - }) { - LOGGER.info { "Canceled $it" } - } + var monitor: SubscriberMonitor? = null + try { + monitor = connectionManager.basicConsume(queueName, { _, delivery, _ -> + counter.incrementAndGet() + LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from \"${delivery.envelope.routingKey}\"" } + }) { + LOGGER.info { "Canceled $it" } + } - LOGGER.info { "Starting first publishing..." } - connectionManager.basicPublish(exchange, "", null, "Hello1".toByteArray(Charsets.UTF_8)) - Thread.sleep(200) - LOGGER.info { "Publication finished!" } - Assertions.assertEquals( - 0, - counter.get() - ) { "Unexpected number of messages received. The first message shouldn't be received" } - Thread.sleep(200) - LOGGER.info { "Creating the correct exchange..." } - declareFanoutExchangeWithBinding(it, exchange, queueName) - Thread.sleep(200) - LOGGER.info { "Exchange created!" } - - Assertions.assertDoesNotThrow { - connectionManager.basicPublish(exchange, "", null, "Hello2".toByteArray(Charsets.UTF_8)) - } + LOGGER.info { "Starting first publishing..." } + connectionManager.basicPublish(exchange, "", null, "Hello1".toByteArray(Charsets.UTF_8)) + Thread.sleep(200) + LOGGER.info { "Publication finished!" } + Assertions.assertEquals( + 0, + counter.get() + ) { "Unexpected number of messages received. The first message shouldn't be received" } + Thread.sleep(200) + LOGGER.info { "Creating the correct exchange..." } + declareFanoutExchangeWithBinding(it, exchange, queueName) + Thread.sleep(200) + LOGGER.info { "Exchange created!" } + + Assertions.assertDoesNotThrow { + connectionManager.basicPublish(exchange, "", null, "Hello2".toByteArray(Charsets.UTF_8)) + } - Thread.sleep(200) - Assertions.assertEquals( - 1, - counter.get() - ) { "Unexpected number of messages received. The second message should be received" } + Thread.sleep(200) + Assertions.assertEquals( + 1, + counter.get() + ) { "Unexpected number of messages received. The second message should be received" } + } finally { + Assertions.assertNotNull(monitor) + Assertions.assertDoesNotThrow { + monitor!!.unsubscribe() + } + } } } @@ -513,26 +528,33 @@ class TestConnectionManager { maxRecoveryAttempts = 5 ), ).use { connectionManager -> + var monitor: SubscriberMonitor? = null + try { + declareQueue(it, queueName) + declareFanoutExchangeWithBinding(it, exchange, queueName) - declareQueue(it, queueName) - declareFanoutExchangeWithBinding(it, exchange, queueName) + connectionManager.basicPublish(exchange, routingKey, null, "Hello1".toByteArray(Charsets.UTF_8)) - connectionManager.basicPublish(exchange, routingKey, null, "Hello1".toByteArray(Charsets.UTF_8)) + Thread.sleep(200) + monitor = connectionManager.basicConsume(queueName, { _, delivery, ack -> + LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from ${delivery.envelope.routingKey}" } + counter.incrementAndGet() + ack.confirm() + }) { + LOGGER.info { "Canceled $it" } + } + Thread.sleep(200) - Thread.sleep(200) - connectionManager.basicConsume(queueName, { _, delivery, ack -> - LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from ${delivery.envelope.routingKey}" } - counter.incrementAndGet() - ack.confirm() - }) { - LOGGER.info { "Canceled $it" } + Assertions.assertEquals(1, counter.get()) { "Wrong number of received messages" } + Assertions.assertTrue( + getQueuesInfo(it).toString().contains("$queueName\t0") + ) { "There should be no messages left in the queue" } + } finally { + Assertions.assertNotNull(monitor) + Assertions.assertDoesNotThrow { + monitor!!.unsubscribe() + } } - Thread.sleep(200) - - Assertions.assertEquals(1, counter.get()) { "Wrong number of received messages" } - Assertions.assertTrue( - getQueuesInfo(it).toString().contains("$queueName\t0") - ) { "There should be no messages left in the queue" } } } @@ -643,33 +665,41 @@ class TestConnectionManager { maxRecoveryAttempts = 5 ), ).use { connectionManager -> - - val thread = Thread { - // marker that thread is actually running - connectionManager.basicConsume(queueName, { _, delivery, ack -> - LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from ${delivery.envelope.routingKey}" } - counter.incrementAndGet() - ack.confirm() - }) { - LOGGER.info { "Canceled $it" } + var monitor: SubscriberMonitor? = null + var thread: Thread? = null + try { + thread = thread { + // marker that thread is actually running + monitor = connectionManager.basicConsume(queueName, { _, delivery, ack -> + LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from ${delivery.envelope.routingKey}" } + counter.incrementAndGet() + ack.confirm() + }) { + LOGGER.info { "Canceled $it" } + } + } + Thread.sleep(2000) + LOGGER.info { "Interrupting..." } + thread.interrupt() + LOGGER.info { "Interrupted!" } + Thread.sleep(4000) + LOGGER.info { "Sleep done" } + + Assertions.assertFalse(thread.isAlive) + + Assertions.assertEquals(1, counter.get()) { "Wrong number of received messages" } + Assertions.assertTrue( + getQueuesInfo(it).toString().contains("$queueName\t0") + ) { "There should be no messages left in the queue" } + } finally { + Assertions.assertDoesNotThrow { + monitor?.unsubscribe() } + Assertions.assertNotNull(thread) + thread?.interrupt() + thread?.join(100) + Assertions.assertFalse(thread!!.isAlive) } - thread.start() - Thread.sleep(2000) - LOGGER.info { "Interrupting..." } - thread.interrupt() - LOGGER.info { "Interrupted!" } - // todo try join - Thread.sleep(4000) - LOGGER.info { "Sleep done" } - -// todo assert thread.isAlive - -// Assertions.assertEquals(1, counter.get()) { "Wrong number of received messages" } -// Assertions.assertTrue( -// getQueuesInfo(it).toString().contains("$queueName\t0") -// ) { "There should be no messages left in the queue" } - } } } @@ -702,32 +732,41 @@ class TestConnectionManager { ), ).use { connectionManager -> - declareQueue(it, queueName) + var thread: Thread? = null + var monitor: SubscriberMonitor? + try { + declareQueue(it, queueName) - val thread = Thread { - val subscriberMonitor = connectionManager.basicConsume(queueName, { _, delivery, ack -> - LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from ${delivery.envelope.routingKey}" } - counter.incrementAndGet() - ack.confirm() - }) { - LOGGER.info { "Canceled $it" } + thread = thread { + monitor = connectionManager.basicConsume(queueName, { _, delivery, ack -> + LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from ${delivery.envelope.routingKey}" } + counter.incrementAndGet() + ack.confirm() + }) { + LOGGER.info { "Canceled $it" } + } + + Thread.sleep(3500) + LOGGER.info { "Unsubscribing..." } + monitor!!.unsubscribe() + } + for (i in 1..5) { + putMessageInQueue(it, queueName) + Thread.sleep(1000) } - Thread.sleep(3500) - LOGGER.info { "Unsubscribing..." } - subscriberMonitor.unsubscribe() - } - thread.start() - for (i in 1..5) { - putMessageInQueue(it, queueName) - Thread.sleep(1000) + Assertions.assertEquals(3, counter.get()) { "Wrong number of received messages" } + Assertions.assertTrue( + getQueuesInfo(it).toString().contains("$queueName\t2") + ) { "There should be messages in the queue" } + } finally { + Assertions.assertNotNull(thread) + Assertions.assertDoesNotThrow { + thread!!.interrupt() + } + Assertions.assertFalse(thread!!.isAlive) } - Assertions.assertEquals(3, counter.get()) { "Wrong number of received messages" } - Assertions.assertTrue( - getQueuesInfo(it).toString().contains("$queueName\t2") - ) { "There should be messages in the queue" } - } } } @@ -763,46 +802,54 @@ class TestConnectionManager { maxRecoveryAttempts = 5 ), ).use { connectionManager -> - Thread { - val subscriberMonitor = connectionManager.basicConsume(queueName, { _, delivery, ack -> - LOGGER.info { "Received 1 ${delivery.body.toString(Charsets.UTF_8)} from \"${delivery.envelope.routingKey}\"" } - if (counter.get() == 0) { - ack.confirm() - LOGGER.info { "Confirmed!" } - } else { - LOGGER.info { "Left this message unacked" } + var thread: Thread? = null + try { + thread = thread { + val subscriberMonitor = connectionManager.basicConsume(queueName, { _, delivery, ack -> + LOGGER.info { "Received 1 ${delivery.body.toString(Charsets.UTF_8)} from \"${delivery.envelope.routingKey}\"" } + if (counter.get() == 0) { + ack.confirm() + LOGGER.info { "Confirmed!" } + } else { + LOGGER.info { "Left this message unacked" } + } + counter.incrementAndGet() + }) { + LOGGER.info { "Canceled $it" } } - counter.incrementAndGet() - }) { - LOGGER.info { "Canceled $it" } - } - - Thread.sleep(30000) - LOGGER.info { "Unsubscribing..." } - subscriberMonitor.unsubscribe() - }.start() - LOGGER.info { "Sending first message" } - putMessageInQueue(it, queueName) + Thread.sleep(30000) + LOGGER.info { "Unsubscribing..." } + subscriberMonitor.unsubscribe() + } - LOGGER.info { "queues list: \n ${getQueuesInfo(it)}" } + LOGGER.info { "Sending first message" } + putMessageInQueue(it, queueName) - LOGGER.info { "Sending second message" } - putMessageInQueue(it, queueName) - LOGGER.info { "Sleeping..." } - Thread.sleep(63000) + LOGGER.info { "queues list: \n ${getQueuesInfo(it)}" } - LOGGER.info { "queues list: \n ${getQueuesInfo(it)}" } + LOGGER.info { "Sending second message" } + putMessageInQueue(it, queueName) + LOGGER.info { "Sleeping..." } + Thread.sleep(63000) + LOGGER.info { "queues list: \n ${getQueuesInfo(it)}" } - val queuesListExecResult = getQueuesInfo(it) - LOGGER.info { "queues list: \n $queuesListExecResult" } - Assertions.assertEquals(2, counter.get()) { "Wrong number of received messages" } - Assertions.assertTrue( - queuesListExecResult.toString().contains("$queueName\t1") - ) { "There should a message left in the queue" } + val queuesListExecResult = getQueuesInfo(it) + LOGGER.info { "queues list: \n $queuesListExecResult" } + Assertions.assertEquals(2, counter.get()) { "Wrong number of received messages" } + Assertions.assertTrue( + queuesListExecResult.toString().contains("$queueName\t1") + ) { "There should a message left in the queue" } + } finally { + Assertions.assertNotNull(thread) + Assertions.assertDoesNotThrow { + thread!!.interrupt() + } + Assertions.assertFalse(thread!!.isAlive) + } } } } From 2f63aa7e534459fb914821cd556b6c77ab0fe87d Mon Sep 17 00:00:00 2001 From: "fiodar.rekish" Date: Fri, 18 Nov 2022 12:58:20 +0400 Subject: [PATCH 22/51] [TH2-2212] assertion fix --- .../impl/rabbitmq/connection/TestConnectionManager.kt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt index a856cda5e..5d2411366 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt @@ -679,18 +679,16 @@ class TestConnectionManager { } } Thread.sleep(2000) + Assertions.assertTrue(thread.isAlive) LOGGER.info { "Interrupting..." } thread.interrupt() LOGGER.info { "Interrupted!" } - Thread.sleep(4000) + Thread.sleep(1000) LOGGER.info { "Sleep done" } Assertions.assertFalse(thread.isAlive) - Assertions.assertEquals(1, counter.get()) { "Wrong number of received messages" } - Assertions.assertTrue( - getQueuesInfo(it).toString().contains("$queueName\t0") - ) { "There should be no messages left in the queue" } + Assertions.assertEquals(0, counter.get()) { "Wrong number of received messages" } } finally { Assertions.assertDoesNotThrow { monitor?.unsubscribe() From 7ed2dd6f0c9b86260fc64010fb9469902a16ddda Mon Sep 17 00:00:00 2001 From: "fiodar.rekish" Date: Fri, 18 Nov 2022 12:10:05 +0400 Subject: [PATCH 23/51] [TH2-2212] refactored --- .../connection/ConnectionManager.java | 7 +- .../connection/TestConnectionManager.kt | 450 ++++++++---------- 2 files changed, 196 insertions(+), 261 deletions(-) diff --git a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java index 859648503..6e3100ec5 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java @@ -515,7 +515,7 @@ private static void basicAck(Channel channel, long deliveryTag) throws IOExcepti channel.basicAck(deliveryTag, false); } - private static class RabbitMqSubscriberMonitor implements SubscriberMonitor { + private class RabbitMqSubscriberMonitor implements SubscriberMonitor { private final ChannelHolder holder; private final String tag; @@ -531,8 +531,9 @@ public RabbitMqSubscriberMonitor(ChannelHolder holder, String tag, @Override public void unsubscribe() throws Exception { holder.withLock(false, channel -> { - holder.subscriptionCallbacks = null; + channelsByPin.values().remove(holder); action.execute(channel, tag); + channel.abort(); }); } } @@ -591,7 +592,7 @@ private static class ChannelHolder { private final Supplier supplier; private final BiConsumer reconnectionChecker; private final int maxCount; - private SubscriptionCallbacks subscriptionCallbacks; + private final SubscriptionCallbacks subscriptionCallbacks; @GuardedBy("lock") private int pending; @GuardedBy("lock") diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt index 269ebdc8e..cf3eef97f 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt @@ -34,6 +34,9 @@ import kotlin.concurrent.thread import mu.KotlinLogging import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test import org.testcontainers.containers.RabbitMQContainer @@ -51,28 +54,19 @@ class TestConnectionManager { val routingKey = "routingKey1" val queueName = "queue1" val exchange = "test-exchange1" - val prefetchCount = 10 rabbit .let { declareQueue(rabbit, queueName) declareFanoutExchangeWithBinding(rabbit, exchange, queueName) LOGGER.info { "Started with port ${it.amqpPort}" } - val queue = ArrayBlockingQueue(prefetchCount) - val countDown = CountDownLatch(prefetchCount) - val confirmationTimeout = Duration.ofSeconds(1) - ConnectionManager( - RabbitMQConfiguration( - host = it.host, - vHost = "", - port = it.amqpPort, - username = it.adminUsername, - password = it.adminPassword, - ), - ConnectionManagerConfiguration( + val queue = ArrayBlockingQueue(PREFETCH_COUNT) + val countDown = CountDownLatch(PREFETCH_COUNT) + createConnectionManager( + it, ConnectionManagerConfiguration( subscriberName = "test", - prefetchCount = prefetchCount, - confirmationTimeout = confirmationTimeout, - ), + prefetchCount = PREFETCH_COUNT, + confirmationTimeout = CONFIRMATION_TIMEOUT, + ) ).use { manager -> manager.basicConsume(queueName, { _, delivery, ack -> LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from ${delivery.envelope.routingKey}" } @@ -82,34 +76,29 @@ class TestConnectionManager { LOGGER.info { "Canceled $it" } } - repeat(prefetchCount + 1) { index -> + repeat(PREFETCH_COUNT + 1) { index -> manager.basicPublish(exchange, routingKey, null, "Hello $index".toByteArray(Charsets.UTF_8)) } - Assertions.assertTrue( - countDown.await( - 1L, - TimeUnit.SECONDS - ) - ) { "Not all messages were received: ${countDown.count}" } + countDown.assertComplete("Not all messages were received") - Assertions.assertTrue(manager.isAlive) { "Manager should still be alive" } - Assertions.assertTrue(manager.isReady) { "Manager should be ready until the confirmation timeout expires" } + assertTrue(manager.isAlive) { "Manager should still be alive" } + assertTrue(manager.isReady) { "Manager should be ready until the confirmation timeout expires" } - Thread.sleep(confirmationTimeout.toMillis() + 100/*just in case*/) // wait for confirmation timeout + Thread.sleep(CONFIRMATION_TIMEOUT.toMillis() + 100/*just in case*/) // wait for confirmation timeout - Assertions.assertTrue(manager.isAlive) { "Manager should still be alive" } - Assertions.assertFalse(manager.isReady) { "Manager should not be ready" } + assertTrue(manager.isAlive) { "Manager should still be alive" } + assertFalse(manager.isReady) { "Manager should not be ready" } queue.poll().confirm() - Assertions.assertTrue(manager.isAlive) { "Manager should still be alive" } - Assertions.assertTrue(manager.isReady) { "Manager should be ready" } + assertTrue(manager.isAlive) { "Manager should still be alive" } + assertTrue(manager.isReady) { "Manager should be ready" } val receivedData = generateSequence { queue.poll(10L, TimeUnit.MILLISECONDS) } .onEach(ManualAckDeliveryCallback.Confirmation::confirm) .count() - Assertions.assertEquals(prefetchCount, receivedData) { "Unexpected number of messages received" } + assertEquals(PREFETCH_COUNT, receivedData) { "Unexpected number of messages received" } } } } @@ -117,24 +106,15 @@ class TestConnectionManager { @Test fun `connection manager receives a message from a queue that did not exist at the time of subscription`() { val wrongQueue = "wrong-queue2" - val prefetchCount = 10 rabbit - .let { - LOGGER.info { "Started with port ${it.amqpPort}" } - val counter = AtomicInteger(0) - val confirmationTimeout = Duration.ofSeconds(1) - ConnectionManager( - RabbitMQConfiguration( - host = it.host, - vHost = "", - port = it.amqpPort, - username = it.adminUsername, - password = it.adminPassword, - ), + .let { rabbitMQContainer -> + LOGGER.info { "Started with port ${rabbitMQContainer.amqpPort}" } + createConnectionManager( + rabbitMQContainer, ConnectionManagerConfiguration( subscriberName = "test", - prefetchCount = prefetchCount, - confirmationTimeout = confirmationTimeout, + prefetchCount = PREFETCH_COUNT, + confirmationTimeout = CONFIRMATION_TIMEOUT, minConnectionRecoveryTimeout = 100, maxConnectionRecoveryTimeout = 200, maxRecoveryAttempts = 5 @@ -142,37 +122,42 @@ class TestConnectionManager { ).use { connectionManager -> var thread: Thread? = null try { + val start = CountDownLatch(1) + val consume = CountDownLatch(1) thread = thread { + start.countDown() connectionManager.basicConsume(wrongQueue, { _, delivery, ack -> LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from ${delivery.envelope.routingKey}" } - counter.incrementAndGet() + consume.countDown() ack.confirm() }) { LOGGER.info { "Canceled $it" } } } + start.assertComplete("Thread for consuming isn't started") + // todo check isReady and isAlive, it should be false at some point +// assertTarget(false, "Readiness probe doesn't fall down", connectionManager::isReady) + LOGGER.info { "creating the queue..." } - declareQueue(it, wrongQueue) + declareQueue(rabbitMQContainer, wrongQueue) + assertTarget(false, "Thread for consuming isn't completed", thread::isAlive) + LOGGER.info { - "Adding message to the queue: \n" + putMessageInQueue( - it, - wrongQueue - ) + "Adding message to the queue:\n${putMessageInQueue(rabbitMQContainer, wrongQueue)}" } - LOGGER.info { "queues list: \n ${it.execInContainer("rabbitmqctl", "list_queues")}" } + LOGGER.info { "queues list: \n ${rabbitMQContainer.execInContainer("rabbitmqctl", "list_queues")}" } - // todo check isReady and isAlive, it should be false at some point - Assertions.assertEquals( - 1, - counter.get() - ) { "Unexpected number of messages received. The message should be received" } - Assertions.assertTrue(connectionManager.isAlive) - Assertions.assertTrue(connectionManager.isReady) + consume.assertComplete("Unexpected number of messages received. The message should be received") + + assertTrue(connectionManager.isAlive) + assertTrue(connectionManager.isReady) } finally { - thread?.interrupt() - thread?.join(100) - // assert fail + thread?.let { + thread.interrupt() + thread.join(100) + // todo assert fail + } } } @@ -183,44 +168,34 @@ class TestConnectionManager { fun `connection manager sends a message to wrong exchange`() { val queueName = "queue3" val exchange = "test-exchange3" - val prefetchCount = 10 rabbit .let { declareQueue(rabbit, queueName) LOGGER.info { "Started with port ${it.amqpPort}" } - val confirmationTimeout = Duration.ofSeconds(1) val counter = AtomicInteger(0) - ConnectionManager( - RabbitMQConfiguration( - host = it.host, - vHost = "", - port = it.amqpPort, - username = it.adminUsername, - password = it.adminPassword, - ), + createConnectionManager( + it, ConnectionManagerConfiguration( subscriberName = "test", - prefetchCount = prefetchCount, - confirmationTimeout = confirmationTimeout, + prefetchCount = PREFETCH_COUNT, + confirmationTimeout = CONFIRMATION_TIMEOUT, minConnectionRecoveryTimeout = 100, maxConnectionRecoveryTimeout = 200, maxRecoveryAttempts = 5 ), ).use { connectionManager -> - Thread { - connectionManager.basicConsume(queueName, { _, delivery, _ -> - counter.incrementAndGet() - LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from \"${delivery.envelope.routingKey}\"" } - }) { - LOGGER.info { "Canceled $it" } - } - }.start() + connectionManager.basicConsume(queueName, { _, delivery, _ -> + counter.incrementAndGet() + LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from \"${delivery.envelope.routingKey}\"" } + }) { + LOGGER.info { "Canceled $it" } + } LOGGER.info { "Starting first publishing..." } connectionManager.basicPublish(exchange, "", null, "Hello1".toByteArray(Charsets.UTF_8)) Thread.sleep(200) LOGGER.info { "Publication finished!" } - Assertions.assertEquals( + assertEquals( 0, counter.get() ) { "Unexpected number of messages received. The first message shouldn't be received" } @@ -235,7 +210,7 @@ class TestConnectionManager { } Thread.sleep(200) - Assertions.assertEquals( + assertEquals( 1, counter.get() ) { "Unexpected number of messages received. The second message should be received" } @@ -248,7 +223,6 @@ class TestConnectionManager { fun `connection manager handles ack timeout`() { val configFilename = "rabbitmq_it.conf" val queueName = "queue4" - val prefetchCount = 10 RabbitMQContainer(DockerImageName.parse(RABBIT_IMAGE_NAME)) .withRabbitMQConfig(MountableFile.forClasspathResource(configFilename)) @@ -256,39 +230,30 @@ class TestConnectionManager { .use { it.start() LOGGER.info { "Started with port ${it.amqpPort}" } - val confirmationTimeout = Duration.ofSeconds(1) val counter = AtomicInteger(0) - ConnectionManager( - RabbitMQConfiguration( - host = it.host, - vHost = "", - port = it.amqpPort, - username = it.adminUsername, - password = it.adminPassword, - ), + createConnectionManager( + it, ConnectionManagerConfiguration( subscriberName = "test", - prefetchCount = prefetchCount, - confirmationTimeout = confirmationTimeout, + prefetchCount = PREFETCH_COUNT, + confirmationTimeout = CONFIRMATION_TIMEOUT, minConnectionRecoveryTimeout = 100, maxConnectionRecoveryTimeout = 200, maxRecoveryAttempts = 5 ), ).use { connectionManager -> - Thread { - connectionManager.basicConsume(queueName, { _, delivery, ack -> - LOGGER.info { "Received 1 ${delivery.body.toString(Charsets.UTF_8)} from \"${delivery.envelope.routingKey}\"" } - if (counter.get() != 0) { - ack.confirm() - LOGGER.info { "Confirmed!" } - } else { - LOGGER.info { "Left this message unacked" } - } - counter.incrementAndGet() - }) { - LOGGER.info { "Canceled $it" } + connectionManager.basicConsume(queueName, { _, delivery, ack -> + LOGGER.info { "Received 1 ${delivery.body.toString(Charsets.UTF_8)} from \"${delivery.envelope.routingKey}\"" } + if (counter.get() != 0) { + ack.confirm() + LOGGER.info { "Confirmed!" } + } else { + LOGGER.info { "Left this message unacked" } } - }.start() + counter.incrementAndGet() + }) { + LOGGER.info { "Canceled $it" } + } LOGGER.info { "Sending first message" } putMessageInQueue(it, queueName) @@ -303,8 +268,8 @@ class TestConnectionManager { val queuesListExecResult = getQueuesInfo(it) LOGGER.info { "queues list: \n $queuesListExecResult" } - Assertions.assertEquals(3, counter.get()) { "Wrong number of received messages" } - Assertions.assertTrue( + assertEquals(3, counter.get()) { "Wrong number of received messages" } + assertTrue( queuesListExecResult.toString().contains("$queueName\t0") ) { "There should be no messages left in the queue" } @@ -316,7 +281,6 @@ class TestConnectionManager { fun `connection manager handles ack timeout with several channels`() { val configFilename = "rabbitmq_it.conf" val queueNames = arrayOf("separate_queues1", "separate_queues2", "separate_queues3") - val prefetchCount = 10 RabbitMQContainer(DockerImageName.parse(RABBIT_IMAGE_NAME)) .withRabbitMQConfig(MountableFile.forClasspathResource(configFilename)) @@ -326,52 +290,43 @@ class TestConnectionManager { .use { it.start() LOGGER.info { "Started with port ${it.amqpPort}" } - val confirmationTimeout = Duration.ofSeconds(1) val counters = mapOf( queueNames[0] to AtomicInteger(), // this subscriber won't ack the first delivery queueNames[1] to AtomicInteger(-1), // this subscriber won't ack two first deliveries queueNames[2] to AtomicInteger(1) ) - ConnectionManager( - RabbitMQConfiguration( - host = it.host, - vHost = "", - port = it.amqpPort, - username = it.adminUsername, - password = it.adminPassword, - ), + createConnectionManager( + it, ConnectionManagerConfiguration( subscriberName = "test", - prefetchCount = prefetchCount, - confirmationTimeout = confirmationTimeout, + prefetchCount = PREFETCH_COUNT, + confirmationTimeout = CONFIRMATION_TIMEOUT, minConnectionRecoveryTimeout = 100, maxConnectionRecoveryTimeout = 200, maxRecoveryAttempts = 5 ), ).use { connectionManager -> - fun createThreadAndSubscribe( + fun subscribeOnQueue( queue: String ) { - Thread { - connectionManager.basicConsume(queue, { _, delivery, ack -> - LOGGER.info { "Received from queue $queue ${delivery.body.toString(Charsets.UTF_8)}" } - if (counters[queue]!!.get() > 0) { - ack.confirm() - LOGGER.info { "Confirmed message form $queue" } - } else { - LOGGER.info { "Left this message from $queue unacked" } - } - counters[queue]!!.incrementAndGet() - }, { - LOGGER.info { "Canceled message form queue $queue" } - }) - }.start() + connectionManager.basicConsume(queue, { _, delivery, ack -> + LOGGER.info { "Received from queue $queue ${delivery.body.toString(Charsets.UTF_8)}" } + if (counters[queue]!!.get() > 0) { + ack.confirm() + LOGGER.info { "Confirmed message form $queue" } + } else { + LOGGER.info { "Left this message from $queue unacked" } + } + counters[queue]!!.incrementAndGet() + }, { + LOGGER.info { "Canceled message form queue $queue" } + }) } - createThreadAndSubscribe(queueNames[0]) - createThreadAndSubscribe(queueNames[1]) - createThreadAndSubscribe(queueNames[2]) + subscribeOnQueue(queueNames[0]) + subscribeOnQueue(queueNames[1]) + subscribeOnQueue(queueNames[2]) LOGGER.info { "Sending the first message batch" } putMessageInQueue(it, queueNames[0]) @@ -399,18 +354,18 @@ class TestConnectionManager { LOGGER.info { "queues list: \n $queuesListExecResult" } for (queueName in queueNames) { - Assertions.assertTrue(queuesListExecResult.toString().contains("$queueName\t0")) + assertTrue(queuesListExecResult.toString().contains("$queueName\t0")) { "There should be no messages left in queue $queueName" } } // 0 + 1 failed ack + 2 successful ack + 1 ack of requeued message - Assertions.assertEquals(4, counters[queueNames[0]]!!.get()) + assertEquals(4, counters[queueNames[0]]!!.get()) { "Wrong number of received messages from queue ${queueNames[0]}" } // -1 + 2 failed ack + 2 ack of requeued message + 1 successful ack - Assertions.assertEquals(4, counters[queueNames[1]]!!.get()) + assertEquals(4, counters[queueNames[1]]!!.get()) { "Wrong number of received messages from queue ${queueNames[1]}" } - Assertions.assertEquals(4, counters[queueNames[2]]!!.get()) + assertEquals(4, counters[queueNames[2]]!!.get()) { "Wrong number of received messages from queue ${queueNames[2]}" } } @@ -420,7 +375,6 @@ class TestConnectionManager { @Test fun `connection manager receives a messages after container restart`() { val queueName = "queue5" - val prefetchCount = 10 val amqpPort = 5672 val container = object : RabbitMQContainer(DockerImageName.parse(RABBIT_IMAGE_NAME)) { fun addFixedPort(hostPort: Int, containerPort: Int) { @@ -436,7 +390,6 @@ class TestConnectionManager { it.start() LOGGER.info { "Started with port ${it.amqpPort}" } val counter = AtomicInteger(0) - val confirmationTimeout = Duration.ofSeconds(1) ConnectionManager( RabbitMQConfiguration( host = it.host, @@ -447,8 +400,8 @@ class TestConnectionManager { ), ConnectionManagerConfiguration( subscriberName = "test", - prefetchCount = prefetchCount, - confirmationTimeout = confirmationTimeout, + prefetchCount = PREFETCH_COUNT, + confirmationTimeout = CONFIRMATION_TIMEOUT, minConnectionRecoveryTimeout = 1000, maxConnectionRecoveryTimeout = 2000, connectionTimeout = 1000, @@ -456,15 +409,13 @@ class TestConnectionManager { ), ).use { connectionManager -> - Thread { - connectionManager.basicConsume(queueName, { _, delivery, ack -> - LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from ${delivery.envelope.routingKey}" } - counter.incrementAndGet() - ack.confirm() - }) { - LOGGER.info { "Canceled $it" } - } - }.start() + connectionManager.basicConsume(queueName, { _, delivery, ack -> + LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from ${delivery.envelope.routingKey}" } + counter.incrementAndGet() + ack.confirm() + }) { + LOGGER.info { "Canceled $it" } + } LOGGER.info { "Rabbit address- ${it.host}:${it.amqpPort}" } LOGGER.info { "Restarting the container" } @@ -482,8 +433,8 @@ class TestConnectionManager { restartContainer(it) Thread.sleep(5000) - Assertions.assertEquals(1, counter.get()) { "Wrong number of received messages" } - Assertions.assertTrue( + assertEquals(1, counter.get()) { "Wrong number of received messages" } + assertTrue( getQueuesInfo(it).toString().contains("$queueName\t0") ) { "There should be no messages left in the queue" } @@ -494,7 +445,6 @@ class TestConnectionManager { @Test fun `connection manager publish a message and receives it`() { val queueName = "queue6" - val prefetchCount = 10 val exchange = "test-exchange6" val routingKey = "routingKey6" @@ -502,19 +452,12 @@ class TestConnectionManager { .let { LOGGER.info { "Started with port ${it.amqpPort}" } val counter = AtomicInteger(0) - val confirmationTimeout = Duration.ofSeconds(1) - ConnectionManager( - RabbitMQConfiguration( - host = it.host, - vHost = "", - port = it.amqpPort, - username = it.adminUsername, - password = it.adminPassword, - ), + createConnectionManager( + it, ConnectionManagerConfiguration( subscriberName = "test", - prefetchCount = prefetchCount, - confirmationTimeout = confirmationTimeout, + prefetchCount = PREFETCH_COUNT, + confirmationTimeout = CONFIRMATION_TIMEOUT, minConnectionRecoveryTimeout = 10000, maxConnectionRecoveryTimeout = 20000, connectionTimeout = 10000, @@ -527,20 +470,18 @@ class TestConnectionManager { connectionManager.basicPublish(exchange, routingKey, null, "Hello1".toByteArray(Charsets.UTF_8)) - Thread.sleep(500) - Thread { - connectionManager.basicConsume(queueName, { _, delivery, ack -> - LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from ${delivery.envelope.routingKey}" } - counter.incrementAndGet() - ack.confirm() - }) { - LOGGER.info { "Canceled $it" } - } - }.start() - Thread.sleep(500) + Thread.sleep(200) + connectionManager.basicConsume(queueName, { _, delivery, ack -> + LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from ${delivery.envelope.routingKey}" } + counter.incrementAndGet() + ack.confirm() + }) { + LOGGER.info { "Canceled $it" } + } + Thread.sleep(200) - Assertions.assertEquals(1, counter.get()) { "Wrong number of received messages" } - Assertions.assertTrue( + assertEquals(1, counter.get()) { "Wrong number of received messages" } + assertTrue( getQueuesInfo(it).toString().contains("$queueName\t0") ) { "There should be no messages left in the queue" } @@ -555,7 +496,6 @@ class TestConnectionManager { val exchange = "test-exchange7" val routingKey = "routingKey7" - val prefetchCount = 10 RabbitMQContainer(DockerImageName.parse(RABBIT_IMAGE_NAME)) .withRabbitMQConfig(MountableFile.forClasspathResource(configFilename)) @@ -565,39 +505,30 @@ class TestConnectionManager { .use { it.start() LOGGER.info { "Started with port ${it.amqpPort}" } - val confirmationTimeout = Duration.ofSeconds(1) val counter = AtomicInteger(0) - ConnectionManager( - RabbitMQConfiguration( - host = it.host, - vHost = "", - port = it.amqpPort, - username = it.adminUsername, - password = it.adminPassword, - ), + createConnectionManager( + it, ConnectionManagerConfiguration( subscriberName = "test", - prefetchCount = prefetchCount, - confirmationTimeout = confirmationTimeout, + prefetchCount = PREFETCH_COUNT, + confirmationTimeout = CONFIRMATION_TIMEOUT, minConnectionRecoveryTimeout = 100, maxConnectionRecoveryTimeout = 200, maxRecoveryAttempts = 5 ), ).use { connectionManager -> - Thread { - connectionManager.basicConsume(queueName, { _, delivery, ack -> - LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} " } - if (counter.get() != 0) { - ack.confirm() - LOGGER.info { "Confirmed!" } - } else { - LOGGER.info { "Left this message unacked" } - } - counter.incrementAndGet() - }) { - LOGGER.info { "Canceled $it" } + connectionManager.basicConsume(queueName, { _, delivery, ack -> + LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} " } + if (counter.get() != 0) { + ack.confirm() + LOGGER.info { "Confirmed!" } + } else { + LOGGER.info { "Left this message unacked" } } - }.start() + counter.incrementAndGet() + }) { + LOGGER.info { "Canceled $it" } + } LOGGER.info { "Sending the first message" } @@ -619,8 +550,8 @@ class TestConnectionManager { val queuesListExecResult = getQueuesInfo(it) LOGGER.info { "queues list: \n $queuesListExecResult" } - Assertions.assertEquals(4, counter.get()) { "Wrong number of received messages" } - Assertions.assertTrue( + assertEquals(4, counter.get()) { "Wrong number of received messages" } + assertTrue( queuesListExecResult.toString().contains("$queueName\t0") ) { "There should be no messages left in the queue" } @@ -631,24 +562,16 @@ class TestConnectionManager { @Test fun `thread interruption test`() { val queueName = "queue8" - val prefetchCount = 10 rabbit .let { LOGGER.info { "Started with port ${it.amqpPort}" } val counter = AtomicInteger(0) - val confirmationTimeout = Duration.ofSeconds(1) - ConnectionManager( - RabbitMQConfiguration( - host = it.host, - vHost = "", - port = it.amqpPort, - username = it.adminUsername, - password = it.adminPassword, - ), + createConnectionManager( + it, ConnectionManagerConfiguration( subscriberName = "test", - prefetchCount = prefetchCount, - confirmationTimeout = confirmationTimeout, + prefetchCount = PREFETCH_COUNT, + confirmationTimeout = CONFIRMATION_TIMEOUT, minConnectionRecoveryTimeout = 100, maxConnectionRecoveryTimeout = 2000, connectionTimeout = 1000, @@ -656,11 +579,6 @@ class TestConnectionManager { ), ).use { connectionManager -> -// declareQueue(it, queueName) -// declareFanoutExchangeWithBinding(it, exchange, queueName) -// -// connectionManager.basicPublish(exchange, routingKey, null, "Hello1".toByteArray(Charsets.UTF_8)) - val thread = Thread { // marker that thread is actually running connectionManager.basicConsume(queueName, { _, delivery, ack -> @@ -694,24 +612,16 @@ class TestConnectionManager { @Test fun `connection manager handles subscription cancel`() { val queueName = "queue9" - val prefetchCount = 10 rabbit .let { LOGGER.info { "Started with port ${it.amqpPort}" } val counter = AtomicInteger(0) - val confirmationTimeout = Duration.ofSeconds(1) - ConnectionManager( - RabbitMQConfiguration( - host = it.host, - vHost = "", - port = it.amqpPort, - username = it.adminUsername, - password = it.adminPassword, - ), + createConnectionManager( + it, ConnectionManagerConfiguration( subscriberName = "test", - prefetchCount = prefetchCount, - confirmationTimeout = confirmationTimeout, + prefetchCount = PREFETCH_COUNT, + confirmationTimeout = CONFIRMATION_TIMEOUT, minConnectionRecoveryTimeout = 100, maxConnectionRecoveryTimeout = 2000, connectionTimeout = 1000, @@ -740,8 +650,8 @@ class TestConnectionManager { Thread.sleep(1000) } - Assertions.assertEquals(3, counter.get()) { "Wrong number of received messages" } - Assertions.assertTrue( + assertEquals(3, counter.get()) { "Wrong number of received messages" } + assertTrue( getQueuesInfo(it).toString().contains("$queueName\t2") ) { "There should be messages in the queue" } @@ -753,7 +663,6 @@ class TestConnectionManager { fun `connection manager handles ack timeout and subscription cancel`() { val configFilename = "rabbitmq_it.conf" val queueName = "queue" - val prefetchCount = 10 RabbitMQContainer(DockerImageName.parse(RABBIT_IMAGE_NAME)) .withRabbitMQConfig(MountableFile.forClasspathResource(configFilename)) @@ -761,20 +670,13 @@ class TestConnectionManager { .use { it.start() LOGGER.info { "Started with port ${it.amqpPort}" } - val confirmationTimeout = Duration.ofSeconds(1) val counter = AtomicInteger(0) - ConnectionManager( - RabbitMQConfiguration( - host = it.host, - vHost = "", - port = it.amqpPort, - username = it.adminUsername, - password = it.adminPassword, - ), + createConnectionManager( + it, ConnectionManagerConfiguration( subscriberName = "test", - prefetchCount = prefetchCount, - confirmationTimeout = confirmationTimeout, + prefetchCount = PREFETCH_COUNT, + confirmationTimeout = CONFIRMATION_TIMEOUT, minConnectionRecoveryTimeout = 100, maxConnectionRecoveryTimeout = 200, maxRecoveryAttempts = 5 @@ -815,8 +717,8 @@ class TestConnectionManager { val queuesListExecResult = getQueuesInfo(it) LOGGER.info { "queues list: \n $queuesListExecResult" } - Assertions.assertEquals(2, counter.get()) { "Wrong number of received messages" } - Assertions.assertTrue( + assertEquals(2, counter.get()) { "Wrong number of received messages" } + assertTrue( queuesListExecResult.toString().contains("$queueName\t1") ) { "There should a message left in the queue" } @@ -824,10 +726,42 @@ class TestConnectionManager { } } + private fun CountDownLatch.assertComplete(message: String) { + assertTrue( + await( + 1L, + TimeUnit.SECONDS + ) + ) { "$message, actual count: $count" } + } + + private fun assertTarget(target: Boolean, message: String, func: () -> Boolean) { + val start = System.currentTimeMillis() + while (System.currentTimeMillis() - start < 1_000) { + if (func() == target) { + return + } + } + assertEquals(target, func(), message) + } + private fun createConnectionManager(container: RabbitMQContainer, configuration: ConnectionManagerConfiguration) = + ConnectionManager( + RabbitMQConfiguration( + host = container.host, + vHost = "", + port = container.amqpPort, + username = container.adminUsername, + password = container.adminPassword, + ), + configuration + ) + companion object { private const val RABBIT_IMAGE_NAME = "rabbitmq:3.8-management-alpine" private lateinit var rabbit: RabbitMQContainer + private const val PREFETCH_COUNT = 10 + private val CONFIRMATION_TIMEOUT = Duration.ofSeconds(1) @BeforeAll @JvmStatic From f1900b7c006dc00532d825f1dda43faa4e6591cd Mon Sep 17 00:00:00 2001 From: "nikita.smirnov" Date: Fri, 18 Nov 2022 14:42:44 +0400 Subject: [PATCH 24/51] [TH2-2212] refactored 'handles ack timeout' test --- .../connection/TestConnectionManager.kt | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt index 17ff813f5..222e08684 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt @@ -135,13 +135,13 @@ class TestConnectionManager { } } - assertTarget(true, "Thread for consuming isn't started", thread::isAlive) + assertTarget(true, message = "Thread for consuming isn't started", func = thread::isAlive) // todo check isReady and isAlive, it should be false at some point // assertTarget(false, "Readiness probe doesn't fall down", connectionManager::isReady) LOGGER.info { "creating the queue..." } declareQueue(rabbitMQContainer, wrongQueue) - assertTarget(false, "Thread for consuming isn't completed", thread::isAlive) + assertTarget(false, message = "Thread for consuming isn't completed", func = thread::isAlive) LOGGER.info { "Adding message to the queue:\n${putMessageInQueue(rabbitMQContainer, wrongQueue)}" @@ -241,7 +241,7 @@ class TestConnectionManager { .use { it.start() LOGGER.info { "Started with port ${it.amqpPort}" } - val counter = AtomicInteger(0) + createConnectionManager( it, ConnectionManagerConfiguration( @@ -253,25 +253,23 @@ class TestConnectionManager { maxRecoveryAttempts = 5 ), ).use { connectionManager -> + val consume = CountDownLatch(3) + connectionManager.basicConsume(queueName, { _, delivery, ack -> LOGGER.info { "Received 1 ${delivery.body.toString(Charsets.UTF_8)} from \"${delivery.envelope.routingKey}\"" } - if (counter.get() != 0) { - ack.confirm() - LOGGER.info { "Confirmed!" } - } else { - LOGGER.info { "Left this message unacked" } - } - counter.incrementAndGet() + consume.countDown() }) { LOGGER.info { "Canceled $it" } } LOGGER.info { "Sending first message" } putMessageInQueue(it, queueName) + assertTarget(3 - 1, message = "Consume first message") { consume.count } LOGGER.info { "queues list: \n ${getQueuesInfo(it)}" } - LOGGER.info { "Sleeping..." } - Thread.sleep(63000) + LOGGER.info { "Waiting for ack timeout ..." } + + assertTarget(3 - 2, 63_000,"Consume first message again") { consume.count } LOGGER.info { "Sending second message" } putMessageInQueue(it, queueName) @@ -279,9 +277,9 @@ class TestConnectionManager { val queuesListExecResult = getQueuesInfo(it) LOGGER.info { "queues list: \n $queuesListExecResult" } - assertEquals(3, counter.get()) { "Wrong number of received messages" } + consume.assertComplete("Wrong number of received messages") assertTrue( - queuesListExecResult.toString().contains("$queueName\t0") + queuesListExecResult.toString().contains("$queueName\t2") ) { "There should be no messages left in the queue" } } @@ -605,16 +603,16 @@ class TestConnectionManager { } } Thread.sleep(2000) - Assertions.assertTrue(thread.isAlive) + assertTrue(thread.isAlive) LOGGER.info { "Interrupting..." } thread.interrupt() LOGGER.info { "Interrupted!" } Thread.sleep(1000) LOGGER.info { "Sleep done" } - Assertions.assertFalse(thread.isAlive) + assertFalse(thread.isAlive) - Assertions.assertEquals(0, counter.get()) { "Wrong number of received messages" } + assertEquals(0, counter.get()) { "Wrong number of received messages" } } finally { Assertions.assertDoesNotThrow { monitor?.unsubscribe() @@ -622,7 +620,7 @@ class TestConnectionManager { Assertions.assertNotNull(thread) thread?.interrupt() thread?.join(100) - Assertions.assertFalse(thread!!.isAlive) + assertFalse(thread!!.isAlive) } } } @@ -680,7 +678,7 @@ class TestConnectionManager { Assertions.assertDoesNotThrow { thread!!.interrupt() } - Assertions.assertFalse(thread!!.isAlive) + assertFalse(thread!!.isAlive) } } @@ -756,7 +754,7 @@ class TestConnectionManager { Assertions.assertDoesNotThrow { thread!!.interrupt() } - Assertions.assertFalse(thread!!.isAlive) + assertFalse(thread!!.isAlive) } } } @@ -771,13 +769,13 @@ class TestConnectionManager { ) { "$message, actual count: $count" } } - private fun assertTarget(target: Boolean, message: String, func: () -> Boolean) { + private fun assertTarget(target: T, timeout: Long = 1_000, message: String, func: () -> T) { val start = System.currentTimeMillis() - while (System.currentTimeMillis() - start < 1_000) { + while (System.currentTimeMillis() - start < timeout) { if (func() == target) { return } - Thread.yield() + Thread.sleep(100) } assertEquals(target, func(), message) } From 8b81bf66c0a16e90d151b53fe0352d4d52438e59 Mon Sep 17 00:00:00 2001 From: "nikita.smirnov" Date: Fri, 18 Nov 2022 14:55:40 +0400 Subject: [PATCH 25/51] [TH2-2212] refactored 'thread interruption test' test --- .../connection/TestConnectionManager.kt | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt index 222e08684..320ea8ad9 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt @@ -602,25 +602,24 @@ class TestConnectionManager { LOGGER.info { "Canceled $it" } } } - Thread.sleep(2000) + + assertTarget(true, message = "Thread for consuming isn't started", func = thread::isAlive) + Thread.sleep(1000) assertTrue(thread.isAlive) LOGGER.info { "Interrupting..." } thread.interrupt() LOGGER.info { "Interrupted!" } - Thread.sleep(1000) - LOGGER.info { "Sleep done" } - - assertFalse(thread.isAlive) - + assertTarget(false, message = "Thread for consuming isn't stopped", func = thread::isAlive) assertEquals(0, counter.get()) { "Wrong number of received messages" } } finally { Assertions.assertDoesNotThrow { monitor?.unsubscribe() } - Assertions.assertNotNull(thread) - thread?.interrupt() - thread?.join(100) - assertFalse(thread!!.isAlive) + thread?.let { + thread.interrupt() + thread.join(100) + assertFalse(thread.isAlive) + } } } } From b91cc5c1626339afdd969d8b713e064933cb6252 Mon Sep 17 00:00:00 2001 From: "fiodar.rekish" Date: Fri, 18 Nov 2022 15:31:57 +0400 Subject: [PATCH 26/51] [TH2-2212] fix `thread interruption test` --- .../rabbitmq/connection/TestConnectionManager.kt | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt index 320ea8ad9..1e9073f82 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt @@ -154,7 +154,7 @@ class TestConnectionManager { assertTrue(connectionManager.isReady) } finally { Assertions.assertDoesNotThrow { - monitor?.unsubscribe() + monitor!!.unsubscribe() } thread?.let { thread.interrupt() @@ -589,12 +589,10 @@ class TestConnectionManager { maxRecoveryAttempts = 5 ), ).use { connectionManager -> - var monitor: SubscriberMonitor? = null var thread: Thread? = null try { thread = thread { - // marker that thread is actually running - monitor = connectionManager.basicConsume(queueName, { _, delivery, ack -> + connectionManager.basicConsume(queueName, { _, delivery, ack -> LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from ${delivery.envelope.routingKey}" } counter.incrementAndGet() ack.confirm() @@ -612,9 +610,6 @@ class TestConnectionManager { assertTarget(false, message = "Thread for consuming isn't stopped", func = thread::isAlive) assertEquals(0, counter.get()) { "Wrong number of received messages" } } finally { - Assertions.assertDoesNotThrow { - monitor?.unsubscribe() - } thread?.let { thread.interrupt() thread.join(100) @@ -812,8 +807,4 @@ class TestConnectionManager { } } - } - - - From 9cbd38860689c97887a1cb318368e284f616fe51 Mon Sep 17 00:00:00 2001 From: "fiodar.rekish" Date: Thu, 24 Nov 2022 15:20:57 +0400 Subject: [PATCH 27/51] [TH2-4466] fixed channels amount after recovery. Added related tests --- .../connection/ConnectionManager.java | 3 +- .../connection/TestConnectionManager.kt | 56 +++++++++++++++---- .../common/util/RabbitTestContainerUtil.kt | 24 ++++++-- 3 files changed, 65 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java index 63167e8aa..5721f3e99 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java @@ -74,7 +74,7 @@ public class ConnectionManager implements AutoCloseable { private static final Logger LOGGER = LoggerFactory.getLogger(ConnectionManager.class); - private final Connection connection; + public final Connection connection; private final Map channelsByPin = new ConcurrentHashMap<>(); private final AtomicBoolean connectionIsClosed = new AtomicBoolean(false); private final ConnectionManagerConfiguration configuration; @@ -271,7 +271,6 @@ private void recoverSubscriptionsOfChannel(Channel channel) { if (subscriptionCallbacks != null) { LOGGER.info("Changing channel for holder with pin id: " + pinId.toString()); channelsByPin.remove(pinId); - channel.abort(400, "Aborted because of the recovery"); basicConsume(pinId.queue, subscriptionCallbacks.deliverCallback, subscriptionCallbacks.cancelCallback); } } diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt index 1e9073f82..956699ea5 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt @@ -22,7 +22,9 @@ import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.Connec import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.RabbitMQConfiguration import com.exactpro.th2.common.util.RabbitTestContainerUtil.Companion.declareFanoutExchangeWithBinding import com.exactpro.th2.common.util.RabbitTestContainerUtil.Companion.declareQueue +import com.exactpro.th2.common.util.RabbitTestContainerUtil.Companion.getChannelsInfo import com.exactpro.th2.common.util.RabbitTestContainerUtil.Companion.getQueuesInfo +import com.exactpro.th2.common.util.RabbitTestContainerUtil.Companion.getSubscribedChannelsCount import com.exactpro.th2.common.util.RabbitTestContainerUtil.Companion.putMessageInQueue import com.exactpro.th2.common.util.RabbitTestContainerUtil.Companion.restartContainer import com.rabbitmq.client.BuiltinExchangeType @@ -37,6 +39,7 @@ import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test @@ -146,10 +149,18 @@ class TestConnectionManager { LOGGER.info { "Adding message to the queue:\n${putMessageInQueue(rabbitMQContainer, wrongQueue)}" } - LOGGER.info { "queues list: \n ${rabbitMQContainer.execInContainer("rabbitmqctl", "list_queues")}" } + LOGGER.info { + "queues list: \n ${ + rabbitMQContainer.execInContainer( + "rabbitmqctl", + "list_queues" + ) + }" + } consume.assertComplete("Unexpected number of messages received. The message should be received") + assertEquals(1, getSubscribedChannelsCount(rabbitMQContainer, wrongQueue)) assertTrue(connectionManager.isAlive) assertTrue(connectionManager.isReady) } finally { @@ -255,7 +266,7 @@ class TestConnectionManager { ).use { connectionManager -> val consume = CountDownLatch(3) - connectionManager.basicConsume(queueName, { _, delivery, ack -> + connectionManager.basicConsume(queueName, { _, delivery, _ -> LOGGER.info { "Received 1 ${delivery.body.toString(Charsets.UTF_8)} from \"${delivery.envelope.routingKey}\"" } consume.countDown() }) { @@ -267,9 +278,14 @@ class TestConnectionManager { assertTarget(3 - 1, message = "Consume first message") { consume.count } LOGGER.info { "queues list: \n ${getQueuesInfo(it)}" } + val channels1 = getChannelsInfo(it) + + LOGGER.info { channels1 } LOGGER.info { "Waiting for ack timeout ..." } - assertTarget(3 - 2, 63_000,"Consume first message again") { consume.count } + assertTarget(3 - 2, 63_000, "Consume first message again") { consume.count } + val channels2 = getChannelsInfo(it) + LOGGER.info { channels2 } LOGGER.info { "Sending second message" } putMessageInQueue(it, queueName) @@ -277,6 +293,10 @@ class TestConnectionManager { val queuesListExecResult = getQueuesInfo(it) LOGGER.info { "queues list: \n $queuesListExecResult" } + assertEquals(1, getSubscribedChannelsCount(it, queueName)) + { "There is must be single channel after recovery" } + assertNotEquals(channels1, channels2) { "The recovered channel must have another pid" } + consume.assertComplete("Wrong number of received messages") assertTrue( queuesListExecResult.toString().contains("$queueName\t2") @@ -365,9 +385,9 @@ class TestConnectionManager { for (queueName in queueNames) { assertTrue(queuesListExecResult.toString().contains("$queueName\t0")) { "There should be no messages left in queue $queueName" } + assertEquals(1, getSubscribedChannelsCount(it, queueName)) } - // 0 + 1 failed ack + 2 successful ack + 1 ack of requeued message assertEquals(4, counters[queueNames[0]]!!.get()) { "Wrong number of received messages from queue ${queueNames[0]}" } @@ -428,15 +448,19 @@ class TestConnectionManager { LOGGER.info { "Restarting the container" } restartContainer(it) + Thread.sleep(5000) LOGGER.info { "Rabbit address after restart - ${it.host}:${it.amqpPort}" } LOGGER.info { getQueuesInfo(it) } LOGGER.info { "Starting publishing..." } putMessageInQueue(it, queueName) + assertEquals(1, getSubscribedChannelsCount(it, queueName)) + LOGGER.info { "Publication finished!" } LOGGER.info { getQueuesInfo(it) } + consume.assertComplete("Wrong number of received messages") assertTrue( getQueuesInfo(it).toString().contains("$queueName\t0") @@ -476,15 +500,16 @@ class TestConnectionManager { connectionManager.basicPublish(exchange, routingKey, null, "Hello1".toByteArray(Charsets.UTF_8)) Thread.sleep(200) - monitor =connectionManager.basicConsume(queueName, { _, delivery, ack -> - LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from ${delivery.envelope.routingKey}" } - counter.incrementAndGet() - ack.confirm() - }) { - LOGGER.info { "Canceled $it" } - } - Thread.sleep(200) + monitor = connectionManager.basicConsume(queueName, { _, delivery, ack -> + LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from ${delivery.envelope.routingKey}" } + counter.incrementAndGet() + ack.confirm() + }) { + LOGGER.info { "Canceled $it" } + } + Thread.sleep(200) + assertEquals(1, getSubscribedChannelsCount(it, queueName)) assertEquals(1, counter.get()) { "Wrong number of received messages" } assertTrue( getQueuesInfo(it).toString().contains("$queueName\t0") @@ -561,6 +586,7 @@ class TestConnectionManager { val queuesListExecResult = getQueuesInfo(it) LOGGER.info { "queues list: \n $queuesListExecResult" } + assertEquals(1, getSubscribedChannelsCount(it, queueName)) assertEquals(4, counter.get()) { "Wrong number of received messages" } assertTrue( queuesListExecResult.toString().contains("$queueName\t0") @@ -609,6 +635,7 @@ class TestConnectionManager { LOGGER.info { "Interrupted!" } assertTarget(false, message = "Thread for consuming isn't stopped", func = thread::isAlive) assertEquals(0, counter.get()) { "Wrong number of received messages" } + assertEquals(0, getSubscribedChannelsCount(it, queueName)) {"There should be no subscribed channels"} } finally { thread?.let { thread.interrupt() @@ -663,6 +690,8 @@ class TestConnectionManager { Thread.sleep(1000) } + assertEquals(0, getSubscribedChannelsCount(it, queueName)) {"There should be no subscribed channels"} + assertEquals(3, counter.get()) { "Wrong number of received messages" } assertTrue( getQueuesInfo(it).toString().contains("$queueName\t2") @@ -739,6 +768,7 @@ class TestConnectionManager { val queuesListExecResult = getQueuesInfo(it) LOGGER.info { "queues list: \n $queuesListExecResult" } + assertEquals(0, getSubscribedChannelsCount(it, queueName)) assertEquals(2, counter.get()) { "Wrong number of received messages" } assertTrue( queuesListExecResult.toString().contains("$queueName\t1") @@ -773,6 +803,8 @@ class TestConnectionManager { } assertEquals(target, func(), message) } + + private fun createConnectionManager(container: RabbitMQContainer, configuration: ConnectionManagerConfiguration) = ConnectionManager( RabbitMQConfiguration( diff --git a/src/test/kotlin/com/exactpro/th2/common/util/RabbitTestContainerUtil.kt b/src/test/kotlin/com/exactpro/th2/common/util/RabbitTestContainerUtil.kt index 24ace57a8..721376daa 100644 --- a/src/test/kotlin/com/exactpro/th2/common/util/RabbitTestContainerUtil.kt +++ b/src/test/kotlin/com/exactpro/th2/common/util/RabbitTestContainerUtil.kt @@ -26,10 +26,6 @@ class RabbitTestContainerUtil { } - fun removeQueue(rabbit: RabbitMQContainer, queueName: String): Container.ExecResult? { - return execCommandWithSplit(rabbit, "rabbitmqadmin delete queue name=$queueName") - } - fun declareFanoutExchangeWithBinding( rabbit: RabbitMQContainer, exchangeName: String, @@ -58,6 +54,19 @@ class RabbitTestContainerUtil { return execCommandWithSplit(rabbit, "rabbitmqctl list_queues") } + fun getChannelsInfo(rabbit: RabbitMQContainer): String { + return execCommandWithSplit(rabbit, "rabbitmqctl list_consumers").toString() + + } + + fun getSubscribedChannelsCount( + rabbitMQContainer: RabbitMQContainer, + queue: String, + ): Int { + val channelsInfo = getChannelsInfo(rabbitMQContainer) + return channelsInfo.countMatches(queue) + } + fun restartContainer(rabbit: RabbitMQContainer) { val tag: String = rabbit.containerId val snapshotId: String = rabbit.dockerClient.commitCmd(tag) @@ -74,5 +83,12 @@ class RabbitTestContainerUtil { ) } + private fun String.countMatches(pattern: String): Int { + return this.substringAfter("active\targuments") + .split(pattern) + .dropLastWhile { s -> s.isEmpty() } + .toTypedArray().size - 1 + } + } } \ No newline at end of file From 822e274efe2dede7940015f47a80382eaca51d3b Mon Sep 17 00:00:00 2001 From: Oleg Smelov Date: Thu, 19 Oct 2023 18:31:05 +0400 Subject: [PATCH 28/51] merge attempt --- .../workflows/dev-java-publish-sonatype.yml | 49 +- .../dev-release-java-publish-sonatype.yml | 19 + .github/workflows/java-publish-sonatype.yml | 24 +- Dockerfile | 2 +- README.md | 504 ++++++++++++++---- build.gradle | 226 ++++---- gradle.properties | 11 +- gradle/wrapper/gradle-wrapper.properties | 2 +- .../common/metrics}/MetricsUtilsBenchmark.kt | 9 +- .../th2/common/ConfigurationUtils.java | 8 +- .../com/exactpro/th2/common/event/Event.java | 397 ++++++++++---- .../exactpro/th2/common/event/EventUtils.java | 89 +++- .../event/bean/builder/CollectionBuilder.java | 5 +- .../event/bean/builder/MessageBuilder.java | 2 +- .../box/configuration/BoxConfiguration.java | 17 + .../common/schema/event/EventBatchRouter.java | 19 +- .../common/schema/event/EventBatchSender.java | 21 +- .../schema/event/EventBatchSubscriber.java | 27 +- .../schema/factory/AbstractCommonFactory.java | 371 ++++++------- .../common/schema/factory/CommonFactory.java | 301 +++++------ .../strategy/impl/AbstractFilterStrategy.java | 4 +- .../impl/AbstractTh2MsgFilterStrategy.java | 19 +- .../grpc/router/AbstractGrpcRouter.java | 14 +- .../common/schema/grpc/router/GrpcRouter.java | 7 +- .../grpc/router/impl/DefaultGrpcRouter.java | 10 +- .../grpc/router/impl/DefaultStubStorage.java | 2 +- .../schema/message/MessageListener.java | 5 +- .../common/schema/message/MessageRouter.java | 69 ++- .../schema/message/MessageSubscriber.java | 25 +- .../schema/message/NotificationRouter.kt | 47 ++ .../common/schema/message/QueueAttribute.java | 6 +- .../schema/message/SubscriberMonitor.java | 6 +- .../impl/rabbitmq/AbstractRabbitSender.java | 5 +- .../rabbitmq/AbstractRabbitSubscriber.java | 251 ++++----- .../connection/ConnectionManager.java | 145 +++-- .../builders/GenericCollectionBuilder.java | 45 ++ .../common/schema/util/Log4jConfigUtils.kt | 64 +-- .../grpc/router/AbstractGrpcInterceptor.kt | 8 +- .../th2/common/message/MessageFilterUtils.kt | 79 ++- .../th2/common/message/MessageUtils.kt | 193 +++++-- .../th2/common/metrics/CommonMetrics.kt | 2 + .../configuration/ConfigurationManager.kt | 3 + .../schema/cradle/CradleConfiguration.kt | 55 +- .../common/schema/factory/ExactproMetaInf.kt | 115 ++++ .../common/schema/factory/FactorySettings.kt | 111 +++- .../strategy/impl/AnyMessageFilterStrategy.kt | 20 +- .../grpc/configuration/GrpcConfiguration.kt | 4 +- .../message/ConfirmationMessageListener.kt | 31 +- .../common/schema/message/DeliveryMetadata.kt | 21 + .../message/ExclusiveSubscriberMonitor.kt | 21 + .../message/ManualAckDeliveryCallback.kt | 9 +- .../schema/message/MessageRouterContext.kt | 4 +- .../schema/message/MessageRouterUtils.kt | 38 +- .../MessageRouterConfiguration.kt | 15 +- .../message/impl/OnlyOnceConfirmation.kt | 12 +- .../context/DefaultMessageRouterContext.kt | 6 +- .../impl/monitor/EventMessageRouterMonitor.kt | 16 +- .../AbstractGroupBatchAdapterRouter.kt | 61 ++- .../impl/rabbitmq/AbstractRabbitRouter.kt | 111 ++-- .../rabbitmq/custom/RabbitCustomRouter.kt | 27 +- .../group/RabbitMessageGroupBatchRouter.kt | 42 +- .../group/RabbitMessageGroupBatchSender.kt | 37 +- .../RabbitMessageGroupBatchSubscriber.kt | 23 +- .../NotificationEventBatchRouter.kt | 74 +++ .../NotificationEventBatchSender.kt | 47 ++ .../NotificationEventBatchSubscriber.kt | 79 +++ .../impl/rabbitmq/transport/Cleanable.kt | 24 + .../message/impl/rabbitmq/transport/Codecs.kt | 398 ++++++++++++++ .../impl/rabbitmq/transport/Direction.kt | 36 ++ .../impl/rabbitmq/transport/EventId.kt | 155 ++++++ .../impl/rabbitmq/transport/GroupBatch.kt | 72 +++ .../impl/rabbitmq/transport/Message.kt | 46 ++ .../impl/rabbitmq/transport/MessageGroup.kt | 83 +++ .../impl/rabbitmq/transport/MessageId.kt | 198 +++++++ .../impl/rabbitmq/transport/ParsedMessage.kt | 317 +++++++++++ .../impl/rabbitmq/transport/RawMessage.kt | 190 +++++++ .../transport/TransportGroupBatchRouter.kt | 75 +++ .../transport/TransportGroupBatchSender.kt | 63 +++ .../TransportGroupBatchSubscriber.kt | 67 +++ .../impl/rabbitmq/transport/TransportUtils.kt | 114 ++++ .../transport/builders/CollectionBuilder.kt | 44 ++ .../rabbitmq/transport/builders/MapBuilder.kt | 38 ++ .../exactpro/th2/common/value/ValueUtils.kt | 117 ++-- .../builder/MessageEventIdBuildersTest.java | 104 ++++ .../th2/common/event/bean/BaseTest.java | 12 +- .../th2/common/event/bean/MessageTest.java | 10 +- .../th2/common/event/bean/TableTest.java | 10 +- .../common/event/bean/TestVerification.java | 13 +- .../th2/common/event/bean/TreeTableTest.java | 28 +- .../exactpro/th2/common/event/TestEvent.kt | 75 ++- .../common/schema/TestJsonConfiguration.kt | 89 ++-- .../schema/factory/CommonFactoryTest.kt | 123 +++++ .../impl/TestAnyMessageFilterStrategy.kt | 490 +++++++++++++++-- .../TestConfirmationMessageListenerWrapper.kt | 10 +- .../AbstractRabbitRouterIntegrationTest.kt | 226 ++++++++ .../impl/rabbitmq/AbstractRabbitRouterTest.kt | 264 +++++++++ .../impl/rabbitmq/TestMessageConverter.kt | 30 ++ .../connection/TestConnectionManager.kt | 95 +++- .../TestMessageConverterLambdaDelegate.kt | 5 +- .../impl/rabbitmq/custom/TestMessageUtil.kt | 59 +- ...IntegrationTestRabbitMessageBatchRouter.kt | 141 +++++ ...ter.kt => TestRabbitMessageBatchRouter.kt} | 94 ++-- .../impl/rabbitmq/transport/CodecsTest.kt | 169 ++++++ .../rabbitmq/transport/ParsedMessageTest.kt | 201 +++++++ ...ransportGroupBatchRouterIntegrationTest.kt | 147 +++++ .../TransportGroupBatchRouterTest.kt | 315 +++++++++++ .../rabbitmq/transport/TransportUtilsTest.kt | 280 ++++++++++ src/test/resources/log4j2.properties | 40 ++ .../custom.json | 1 + .../cassandra_storage_settings.json | 21 + .../cradle_confidential.json | 3 +- .../cradle_non_confidential.json | 5 +- .../cradle_non_confidential_combo.json | 27 + .../test_json_configurations/grpc.json | 3 +- .../message_router.json | 4 +- .../test_message_event_id_builders/box.json | 4 + .../com/exactpro/th2/common/TestUtils.kt | 7 +- .../th2/common/annotations/IntegrationTest.kt | 0 suppressions.xml | 16 + 119 files changed, 7697 insertions(+), 1552 deletions(-) create mode 100644 .github/workflows/dev-release-java-publish-sonatype.yml rename src/jmh/kotlin/{ => com/exactpro/th2/common/metrics}/MetricsUtilsBenchmark.kt (93%) create mode 100644 src/main/java/com/exactpro/th2/common/schema/message/NotificationRouter.kt create mode 100644 src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/builders/GenericCollectionBuilder.java create mode 100644 src/main/kotlin/com/exactpro/th2/common/schema/factory/ExactproMetaInf.kt create mode 100644 src/main/kotlin/com/exactpro/th2/common/schema/message/DeliveryMetadata.kt create mode 100644 src/main/kotlin/com/exactpro/th2/common/schema/message/ExclusiveSubscriberMonitor.kt create mode 100644 src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/notification/NotificationEventBatchRouter.kt create mode 100644 src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/notification/NotificationEventBatchSender.kt create mode 100644 src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/notification/NotificationEventBatchSubscriber.kt create mode 100644 src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/Cleanable.kt create mode 100644 src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/Codecs.kt create mode 100644 src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/Direction.kt create mode 100644 src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/EventId.kt create mode 100644 src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/GroupBatch.kt create mode 100644 src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/Message.kt create mode 100644 src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/MessageGroup.kt create mode 100644 src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/MessageId.kt create mode 100644 src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/ParsedMessage.kt create mode 100644 src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/RawMessage.kt create mode 100644 src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/TransportGroupBatchRouter.kt create mode 100644 src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/TransportGroupBatchSender.kt create mode 100644 src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/TransportGroupBatchSubscriber.kt create mode 100644 src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/TransportUtils.kt create mode 100644 src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/builders/CollectionBuilder.kt create mode 100644 src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/builders/MapBuilder.kt create mode 100644 src/test/java/com/exactpro/th2/common/builder/MessageEventIdBuildersTest.java create mode 100644 src/test/kotlin/com/exactpro/th2/common/schema/factory/CommonFactoryTest.kt create mode 100644 src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitRouterIntegrationTest.kt create mode 100644 src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitRouterTest.kt create mode 100644 src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/TestMessageConverter.kt create mode 100644 src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/group/IntegrationTestRabbitMessageBatchRouter.kt rename src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/group/{TestRabbitMessageGroupBatchRouter.kt => TestRabbitMessageBatchRouter.kt} (72%) create mode 100644 src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/CodecsTest.kt create mode 100644 src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/ParsedMessageTest.kt create mode 100644 src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/TransportGroupBatchRouterIntegrationTest.kt create mode 100644 src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/TransportGroupBatchRouterTest.kt create mode 100644 src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/TransportUtilsTest.kt create mode 100644 src/test/resources/log4j2.properties create mode 100644 src/test/resources/test_common_factory_load_configs/custom.json create mode 100644 src/test/resources/test_json_configurations/cassandra_storage_settings.json create mode 100644 src/test/resources/test_json_configurations/cradle_non_confidential_combo.json create mode 100644 src/test/resources/test_message_event_id_builders/box.json rename src/{test => testFixtures}/kotlin/com/exactpro/th2/common/annotations/IntegrationTest.kt (100%) create mode 100644 suppressions.xml diff --git a/.github/workflows/dev-java-publish-sonatype.yml b/.github/workflows/dev-java-publish-sonatype.yml index 4b6e75dc6..ec39beb2f 100644 --- a/.github/workflows/dev-java-publish-sonatype.yml +++ b/.github/workflows/dev-java-publish-sonatype.yml @@ -12,42 +12,13 @@ on: - README.md jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 -# Prepare custom build version - - name: Get branch name - id: branch - run: echo ::set-output name=branch_name::${GITHUB_REF#refs/*/} - - name: Get release_version - id: ver - uses: christian-draeger/read-properties@1.0.1 - with: - path: gradle.properties - property: release_version - - name: Build custom release version - id: release_ver - run: echo ::set-output name=value::"${{ steps.ver.outputs.value }}-${{ steps.branch.outputs.branch_name }}-${{ github.run_id }}-SNAPSHOT" - - name: Write custom release version to file - uses: christian-draeger/write-properties@1.0.1 - with: - path: gradle.properties - property: release_version - value: ${{ steps.release_ver.outputs.value }} - - name: Show custom release version - run: echo ${{ steps.release_ver.outputs.value }} -# Build and publish package - - name: Set up JDK 11 - uses: actions/setup-java@v1 - with: - java-version: '11' - - name: Build with Gradle - run: ./gradlew --info clean build publish - env: - ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} - ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} - ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SONATYPE_GPG_ARMORED_KEY }} - ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SONATYPE_SIGNING_PASSWORD }} - + build-job: + uses: th2-net/.github/.github/workflows/compound-java-dev.yml@main + with: + build-target: 'Sonatype' + runsOn: ubuntu-latest + secrets: + sonatypeUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} + sonatypePassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} + sonatypeSigningKey: ${{ secrets.SONATYPE_GPG_ARMORED_KEY }} + sonatypeSigningPassword: ${{ secrets.SONATYPE_SIGNING_PASSWORD }} diff --git a/.github/workflows/dev-release-java-publish-sonatype.yml b/.github/workflows/dev-release-java-publish-sonatype.yml new file mode 100644 index 000000000..0c23d75c8 --- /dev/null +++ b/.github/workflows/dev-release-java-publish-sonatype.yml @@ -0,0 +1,19 @@ +name: Build and publish dev-release Java distributions to sonatype. + +on: + push: + tags: + - \d+.\d+.\d+-dev + +jobs: + build: + uses: th2-net/.github/.github/workflows/compound-java.yml@main + with: + build-target: 'Sonatype' + runsOn: ubuntu-latest + devRelease: true + secrets: + sonatypeUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} + sonatypePassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} + sonatypeSigningKey: ${{ secrets.SONATYPE_GPG_ARMORED_KEY }} + sonatypeSigningPassword: ${{ secrets.SONATYPE_SIGNING_PASSWORD }} \ No newline at end of file diff --git a/.github/workflows/java-publish-sonatype.yml b/.github/workflows/java-publish-sonatype.yml index b8c366887..aab717525 100644 --- a/.github/workflows/java-publish-sonatype.yml +++ b/.github/workflows/java-publish-sonatype.yml @@ -10,18 +10,12 @@ on: jobs: build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v1 - with: - java-version: '11' - - name: Build with Gradle - run: ./gradlew --info clean build publish closeAndReleaseSonatypeStagingRepository - env: - ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} - ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} - ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SONATYPE_GPG_ARMORED_KEY }} - ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SONATYPE_SIGNING_PASSWORD }} + uses: th2-net/.github/.github/workflows/compound-java.yml@main + with: + build-target: 'Sonatype' + runsOn: ubuntu-latest + secrets: + sonatypeUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} + sonatypePassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} + sonatypeSigningKey: ${{ secrets.SONATYPE_GPG_ARMORED_KEY }} + sonatypeSigningPassword: ${{ secrets.SONATYPE_SIGNING_PASSWORD }} diff --git a/Dockerfile b/Dockerfile index c7e2c2e67..aa3658bda 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM gradle:7.5.1-jdk11 AS build +FROM gradle:7.6-jdk11 AS build ARG release_version ARG bintray_user ARG bintray_key diff --git a/README.md b/README.md index 115fad0f9..1224e99c1 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,27 @@ -# th2 common library (Java) (3.42.1) +# th2 common library (Java) (5.6.1) ## Usage Firstly, you must import CommonFactory class: + ``` import com.exactpro.th2.common.schema.factory.CommonFactory ``` + Then you will create an instance of imported class, by choosing one of the following options: -1. Create factory with configs from the default path (`/var/th2/config/*`): + +1. Create factory with configs from the `th2.common.configuration-directory` environment variable or default path (`/var/th2/config/*`): ``` var factory = CommonFactory(); ``` -1. Create factory with configs from the specified file paths: - ``` - var factory = CommonFactory(rabbitMQ, routerMQ, routerGRPC, cradle, custom, prometheus, dictionariesDir); - ``` 1. Create factory with configs from the specified arguments: ``` var factory = CommonFactory.createFromArguments(args); ``` - You can use one of the following groups of arguments. Arguments from different - groups cannot be used together. - - The first group: + You can use one of the following groups of arguments. Arguments from different + groups cannot be used together. + + The first group: * --rabbitConfiguration - path to json file with RabbitMQ configuration * --messageRouterConfiguration - path to json file with configuration for MessageRouter * --grpcRouterConfiguration - path to json file with configuration for GrpcRouter @@ -31,57 +30,72 @@ Then you will create an instance of imported class, by choosing one of the follo * --dictionariesDir - path to the directory which contains files with the encoded dictionaries * --prometheusConfiguration - path to json file with configuration for prometheus metrics server * --boxConfiguration - path to json file with boxes configuration and information - * -c/--configs - folder with json files for schemas configurations with special names: + * -c/--configs - folder with json files for schemas configurations with special names. + If you doesn't specify -c/--configs common factory uses configs from the `th2.common.configuration-directory` environment variable or default path (`/var/th2/config/*`) + 1. rabbitMq.json - configuration for RabbitMQ 2. mq.json - configuration for MessageRouter 3. grpc.json - configuration for GrpcRouter 4. cradle.json - configuration for cradle 5. custom.json - custom configuration - - The second group: + + The second group: * --namespace - the namespace in Kubernetes to search config maps * --boxName - the name of the target th2 box placed in the specified Kubernetes namespace * --contextName - the context name to search connect parameters in Kube config - * --dictionaries - the mapping between a dictionary in infra schema and a dictionary type in the format: - `--dictionaries =[ =]`. - It can be useful when you required dictionaries to start a specific box. - - Their usage is disclosed further. - + * --dictionaries - the mapping between a dictionary in infra schema and a dictionary type in the format: + `--dictionaries =[ =]`. + It can be useful when you required dictionaries to start a specific box. + + Their usage is disclosed further. + 1. Create factory with a namespace in Kubernetes and the name of the target th2 box from Kubernetes: ``` var factory = CommonFactory.createFromKubernetes(namespace, boxName); ``` - It also can be called by using `createFromArguments(args)` with arguments `--namespace` and `--boxName`. -1. Create factory with a namespace in Kubernetes, the name of the target th2 box from Kubernetes and the name of context to choose from Kube config: + It also can be called by using `createFromArguments(args)` with arguments `--namespace` and `--boxName`. +1. Create factory with a namespace in Kubernetes, the name of the target th2 box from Kubernetes and the name of context + to choose from Kube config: ``` var factory = CommonFactory.createFromKubernetes(namespace, boxName, contextName); ``` - It can also be called by using `createFromArguments(args)` with the arguments `--namespace`, `--boxName` and `--contextName`. - ContextName parameter is `@Nullable`; if it is set to null, the current context will not be changed. + It also can be called by using `createFromArguments(args)` with arguments `--namespace`, `--boxName` + and `--contextName`. + ContextName parameter is `@Nullable`; if it is set to null, the current context will not be changed. ### Configuration formats The `CommonFactory` reads a RabbitMQ configuration from the rabbitMQ.json file. + * host - the required setting defines the RabbitMQ host. -* vHost - the required setting defines the virtual host that will be used for connecting to RabbitMQ. +* vHost - the required setting defines the virtual host that will be used for connecting to RabbitMQ. Please see more details about the virtual host in RabbitMQ via [link](https://www.rabbitmq.com/vhosts.html) * port - the required setting defines the RabbitMQ port. -* username - the required setting defines the RabbitMQ username. +* username - the required setting defines the RabbitMQ username. The user must have permission to publish messages via routing keys and subscribe to message queues. * password - the required setting defines the password that will be used for connecting to RabbitMQ. -* exchangeName - the required setting defines the exchange that will be used for sending/subscribing operation in MQ routers. - Please see more details about the exchanges in RabbitMQ via [link](https://www.rabbitmq.com/tutorials/amqp-concepts.html#exchanges) -* connectionTimeout - the connection TCP establishment timeout in milliseconds with its default value set to 60000. Use zero for infinite waiting. -* connectionCloseTimeout - the timeout in milliseconds for completing all the close-related operations, use -1 for infinity, the default value is set to 10000. -* maxRecoveryAttempts - this option defines the number of reconnection attempts to RabbitMQ, with its default value set to 5. - The `th2_readiness` probe is set to false and publishers are blocked after a lost connection to RabbitMQ. The `th2_readiness` probe is reverted to true if the connection will be recovered during specified attempts otherwise the `th2_liveness` probe will be set to false. -* minConnectionRecoveryTimeout - this option defines a minimal interval in milliseconds between reconnect attempts, with its default value set to 10000. Common factory increases the reconnect interval values from minConnectionRecoveryTimeout to maxConnectionRecoveryTimeout. -* maxConnectionRecoveryTimeout - this option defines a maximum interval in milliseconds between reconnect attempts, with its default value set to 60000. Common factory increases the reconnect interval values from minConnectionRecoveryTimeout to maxConnectionRecoveryTimeout. -* retryTimeDeviationPercent - if the current number of retry attempts is more than maxRecoveryAttempts, then following intervals will be in range `[maxConnectionRecoveryTimeout - deviationPercent%, maxConnectionRecoveryTimeout + deviationPercent%]`. Default value is 10%. -* prefetchCount - this option is the maximum number of messages that the server will deliver, with its value set to 0 if unlimited, the default value is set to 10. +* exchangeName - the required setting defines the exchange that will be used for sending/subscribing operation in MQ + routers. + Please see more details about the exchanges in RabbitMQ + via [link](https://www.rabbitmq.com/tutorials/amqp-concepts.html#exchanges) +* connectionTimeout - the connection TCP establishment timeout in milliseconds with its default value set to 60000. Use zero for + infinite waiting. +* connectionCloseTimeout - the timeout in milliseconds for completing all the close-related operations, use -1 for + infinity, the default value is set to 10000. +* maxRecoveryAttempts - this option defines the number of reconnection attempts to RabbitMQ, with its default value set + to 5. + The `th2_readiness` probe is set to false and publishers are blocked after a lost connection to RabbitMQ. + The `th2_readiness` probe is reverted to true if the connection will be recovered during specified attempts otherwise + the `th2_liveness` probe will be set to false. +* minConnectionRecoveryTimeout - this option defines a minimal interval in milliseconds between reconnect attempts, with + its default value set to 10000. Common factory increases the reconnect interval values from + minConnectionRecoveryTimeout to maxConnectionRecoveryTimeout. +* maxConnectionRecoveryTimeout - this option defines a maximum interval in milliseconds between reconnect attempts, with + its default value set to 60000. Common factory increases the reconnect interval values from + minConnectionRecoveryTimeout to maxConnectionRecoveryTimeout. +* prefetchCount - this option is the maximum number of messages that the server will deliver, with its value set to 0 if + unlimited, the default value is set to 10. * messageRecursionLimit - an integer number denotes how deep the nested protobuf message might be, set by default 100 - ```json { "host": "", @@ -102,6 +116,7 @@ The `CommonFactory` reads a RabbitMQ configuration from the rabbitMQ.json file. ``` The `CommonFactory` reads a message's router configuration from the `mq.json` file. + * queues - the required settings defines all pins for an application * name - routing key in RabbitMQ for sending * queue - queue's name in RabbitMQ for subscribe @@ -110,17 +125,21 @@ The `CommonFactory` reads a message's router configuration from the `mq.json` fi * first * second * subscribe - * publish + * publish * parsed * raw * store * event - + * filters - pin's message's filters * metadata - a metadata filters * message - a message's fields filters - -Filters format: + +* globalNotification - notification exchange in RabbitMQ + * exchange - `global-notification` by default + +Filters format: + * fieldName - a field's name * expectedValue - expected field's value (not used for all operations) * operation - operation's type @@ -157,15 +176,22 @@ Filters format: } ] } + }, + "globalNotification": { + "exchange": "global-notification" } } } ``` The `CommonFactory` reads a gRPC router configuration from the `grpc_router.json` file. -* enableSizeMeasuring - this option enables the gRPC message size measuring. Please note the feature decreases gRPC throughput. Default value is false. + +* enableSizeMeasuring - this option enables the gRPC message size measuring. Please note the feature decreases gRPC + throughput. Default value is false. * keepAliveInterval - number of seconds between keep alive messages. Default value is 60 -* maxMessageSize - this option enables endpoint message filtering based on message size (message with size larger than option value will be skipped). By default, it has a value of `4 MB`. The unit of measurement of the value is number of bytes. +* maxMessageSize - this option enables endpoint message filtering based on message size (message with size larger than + option value will be skipped). By default, it has a value of `4 MB`. The unit of measurement of the value is number of + bytes. ```json { @@ -176,6 +202,7 @@ The `CommonFactory` reads a gRPC router configuration from the `grpc_router.json ``` The `CommonFactory` reads a gRPC configuration from the `grpc.json` file. + * services - grpc services configurations * server - grpc server configuration * endpoint - grpc endpoint configuration @@ -211,17 +238,22 @@ The `CommonFactory` reads a gRPC configuration from the `grpc.json` file. ``` The `CommonFactory` reads a Cradle configuration from the cradle.json file. + * dataCenter - the required setting defines the data center in the Cassandra cluster. * host - the required setting defines the Cassandra host. * port - the required setting defines the Cassandra port. * keyspace - the required setting defines the keyspace (top-level database object) in the Cassandra data center. -* username - the required setting defines the Cassandra username. The user must have permission to write data using a specified keyspace. +* username - the required setting defines the Cassandra username. The user must have permission to write data using a + specified keyspace. * password - the required setting defines the password that will be used for connecting to Cassandra. -* cradleInstanceName - this option defines a special identifier that divides data within one keyspace with infra set as the default value. -* cradleMaxEventBatchSize - this option defines the maximum event batch size in bytes with its default value set to 1048576. -* cradleMaxMessageBatchSize - this option defines the maximum message batch size in bytes with its default value set to 1048576. -* timeout - this option defines connection timeout in milliseconds. If set to 0 or omitted, the default value of 5000 is used. -* pageSize - this option defines the size of the result set to fetch at a time. If set to 0 or omitted, the default value of 5000 is used. +* cradleMaxEventBatchSize - this option defines the maximum event batch size in bytes with its default value set to + 1048576. +* cradleMaxMessageBatchSize - this option defines the maximum message batch size in bytes with its default value set to + 1048576. +* timeout - this option defines connection timeout in milliseconds. If set to 0 or omitted, the default value of 5000 is + used. +* pageSize - this option defines the size of the result set to fetch at a time. If set to 0 or omitted, the default + value of 5000 is used. * prepareStorage - enables database schema initialization if Cradle is used. By default, it has a value of `false` ```json @@ -232,7 +264,6 @@ The `CommonFactory` reads a Cradle configuration from the cradle.json file. "keyspace": "", "username": "", "password": "", - "cradleInstanceName": "", "cradleMaxEventBatchSize": 1048576, "cradleMaxMessageBatchSize": 1048576, "timeout": 5000, @@ -243,49 +274,78 @@ The `CommonFactory` reads a Cradle configuration from the cradle.json file. ### Requirements for creating factory with Kubernetes -1. It is necessary to have Kubernetes configuration written in ~/.kube/config. See more on kubectl configuration [here](https://kubernetes.io/docs/tasks/access-application-cluster/configure-access-multiple-clusters/). +1. It is necessary to have Kubernetes configuration written in ~/.kube/config. See more on kubectl + configuration [here](https://kubernetes.io/docs/tasks/access-application-cluster/configure-access-multiple-clusters/). -1. Also note that `generated_configs` directory will be created to store `.json` files with configs from Kubernetes. Those files are overridden when `CommonFactory.createFromKubernetes(namespace, boxName)` and `CommonFactory.createFromKubernetes(namespace, boxName, contextName)` are invoked again. +1. Also note that `generated_configs` directory will be created to store `.json` files with configs from Kubernetes. + Those files are overridden when `CommonFactory.createFromKubernetes(namespace, boxName)` + and `CommonFactory.createFromKubernetes(namespace, boxName, contextName)` are invoked again. -1. Users need to have authentication with the service account token that has the necessary access to read CRs and secrets from the specified namespace. +1. Users need to have authentication with the service account token that has the necessary access to read CRs and + secrets from the specified namespace. After that you can receive various Routers through factory properties: + ``` -var messageRouter = factory.getMessageRouterParsedBatch(); -var rawRouter = factory.getMessageRouterRawBatch(); -var eventRouter = factory.getEventBatchRouter(); + +var protoMessageGroupRouter = factory.getMessageRouterMessageGroupBatch(); +var protoMessageRouter = factory.getMessageRouterParsedBatch(); +var protoRawRouter = factory.getMessageRouterRawBatch(); +var protoEventRouter = factory.getEventBatchRouter(); + +var transportGroupRouter = factory.getTransportGroupBatchRouter(); ``` -`messageRouter` is working with `MessageBatch`
-`rawRouter` is working with `RawMessageBatch`
-`eventRouter` is working with `EventBatch` +`protoMessageGroupRouter` is working with `MessageGroupBatch`
+`protoMessageRouter` is working with `MessageBatch`
+`protoRawRouter` is working with `RawMessageBatch`
+`protoEventRouter` is working with `EventBatch`
+ +`transportGroupRouter` is working with `GroupBatch`
+ +Note: MessageRouterParsedBatch and MessageRouterRawBatch are not recommended to use because they are adapters for +MessageRouterMessageGroupBatch and execute additional repacking -Please refer to [th2-grpc-common](https://github.com/th2-net/th2-grpc-common/blob/master/src/main/proto/th2_grpc_common/common.proto "common.proto") for further details. +Please refer +to [th2-grpc-common](https://github.com/th2-net/th2-grpc-common/blob/master/src/main/proto/th2_grpc_common/common.proto "common.proto") +for further details. + +With the router created, you can subscribe to pins (by specifying the callback function) or to send data that the router +works with: -With the router created, you can subscribe to pins (by specifying the callback function) or to send data that the router works with: ``` router.subscribe(callback) # subscribe to only one pin router.subscribeAll(callback) # subscribe to one or several pins router.send(message) # send to only one pin router.sendAll(message) # send to one or several pins ``` + You can perform these actions by providing pin attributes in addition to the default ones. + ``` router.subscribe(callback, attrs...) # subscribe to only one pin router.subscribeAll(callback, attrs...) # subscribe to one or several pins router.send(message, attrs...) # send to only one pin router.sendAll(message, attrs...) # send to one or several pins ``` + The default attributes are: -- `message_parsed_batch_router` + +- `proto_message_group_batch_router` + - Subscribe: `subscribe` + - Send: `publish` +- `proto_message_parsed_batch_router` - Subscribe: `subscribe`, `parsed` - Send: `publish`, `parsed` -- `message_raw_batch_router` +- `proto_message_raw_batch_router` - Subscribe: `subscribe`, `raw` - Send: `publish`, `raw` -- `event_batch_router` +- `proto_event_batch_router` - Subscribe: `subscribe`, `event` - Send: `publish`, `event` +- `transport_group_message_batch_router` + - Subscribe: `subscribe`, `transport-group` + - Send: `publish`, `transport-group` This library allows you to: @@ -297,11 +357,54 @@ This kind of router provides the ability for boxes to interact between each othe #### Server -gRPC router rises a gRPC server with enabled [grpc-service-reflection](https://github.com/grpc/grpc-java/blob/master/documentation/server-reflection-tutorial.md#grpc-server-reflection-tutorial) since the 3.38.0 version. -It means that the users can execute calls from the console or through scripts via [grpcurl](https://github.com/fullstorydev/grpcurl#grpcurl) without gRPC schema (files with proto extensions describes gRPC service structure) +gRPC router rises a gRPC server with +enabled [grpc-service-reflection](https://github.com/grpc/grpc-java/blob/master/documentation/server-reflection-tutorial.md#grpc-server-reflection-tutorial) +since the 3.38.0 version. +It means that the users can execute calls from the console or through scripts +via [grpcurl](https://github.com/fullstorydev/grpcurl#grpcurl) without gRPC schema (files with proto extensions +describes gRPC service structure) + +## MQ router + +This kind of router provides the ability for component to send / receive messages via RabbitMQ. +Router has several methods to subscribe and publish RabbitMQ messages steam (th2 use batches of messages or events as transport). + +#### Choice pin by attributes + +Pin attributes are key mechanism to choose pin for action execution. Router search all pins which have full set of passed attributes. +For example, the pins: `first` [`publish`, `raw`, `custom_a` ], `second` [`publish`, `raw`, `custom_b` ]. +* only the `first` pin will be chosen by attribut sets: [`custom_a`], [`custom_a`, `raw`], [`custom_a`, `publish`], [`publish`, `raw`, `custom_a` ] +* both pins will be chosen by attribut sets: [`raw`], [`publish`], [`publish`, `raw` ] + +Router implementation and methods have predefined attributes. Result set of attributes for searching pin is union of , , attributes. +Predefined attributes: +* `RabbitMessageGroupBatchRouter` hasn't got any predefined attributes +* `EventBatchRouter` has `evnet` attribute +* `TransportGroupBatchRouter` has `transport-group` attribute + +* `send*` exclude `sendExclusive` methods have `publish` attribute +* `subscribe*` excluded `subscribeExclusive` methods have `subscribe` attribute + +#### Choice publish pin + +Router chooses pins in two stages. At first select all pins matched by attributes than check passed message (batch) by +pin's filters and then send the whole message or its part via pins leaved after two steps. + +#### Choice subscribe pin + +Router chooses pins only by attributes. Pin's filters are used when message has been delivered and parsed. Registered lister doesn't receive message, or it parts failure check by pin's filter. + +### Restrictions: + +Some methods have `All` suffix, it means that developer can publish or subscribe message via 1 or several pins otherwise via only 1 pin. +If number of passed check pins are different then required range, method throw an exception. + +Developer can register only one listener for each pin but one listener can handle messages from several pins. + +`TransportGroupBatchRouter` router doesn't split incoming or outgoing batch by filter not unlike `RabbitMessageGroupBatchRouter` router ## Export common metrics to Prometheus - + It can be performed by the following utility methods in CommonMetrics class * `setLiveness` - sets "liveness" metric of a service (exported as `th2_liveness` gauge) @@ -311,31 +414,59 @@ NOTES: * in order for the metrics to be exported, you will also need to create an instance of CommonFactory * common JVM metrics will also be exported alongside common service metrics -* some metric labels are enumerations (`th2_type`: `MESSAGE_GROUP`, `EVENT`, ``;`message_type`: `RAW_MESSAGE`, `MESSAGE`) +* some metric labels are + enumerations (`th2_type`: `MESSAGE_GROUP`, `EVENT`, ``;`message_type`: `RAW_MESSAGE`, `MESSAGE`) RABBITMQ METRICS: -* th2_rabbitmq_message_size_publish_bytes (`th2_pin`, `th2_type`, `exchange`, `routing_key`): number of published message bytes to RabbitMQ. The intended is intended for any data transferred via RabbitMQ, for example, th2 batch message or event or custom content -* th2_rabbitmq_message_publish_total (`th2_pin`, `th2_type`, `exchange`, `routing_key`): quantity of published messages to RabbitMQ. The intended is intended for any data transferred via RabbitMQ, for example, th2 batch message or event or custom content -* th2_rabbitmq_message_size_subscribe_bytes (`th2_pin`, `th2_type`, `queue`): number of bytes received from RabbitMQ, it includes bytes of messages dropped after filters. For information about the number of dropped messages, please refer to 'th2_message_dropped_subscribe_total' and 'th2_message_group_dropped_subscribe_total'. The message is intended for any data transferred via RabbitMQ, for example, th2 batch message or event or custom content -* th2_rabbitmq_message_process_duration_seconds (`th2_pin`, `th2_type`, `queue`): time of message processing during subscription from RabbitMQ in seconds. The message is intended for any data transferred via RabbitMQ, for example, th2 batch message or event or custom content + +* th2_rabbitmq_message_size_publish_bytes (`th2_pin`, `th2_type`, `exchange`, `routing_key`): number of published + message bytes to RabbitMQ. The intended is intended for any data transferred via RabbitMQ, for example, th2 batch + message or event or custom content +* th2_rabbitmq_message_publish_total (`th2_pin`, `th2_type`, `exchange`, `routing_key`): quantity of published messages + to RabbitMQ. The intended is intended for any data transferred via RabbitMQ, for example, th2 batch message or event + or custom content +* th2_rabbitmq_message_size_subscribe_bytes (`th2_pin`, `th2_type`, `queue`): number of bytes received from RabbitMQ, it + includes bytes of messages dropped after filters. For information about the number of dropped messages, please refer + to 'th2_message_dropped_subscribe_total' and 'th2_message_group_dropped_subscribe_total'. The message is intended for + any data transferred via RabbitMQ, for example, th2 batch message or event or custom content +* th2_rabbitmq_message_process_duration_seconds (`th2_pin`, `th2_type`, `queue`): time of message processing during + subscription from RabbitMQ in seconds. The message is intended for any data transferred via RabbitMQ, for example, th2 + batch message or event or custom content GRPC METRICS: -* th2_grpc_invoke_call_total (`th2_pin`, `service_name`, `service_method`): total number of calling particular gRPC method -* th2_grpc_invoke_call_request_bytes (`th2_pin`, `service_name`, `service_method`): number of bytes sent to particular gRPC call -* th2_grpc_invoke_call_response_bytes (`th2_pin`, `service_name`, `service_method`): number of bytes sent to particular gRPC call -* th2_grpc_receive_call_total (`th2_pin`, `service_name`, `service_method`): total number of consuming particular gRPC method -* th2_grpc_receive_call_request_bytes (`th2_pin`, `service_name`, `service_method`): number of bytes received from particular gRPC call -* th2_grpc_receive_call_response_bytes (`th2_pin`, `service_name`, `service_method`): number of bytes sent to particular gRPC call + +* th2_grpc_invoke_call_total (`th2_pin`, `service_name`, `service_method`): total number of calling particular gRPC + method +* th2_grpc_invoke_call_request_bytes (`th2_pin`, `service_name`, `service_method`): number of bytes sent to particular + gRPC call +* th2_grpc_invoke_call_response_bytes (`th2_pin`, `service_name`, `service_method`): number of bytes sent to particular + gRPC call +* th2_grpc_receive_call_total (`th2_pin`, `service_name`, `service_method`): total number of consuming particular gRPC + method +* th2_grpc_receive_call_request_bytes (`th2_pin`, `service_name`, `service_method`): number of bytes received from + particular gRPC call +* th2_grpc_receive_call_response_bytes (`th2_pin`, `service_name`, `service_method`): number of bytes sent to particular + gRPC call MESSAGES METRICS: -* th2_message_publish_total (`th2_pin`, `session_alias`, `direction`, `message_type`): quantity of published raw or parsed messages -* th2_message_subscribe_total (`th2_pin`, `session_alias`, `direction`, `message_type`): quantity of received raw or parsed messages, includes dropped after filters. For information about the number of dropped messages, please refer to 'th2_message_dropped_subscribe_total' -* th2_message_dropped_publish_total (`th2_pin`, `session_alias`, `direction`, `message_type`): quantity of published raw or parsed messages dropped after filters -* th2_message_dropped_subscribe_total (`th2_pin`, `session_alias`, `direction`, `message_type`): quantity of received raw or parsed messages dropped after filters -* th2_message_group_publish_total (`th2_pin`, `session_alias`, `direction`): quantity of published message groups -* th2_message_group_subscribe_total (`th2_pin`, `session_alias`, `direction`): quantity of received message groups, includes dropped after filters. For information about the number of dropped messages, please refer to 'th2_message_group_dropped_subscribe_total' -* th2_message_group_dropped_publish_total (`th2_pin`, `session_alias`, `direction`): quantity of published message groups dropped after filters -* th2_message_group_dropped_subscribe_total (`th2_pin`, `session_alias`, `direction`): quantity of received message groups dropped after filters + +* th2_message_publish_total (`th2_pin`, `session_alias`, `direction`, `message_type`): quantity of published raw or + parsed messages +* th2_message_subscribe_total (`th2_pin`, `session_alias`, `direction`, `message_type`): quantity of received raw or + parsed messages, includes dropped after filters. For information about the number of dropped messages, please refer + to 'th2_message_dropped_subscribe_total' +* th2_message_dropped_publish_total (`th2_pin`, `session_alias`, `direction`, `message_type`): quantity of published raw + or parsed messages dropped after filters +* th2_message_dropped_subscribe_total (`th2_pin`, `session_alias`, `direction`, `message_type`): quantity of received + raw or parsed messages dropped after filters +* th2_message_group_publish_total (`th2_pin`, `session_alias`, `direction`): quantity of published message groups +* th2_message_group_subscribe_total (`th2_pin`, `session_alias`, `direction`): quantity of received message groups, + includes dropped after filters. For information about the number of dropped messages, please refer to ' + th2_message_group_dropped_subscribe_total' +* th2_message_group_dropped_publish_total (`th2_pin`, `session_alias`, `direction`): quantity of published message + groups dropped after filters +* th2_message_group_dropped_subscribe_total (`th2_pin`, `session_alias`, `direction`): quantity of received message + groups dropped after filters * th2_message_group_sequence_publish (`th2_pin`, `session_alias`, `direction`): last published sequence * th2_message_group_sequence_subscribe (`th2_pin`, `session_alias`, `direction`): last received sequence @@ -346,7 +477,8 @@ EVENTS METRICS: ### Test extensions: -To be able to use test extensions please fill build.gradle as in the example below: +To be able to use test extensions please fill build.gradle as in the example below: + ```groovy plugins { id 'java-test-fixtures' @@ -359,6 +491,119 @@ dependencies { ## Release notes +### 5.6.0-dev + +#### Added: ++ New methods for transport message builders which allows checking whether the field is set or not ++ Serialization support for date time types (e.g. Instant, LocalDateTime/Date/Time) to event body serialization + +### 5.5.0-dev + +#### Changed: ++ Provided the ability to define configs directory using the `th2.common.configuration-directory` environment variable + +### 5.4.2-dev + +#### Fix + ++ The serialization of `LocalTime`, `LocalDate` and `LocalDateTime` instances corrected for th2 transport parsed message. + Old result would look like `[2023,9,7]`. Corrected serialization result looks like `2023-09-07` + +### 5.4.1-dev +#### Fix ++ `SubscriberMonitor` is returned from `MessageRouter.subscribe` methods is proxy object to manage RabbitMQ subscribtion without internal listener + +### 5.4.0-dev +#### Updated ++ bom: `4.4.0-dev` to `4.5.0-dev` ++ kotlin: `1.8.22` ++ kubernetes-client: `6.1.1` to `6.8.0` + + okhttp: `4.10.0` to `4.11.0` + + okio: `3.0.0` to `3.5.0` + +### 5.3.2-dev + +#### Fix ++ Pin filters behaviour changed: conditions inside the message and metadata now combined as "and" + +#### Feature ++ Added `protocol` field name for MQ pin filter ++ Added `hashCode`, `equals`, `toString`, `toBuilder` for th2 transport classes ++ Added `get` method for th2 transport `MapBuilder`, `CollectionBuilder` classes + +### 5.3.1-dev + ++ Auto-print git metadata from `git.properties` resource file. + Child project should include `com.gorylenko.gradle-git-properties` Gradle plugin to generate required file + +#### Change user code required: ++ Migrated from JsonProcessingException to IOException in Event class methods. + This change allow remove required `jackson.core` dependency from child projects + +### 5.3.0-dev + ++ Implemented message routers used th2 transport protocol for interaction + +#### Updated: ++ cradle: `5.1.1-dev` ++ bom: `4.4.0` ++ grpc-common: `4.3.0-dev` ++ grpc-service-generator: `3.4.0` + +#### Gradle plugins: ++ Updated org.owasp.dependencycheck: `8.3.1` ++ Added com.gorylenko.gradle-git-properties `2.4.1` ++ Added com.github.jk1.dependency-license-report `2.5` ++ Added de.undercouch.download `5.4.0` + +### 5.2.2 + +#### Changed: ++ Book, session group and protocol message filtering added. + +### 5.2.1 + +#### Changed: + ++ The Cradle version is update to 5.0.2-dev-*. + +### 5.2.0 + ++ Merged with 3.44.1 + +### 5.1.1 + ++ Added script for publishing dev-release for maven artefacts ++ Migrated to bom:4.2.0 ++ Migrated to grpc-common:4.1.1-dev + +### 5.1.0 + ++ Migrated to grpc-common 4.1.0 + +### 5.0.0 + ++ Migration to books/pages cradle 4.0.0 ++ Migration to bom 4.0.2 ++ Removed log4j 1.x from dependency ++ Removed `cradleInstanceName` parameter from `cradle.json` ++ Added `prepareStorage` property to `cradle.json` ++ `com.exactpro.th2.common.event.Event.toProto...()` by `parentEventId`/`bookName`/`(bookName + scope)` ++ Added `isRedelivered` flag to message + +--- + +### 3.44.1 + ++ Remove unused dependency ++ Updated bom:4.2.0 + +### 3.43.0 + ++ There is no support for log4j version 1. ++ Work was done to eliminate vulnerabilities in _common_ and _bom_ dependencies. + + **th2-bom must be updated to _4.0.3_ or higher.** + ### 3.42.1 + Added retry in case of a RabbitMQ channel or connection error (when possible). + Added InterruptedException to basicConsume method signature. @@ -367,8 +612,9 @@ dependencies { + Integration tests for these scenarios. ### 3.42.0 -+ Added the `enableSizeMeasuring`, `maxMessageSize`, `keepAliveInterval` options into gRPC router configuration. - Default values are false, 4194304, 60 + ++ Added the `enableSizeMeasuring`, `maxMessageSize`, `keepAliveInterval` options into gRPC router configuration. + Default values are false, 4194304, 60 ### 3.41.1 @@ -377,12 +623,13 @@ dependencies { ### 3.41.0 -+ Work was done to eliminate vulnerabilities in _common_ and _bom_ dependencies. - + **th2-bom must be updated to _4.0.1_ or higher.** ++ Work was done to eliminate vulnerabilities in _common_ and _bom_ dependencies. + + **th2-bom must be updated to _4.0.1_ or higher.** ### 3.40.0 -+ gRPC router creates server support [grpc-service-reflection](https://github.com/grpc/grpc-java/blob/master/documentation/server-reflection-tutorial.md#grpc-server-reflection-tutorial) ++ gRPC router creates server + support [grpc-service-reflection](https://github.com/grpc/grpc-java/blob/master/documentation/server-reflection-tutorial.md#grpc-server-reflection-tutorial) ### 3.39.3 @@ -392,12 +639,13 @@ dependencies { ### 3.39.2 + Fixed: - + The message is not confirmed when it was filtered + + The message is not confirmed when it was filtered ### 3.39.1 + Fixed: - + cradle version updated to 3.1.2. Unicode character sizes are calculated properly during serialization and tests are added for it also. + + cradle version updated to 3.1.2. Unicode character sizes are calculated properly during serialization and tests + are added for it also. ### 3.39.0 @@ -406,8 +654,8 @@ dependencies { ### 3.38.0 + Migration to log4j2. **th2-bom must be updated to _3.2.0_ or higher** - + There is backward compatibility with the log4j format. - + If both configurations are available, log4j2 is preferable. + + There is backward compatibility with the log4j format. + + If both configurations are available, log4j2 is preferable. ### 3.37.2 @@ -416,7 +664,7 @@ dependencies { ### 3.37.1 + Fixed: - + When creating the `CommonFactory` from k8s the logging configuration wouldn't be downloaded + + When creating the `CommonFactory` from k8s the logging configuration wouldn't be downloaded ### 3.37.0 @@ -443,12 +691,13 @@ dependencies { #### Added: + Methods for subscription with manual acknowledgement - (if the **prefetch count** is requested and no messages are acknowledged the reading from the queue will be suspended). + (if the **prefetch count** is requested and no messages are acknowledged the reading from the queue will be + suspended). Please, note that only one subscriber with manual acknowledgement can be subscribed to a queue ### 3.32.1 -+ Fixed: gRPC router didn't shut down underlying Netty's EventLoopGroup and ExecutorService ++ Fixed: gRPC router didn't shut down underlying Netty's EventLoopGroup and ExecutorService ### 3.32.0 @@ -471,6 +720,7 @@ dependencies { + Update grpc-common from 3.8.0 to 3.9.0 ### 3.31.1 + + Feature as test assertion methods for messages from fixtures ### 3.31.0 @@ -505,24 +755,30 @@ dependencies { + Added new metrics and removed old ### 3.26.5 + + Migrated `grpc-common` version from `3.7.0` to `3.8.0` + Added `time_precision` and `decimal_precision` parameters to `RootComparisonSettings` ### 3.26.4 + + Migrated `grpc-common` version from `3.6.0` to `3.7.0` - + Added `check_repeating_group_order` parameter to `RootComparisonSettings` message + + Added `check_repeating_group_order` parameter to `RootComparisonSettings` message ### 3.26.3 + + Migrated `grpc-common` version from `3.5.0` to `3.6.0` - + Added `description` parameter to `RootMessageFilter` message + + Added `description` parameter to `RootMessageFilter` message ### 3.26.2 + + Fix `SimpleFilter` and `ValueFilter` treeTable convertation ### 3.26.1 + + Add `SimpleList` display to `TreeTableEntry`; ### 3.26.0 + + Update the grpc-common version to 3.5.0 + Added `SimpleList` to `SimpleFilter` @@ -533,21 +789,29 @@ dependencies { ### 3.25.1 #### Changed: -+ Extension method for `MessageRouter` now send the event to all pins that satisfy the requested attributes set + ++ Extension method for `MessageRouter` now send the event to all pins that satisfy the requested attributes + set ### 3.25.0 + + Added util to convert RootMessageFilter into readable collection of payload bodies ### 3.24.2 + + Fixed `messageRecursionLimit` - still was not applied to all kind of RabbitMQ subscribers ### 3.24.1 + + Fixed `messageRecursionLimit` setting for all kind of RabbitMQ subscribers ### 3.24.0 -+ Added setting `messageRecursionLimit`(the default value is set to 100) to RabbitMQ configuration that denotes how deep nested protobuf messages might be. + ++ Added setting `messageRecursionLimit`(the default value is set to 100) to RabbitMQ configuration that denotes how deep + nested protobuf messages might be. ### 3.23.0 + + Update the grpc-common version to 3.4.0 + Added `IN`, `LIKE`, `MORE`, `LESS`, `WILDCARD` FilterOperation and their negative versions @@ -557,13 +821,16 @@ dependencies { + Added `WILDCARD` filter operation, which filter a field by wildcard expression. ### 3.21.2 + + Fixed grpc server start. ### 3.21.1 + + Update the grpc-common version to 3.3.0: - + Added information about message timestamp into the checkpoint + + Added information about message timestamp into the checkpoint ### 3.21.0 + + Added classes for management metrics. + Added the ability for resubscribe on canceled subscriber. @@ -597,10 +864,12 @@ dependencies { + Update Cradle version from `2.9.1` to `2.13.0` ### 3.17.0 + + Extended message utility class - + Added the toRootMessageFilter method to convert a message to root message filter + + Added the toRootMessageFilter method to convert a message to root message filter ### 3.16.5 + + Update `th2-grpc-common` and `th2-grpc-service-generator` versions to `3.2.0` and `3.1.12` respectively ### 3.16.4 @@ -610,7 +879,8 @@ dependencies { ### 3.16.3 + Change the way that channels are stored (they are mapped to the pin instead of to the thread). - It might increase the average number of channels used by the box, but it also limits the max number of channels to the number of pins + It might increase the average number of channels used by the box, but it also limits the max number of channels to the + number of pins ### 3.16.2 @@ -618,13 +888,14 @@ dependencies { **NOTE: one of the methods was not restored and an update to this version might require manual update for your code**. The old methods without `toTraceString` supplier will be removed in the future + Fixed configuration for gRPC server. - + Added the property `workers`, which changes the count of gRPC server's threads - + + Added the property `workers`, which changes the count of gRPC server's threads + ### 3.16.0 + Extended Utility classes - + Added the toTreeTable method to convert message/message filter to event data - + Added the Event.exception method to include an exception and optionally all the causes to the body data as a series of messages + + Added the toTreeTable method to convert message/message filter to event data + + Added the Event.exception method to include an exception and optionally all the causes to the body data as a + series of messages ### 3.15.0 @@ -639,7 +910,7 @@ dependencies { + resets embedded `log4j` configuration before configuring it from a file -### 3.13.5 +### 3.13.5 + fixed a bug with message filtering by `message_type` @@ -657,16 +928,18 @@ dependencies { ### 3.13.1 -+ removed gRPC event loop handling ++ removed gRPC event loop handling + fixed dictionary reading -### 3.13.0 +### 3.13.0 + + reads dictionaries from the /var/th2/config/dictionary folder. -+ uses mq_router, grpc_router, cradle_manager optional JSON configs from the /var/th2/config folder ++ uses mq_router, grpc_router, cradle_manager optional JSON configs from the `/var/th2/config` folder ### 3.11.0 -+ tries to load log4j.properties files from sources in order: '/var/th2/config', '/home/etc', configured path via cmd, default configuration ++ tries to load log4j.properties files from sources in order: '/var/th2/config', '/home/etc', configured path via cmd, + default configuration ### 3.6.0 @@ -674,4 +947,5 @@ dependencies { ### 3.0.1 -+ metrics related to time measurement of an incoming message handling (Raw / Parsed / Event) migrated to Prometheus [histogram](https://prometheus.io/docs/concepts/metric_types/#histogram) ++ metrics related to time measurement of an incoming message handling (Raw / Parsed / Event) migrated to + Prometheus [histogram](https://prometheus.io/docs/concepts/metric_types/#histogram) diff --git a/build.gradle b/build.gradle index c870b05e4..5d1a3a08e 100644 --- a/build.gradle +++ b/build.gradle @@ -1,10 +1,14 @@ +import com.github.jk1.license.filter.LicenseBundleNormalizer +import com.github.jk1.license.render.JsonReportRenderer +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + buildscript { repositories { mavenCentral() } ext { - kotlin_version = "1.6.21" + kotlin_version = "1.8.22" } dependencies { @@ -21,67 +25,76 @@ plugins { id 'signing' id 'com.google.protobuf' version '0.8.8' apply false id 'org.jetbrains.kotlin.jvm' version "${kotlin_version}" - id "org.owasp.dependencycheck" version "7.2.0" + id 'org.jetbrains.kotlin.kapt' version "${kotlin_version}" + id "org.owasp.dependencycheck" version "8.3.1" id "me.champeau.jmh" version "0.6.8" + id "com.gorylenko.gradle-git-properties" version "2.4.1" + id 'com.github.jk1.dependency-license-report' version '2.5' + id "de.undercouch.download" version "5.4.0" } group = 'com.exactpro.th2' version = release_version -sourceCompatibility = 11 -targetCompatibility = 11 - ext { - cradleVersion = '3.1.2' - junitVersion = '5.8.2' - sharedDir = file("${project.rootDir}/shared") + cradleVersion = '5.1.1-dev' + junitVersion = '5.10.0' } repositories { mavenCentral() maven { - name 'Sonatype_snapshots' - url 'https://s01.oss.sonatype.org/content/repositories/snapshots/' + name 'Sonatype_snapshots' + url 'https://s01.oss.sonatype.org/content/repositories/snapshots/' } maven { - name 'Sonatype_releases' - url 'https://s01.oss.sonatype.org/content/repositories/releases/' + name 'Sonatype_releases' + url 'https://s01.oss.sonatype.org/content/repositories/releases/' } mavenLocal() - configurations.all { + configurations.configureEach { resolutionStrategy.cacheChangingModulesFor 0, 'seconds' resolutionStrategy.cacheDynamicVersionsFor 0, 'seconds' } } +configurations { + compileClasspath { + resolutionStrategy.activateDependencyLocking() + } +} + java { + sourceCompatibility = 11 + targetCompatibility = 11 + withJavadocJar() withSourcesJar() } // conditionals for publications -tasks.withType(PublishToMavenRepository) { +tasks.withType(PublishToMavenRepository).configureEach { onlyIf { (repository == publishing.repositories.nexusRepository && - project.hasProperty('nexus_user') && - project.hasProperty('nexus_password') && - project.hasProperty('nexus_url')) || - (repository == publishing.repositories.sonatype && - project.hasProperty('sonatypeUsername') && - project.hasProperty('sonatypePassword')) || - (repository == publishing.repositories.localRepo) + project.hasProperty('nexus_user') && + project.hasProperty('nexus_password') && + project.hasProperty('nexus_url')) || + (repository == publishing.repositories.sonatype && + project.hasProperty('sonatypeUsername') && + project.hasProperty('sonatypePassword')) } } -tasks.withType(Sign) { - onlyIf { project.hasProperty('signingKey') && - project.hasProperty('signingPassword') +tasks.withType(Sign).configureEach { + onlyIf { + project.hasProperty('signingKey') && + project.hasProperty('signingPassword') } } // disable running task 'initializeSonatypeStagingRepository' on a gitlab -tasks.whenTaskAdded {task -> - if(task.name.equals('initializeSonatypeStagingRepository') && - !(project.hasProperty('sonatypeUsername') && project.hasProperty('sonatypePassword')) +tasks.configureEach { task -> + if (task.name.equals('initializeSonatypeStagingRepository') && + !(project.hasProperty('sonatypeUsername') && project.hasProperty('sonatypePassword')) ) { task.enabled = false } @@ -92,37 +105,33 @@ publishing { mavenJava(MavenPublication) { from(components.java) pom { - name = rootProject.name - packaging = 'jar' - description = rootProject.description - url = vcs_url - scm { + name = rootProject.name + packaging = 'jar' + description = rootProject.description url = vcs_url - } - licenses { - license { - name = 'The Apache License, Version 2.0' - url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' + scm { + url = vcs_url } - } - developers { - developer { - id = 'developer' - name = 'developer' - email = 'developer@exactpro.com' + licenses { + license { + name = 'The Apache License, Version 2.0' + url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' + } + } + developers { + developer { + id = 'developer' + name = 'developer' + email = 'developer@exactpro.com' + } + } + scm { + url = vcs_url } - } - scm { - url = vcs_url - } } } } repositories { - maven { - name = 'localRepo' - url = sharedDir - } //Nexus repo to publish from gitlab maven { name = 'nexusRepository' @@ -144,13 +153,9 @@ nexusPublishing { } } -clean { - delete sharedDir -} - signing { - def signingKey = findProperty("signingKey") - def signingPassword = findProperty("signingPassword") + String signingKey = findProperty("signingKey") + String signingPassword = findProperty("signingPassword") useInMemoryPgpKeys(signingKey, signingPassword) sign publishing.publications.mavenJava } @@ -169,20 +174,32 @@ tasks.register('integrationTest', Test) { } dependencies { - api platform("com.exactpro.th2:bom:4.0.2") - api ("com.exactpro.th2:cradle-core:${cradleVersion}") { - exclude group: 'org.slf4j', module: 'slf4j-log4j12' // because of the vulnerability + api platform("com.exactpro.th2:bom:4.5.0") + api('com.exactpro.th2:grpc-common:4.3.0-dev') { + because('protobuf transport is main now, this dependnecy should be moved to grpc, mq protobuf modules after splitting') + } + api("com.exactpro.th2:cradle-core:${cradleVersion}") { + because('cradle is included into common library now, this dependnecy should be moved to a cradle module after splitting') + } + api('io.netty:netty-buffer') { + because('th2 transport protocol is included into common library now, this dependnecy should be moved to a th2 transport module after splitting') } - api 'com.exactpro.th2:grpc-common:3.11.1' jmh 'org.openjdk.jmh:jmh-core:0.9' jmh 'org.openjdk.jmh:jmh-generator-annprocess:0.9' implementation 'com.google.protobuf:protobuf-java-util' - implementation 'com.exactpro.th2:grpc-service-generator:3.2.2' - implementation ("com.exactpro.th2:cradle-cassandra:${cradleVersion}") { - exclude group: 'org.slf4j', module: 'slf4j-log4j12' // because of the vulnerability + implementation 'com.exactpro.th2:grpc-service-generator:3.4.0' + implementation "com.exactpro.th2:cradle-cassandra:${cradleVersion}" + + def autoValueVersion = '1.10.1' + implementation "com.google.auto.value:auto-value-annotations:$autoValueVersion" + kapt("com.google.auto.value:auto-value:$autoValueVersion") { + //FIXME: Updated library because it is fat jar + // auto-value-1.10.1.jar/META-INF/maven/com.google.guava/guava/pom.xml (pkg:maven/com.google.guava/guava@31.1-jre, cpe:2.3:a:google:guava:31.1:*:*:*:*:*:*:*) : CVE-2023-2976, CVE-2020-8908 } + //this is required to add generated bridge classes for kotlin default constructors + implementation(files("$buildDir/tmp/kapt3/classes/main")) //FIXME: Add these dependencies as api to grpc-... artifacts implementation "io.grpc:grpc-protobuf" @@ -197,25 +214,26 @@ dependencies { implementation "org.apache.commons:commons-lang3" implementation "org.apache.commons:commons-collections4" implementation "org.apache.commons:commons-text" + implementation("commons-io:commons-io") { + because('we need FilenameUtil to use wildcard matching') + } implementation "commons-cli:commons-cli" + implementation "commons-io:commons-io" implementation "com.fasterxml.jackson.core:jackson-core" - api("com.fasterxml.jackson.core:jackson-databind") { + implementation("com.fasterxml.jackson.core:jackson-databind") { because('provide ability to use object mapper in components') } - api("com.fasterxml.jackson.core:jackson-annotations") { + implementation("com.fasterxml.jackson.core:jackson-annotations") { because('providee ability to use jackson annotations in components') } implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" - implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml" implementation "com.fasterxml.jackson.module:jackson-module-kotlin" + implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-cbor' implementation 'com.fasterxml.uuid:java-uuid-generator:4.0.1' - api 'io.github.microutils:kotlin-logging:2.1.21' - implementation 'org.apache.logging.log4j:log4j-slf4j2-impl' - implementation 'org.apache.logging.log4j:log4j-1.2-api' implementation 'org.apache.logging.log4j:log4j-core' implementation 'io.prometheus:simpleclient' @@ -223,16 +241,26 @@ dependencies { implementation 'io.prometheus:simpleclient_httpserver' implementation 'io.prometheus:simpleclient_log4j2' - implementation ('com.squareup.okhttp3:okhttp:4.10.0') { - because ('fix vulnerability in transitive dependency ') + implementation('com.squareup.okio:okio:3.5.0') { + because('fix vulnerability in transitive dependency ') + } + implementation('com.squareup.okhttp3:okhttp:4.11.0') { + because('fix vulnerability in transitive dependency ') + } + implementation('io.fabric8:kubernetes-client:6.8.0') { + exclude group: 'com.fasterxml.jackson.dataformat', module: 'jackson-dataformat-yaml' } - implementation 'io.fabric8:kubernetes-client:6.1.1' + + implementation 'io.github.microutils:kotlin-logging:3.0.0' // The last version bases on kotlin 1.6.0 testImplementation "org.junit.jupiter:junit-jupiter:${junitVersion}" testImplementation 'org.mockito.kotlin:mockito-kotlin:4.0.0' - testImplementation 'org.testcontainers:testcontainers:1.17.4' - testImplementation 'org.testcontainers:rabbitmq:1.17.4' - + testImplementation 'org.jetbrains.kotlin:kotlin-test-junit5' + testImplementation "org.testcontainers:testcontainers:1.17.4" + testImplementation "org.testcontainers:rabbitmq:1.17.4" + testImplementation("org.junit-pioneer:junit-pioneer:2.1.0") { + because("system property tests") + } testFixturesImplementation group: 'org.jetbrains.kotlin', name: 'kotlin-test-junit5', version: kotlin_version testFixturesImplementation "org.junit.jupiter:junit-jupiter:${junitVersion}" } @@ -240,13 +268,13 @@ dependencies { jar { manifest { attributes( - 'Created-By': "${System.getProperty('java.version')} (${System.getProperty('java.vendor')})", - 'Specification-Title': '', - 'Specification-Vendor': 'Exactpro Systems LLC', - 'Implementation-Title': project.archivesBaseName, - 'Implementation-Vendor': 'Exactpro Systems LLC', + 'Created-By': "${System.getProperty('java.version')} (${System.getProperty('java.vendor')})", + 'Specification-Title': '', + 'Specification-Vendor': 'Exactpro Systems LLC', + 'Implementation-Title': project.archivesBaseName, + 'Implementation-Vendor': 'Exactpro Systems LLC', 'Implementation-Vendor-Id': 'com.exactpro', - 'Implementation-Version': project.version + 'Implementation-Version': project.version ) } } @@ -255,21 +283,39 @@ sourceSets { main.kotlin.srcDirs += "src/main/kotlin" } -tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { +tasks.withType(KotlinCompile).configureEach { kotlinOptions.jvmTarget = "11" -} - -clean { - delete sharedDir + kotlinOptions.freeCompilerArgs += "-Xjvm-default=all" } dependencyCheck { - formats=['JSON', 'HTML'] - failBuildOnCVSS=5 - + formats = ['SARIF', 'JSON', 'HTML'] + failBuildOnCVSS = 5 + suppressionFile = file('suppressions.xml') analyzers { assemblyEnabled = false nugetconfEnabled = false nodeEnabled = false } +} + +licenseReport { + def licenseNormalizerBundlePath = "$buildDir/license-normalizer-bundle.json" + + if (!file(licenseNormalizerBundlePath).exists()) { + download.run { + src 'https://raw.githubusercontent.com/th2-net/.github/main/license-compliance/gradle-license-report/license-normalizer-bundle.json' + dest "$buildDir/license-normalizer-bundle.json" + overwrite false + } + } + + filters = [ + new LicenseBundleNormalizer(licenseNormalizerBundlePath, false) + ] + renderers = [ + new JsonReportRenderer('licenses.json', false), + ] + excludeOwnGroup = false + allowedLicensesFile = new URL("https://raw.githubusercontent.com/th2-net/.github/main/license-compliance/gradle-license-report/allowed-licenses.json") } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index ed2a9768c..5962a2f74 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,6 @@ # -# Copyright 2022 Exactpro (Exactpro Systems Limited) +# Copyright 2022-2023 Exactpro (Exactpro Systems Limited) +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -12,9 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # - -release_version=3.42.1 - -description = 'th2 common library (Java)' - +release_version=5.6.0 +description='th2 common library (Java)' vcs_url=https://github.com/th2-net/th2-common-j +kapt.include.compile.classpath=false diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 7d1733249..8c6e09b97 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ #Thu Jul 02 11:31:27 GMT+04:00 2020 -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStorePath=wrapper/dists diff --git a/src/jmh/kotlin/MetricsUtilsBenchmark.kt b/src/jmh/kotlin/com/exactpro/th2/common/metrics/MetricsUtilsBenchmark.kt similarity index 93% rename from src/jmh/kotlin/MetricsUtilsBenchmark.kt rename to src/jmh/kotlin/com/exactpro/th2/common/metrics/MetricsUtilsBenchmark.kt index 31d5150bd..02a94b48c 100644 --- a/src/jmh/kotlin/MetricsUtilsBenchmark.kt +++ b/src/jmh/kotlin/com/exactpro/th2/common/metrics/MetricsUtilsBenchmark.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.exactpro.th2 +package com.exactpro.th2.common.metrics import com.exactpro.th2.common.grpc.AnyMessage import com.exactpro.th2.common.grpc.Direction @@ -24,13 +24,6 @@ import com.exactpro.th2.common.message.direction import com.exactpro.th2.common.message.plusAssign import com.exactpro.th2.common.message.sequence import com.exactpro.th2.common.message.sessionAlias -import com.exactpro.th2.common.metrics.DIRECTION_LABEL -import com.exactpro.th2.common.metrics.MESSAGE_TYPE_LABEL -import com.exactpro.th2.common.metrics.SESSION_ALIAS_LABEL -import com.exactpro.th2.common.metrics.TH2_PIN_LABEL -import com.exactpro.th2.common.metrics.incrementDroppedMetrics -import com.exactpro.th2.common.metrics.incrementTotalMetrics -import com.exactpro.th2.common.metrics.incrementTotalMetricsOld import io.prometheus.client.Counter import io.prometheus.client.Gauge import org.openjdk.jmh.annotations.Benchmark diff --git a/src/main/java/com/exactpro/th2/common/ConfigurationUtils.java b/src/main/java/com/exactpro/th2/common/ConfigurationUtils.java index 1259c843a..42babd6b0 100644 --- a/src/main/java/com/exactpro/th2/common/ConfigurationUtils.java +++ b/src/main/java/com/exactpro/th2/common/ConfigurationUtils.java @@ -24,6 +24,7 @@ import java.nio.file.Paths; import java.util.function.Supplier; +import com.fasterxml.jackson.core.JsonFactory; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @@ -31,12 +32,11 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; public class ConfigurationUtils { private static final Logger LOGGER = LoggerFactory.getLogger(ConfigurationUtils.class); - private static final ObjectMapper YAML_READER = new ObjectMapper(new YAMLFactory()); + private static final ObjectMapper JSON_READER = new ObjectMapper(new JsonFactory()); @Nullable public static String getEnv(String key, @Nullable String defaultValue) { @@ -47,7 +47,7 @@ public static String getEnv(String key, @Nullable String defaultValue) { public static T getJsonEnv(String key, TypeReference cls) { String env = getEnv(key, null); try { - return env == null ? null : YAML_READER.readValue(env, cls); + return env == null ? null : JSON_READER.readValue(env, cls); } catch (IOException e) { LOGGER.error("Can not parse json environment variable with key: " + key, e); return null; @@ -55,7 +55,7 @@ public static T getJsonEnv(String key, TypeReference cls) { } public static T load(Class _class, InputStream inputStream) throws IOException { - return YAML_READER.readValue(inputStream, _class); + return JSON_READER.readValue(inputStream, _class); } /** diff --git a/src/main/java/com/exactpro/th2/common/event/Event.java b/src/main/java/com/exactpro/th2/common/event/Event.java index dfc01e03c..0f2e2143a 100644 --- a/src/main/java/com/exactpro/th2/common/event/Event.java +++ b/src/main/java/com/exactpro/th2/common/event/Event.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2023 Exactpro (Exactpro Systems Limited) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,51 +15,69 @@ */ package com.exactpro.th2.common.event; -import static com.exactpro.th2.common.event.EventUtils.createMessageBean; -import static com.exactpro.th2.common.event.EventUtils.generateUUID; -import static com.exactpro.th2.common.event.EventUtils.toEventID; -import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL; -import static com.google.protobuf.TextFormat.shortDebugString; -import static java.util.Objects.requireNonNull; -import static java.util.Objects.requireNonNullElse; -import static org.apache.commons.lang3.StringUtils.defaultIfBlank; -import static org.apache.commons.lang3.StringUtils.isNotBlank; +import com.exactpro.th2.common.grpc.EventBatch; +import com.exactpro.th2.common.grpc.EventBatchOrBuilder; +import com.exactpro.th2.common.grpc.EventID; +import com.exactpro.th2.common.grpc.EventStatus; +import com.exactpro.th2.common.grpc.MessageID; +import com.exactpro.th2.common.message.MessageUtils; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.module.kotlin.KotlinFeature; +import com.fasterxml.jackson.module.kotlin.KotlinModule; +import com.google.protobuf.ByteString; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.io.IOException; import java.time.Instant; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.Objects; +import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; -import org.jetbrains.annotations.Contract; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.exactpro.th2.common.grpc.EventBatch; -import com.exactpro.th2.common.grpc.EventBatchOrBuilder; -import com.exactpro.th2.common.grpc.EventID; -import com.exactpro.th2.common.grpc.EventStatus; -import com.exactpro.th2.common.grpc.MessageID; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.protobuf.ByteString; -import com.google.protobuf.Timestamp; +import static com.exactpro.th2.common.event.EventUtils.createMessageBean; +import static com.exactpro.th2.common.event.EventUtils.generateUUID; +import static com.exactpro.th2.common.event.EventUtils.requireNonBlankBookName; +import static com.exactpro.th2.common.event.EventUtils.requireNonBlankScope; +import static com.exactpro.th2.common.event.EventUtils.requireNonNullParentId; +import static com.exactpro.th2.common.event.EventUtils.requireNonNullTimestamp; +import static com.exactpro.th2.common.event.EventUtils.toEventID; +import static com.exactpro.th2.common.event.EventUtils.toTimestamp; +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL; +import static com.google.protobuf.TextFormat.shortDebugString; +import static java.util.Objects.requireNonNull; +import static java.util.Objects.requireNonNullElse; +import static org.apache.commons.lang3.StringUtils.defaultIfBlank; +import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.apache.commons.lang3.StringUtils.isNotBlank; +//TODO: move to common-utils-j +@SuppressWarnings("unused") public class Event { private static final Logger LOGGER = LoggerFactory.getLogger(Event.class); - public static final String UNKNOWN_EVENT_NAME = "Unknown event name"; public static final String UNKNOWN_EVENT_TYPE = "Unknown event type"; public static final EventID DEFAULT_EVENT_ID = EventID.getDefaultInstance(); - - protected static final ThreadLocal OBJECT_MAPPER = ThreadLocal.withInitial(() -> new ObjectMapper().setSerializationInclusion(NON_NULL)); - - protected final String id = generateUUID(); + protected static final ThreadLocal OBJECT_MAPPER = ThreadLocal.withInitial(() -> + new ObjectMapper() + .registerModule(new KotlinModule.Builder() + .enable(KotlinFeature.SingletonSupport) + .build()) + .registerModule(new JavaTimeModule()) + // otherwise, type supported by JavaTimeModule will be serialized as array of date component + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .setSerializationInclusion(NON_NULL)); + protected final String UUID = generateUUID(); + protected final AtomicLong ID_COUNTER = new AtomicLong(); + + protected final String id = UUID + '-' + ID_COUNTER.incrementAndGet(); protected final List subEvents = new ArrayList<>(); protected final List attachedMessageIDS = new ArrayList<>(); protected final List body = new ArrayList<>(); @@ -70,12 +88,15 @@ public class Event { protected String description; protected Status status = Status.PASSED; - protected Event(Instant startTimestamp, @Nullable Instant endTimestamp) { - this.startTimestamp = startTimestamp; + protected Event( + @NotNull Instant startTimestamp, + @Nullable Instant endTimestamp + ) { + this.startTimestamp = requireNonNullTimestamp(startTimestamp); this.endTimestamp = endTimestamp; } - protected Event(Instant startTimestamp) { + protected Event(@NotNull Instant startTimestamp) { this(startTimestamp, null); } @@ -85,6 +106,7 @@ protected Event() { /** * Creates event with current time as start + * * @return new event */ public static Event start() { @@ -93,23 +115,13 @@ public static Event start() { /** * Creates event with passed time as start + * * @return new event */ - public static Event from(Instant startTimestamp) { + public static Event from(@NotNull Instant startTimestamp) { return new Event(startTimestamp); } - @Contract("null -> null") - private static @Nullable Timestamp toTimestamp(@Nullable Instant instant) { - if (instant == null) { - return null; - } - return Timestamp.newBuilder() - .setSeconds(instant.getEpochSecond()) - .setNanos(instant.getNano()) - .build(); - } - public Event endTimestamp() { if (endTimestamp != null) { throw new IllegalStateException(formatStateException("End time", endTimestamp)); @@ -120,7 +132,8 @@ public Event endTimestamp() { /** * Sets event name if passed {@code eventName} is not blank. - * The {@link #UNKNOWN_EVENT_NAME} value will be used as default in the {@link #toProtoEvent(String)} and {@link #toProtoEvents(String)} methods if this property isn't set + * The {@link #UNKNOWN_EVENT_NAME} value will be used as default in the {@link #toProto(com.exactpro.th2.common.grpc.EventID)} and {@link #toListProto(com.exactpro.th2.common.grpc.EventID)} methods if this property isn't set + * * @return current event * @throws IllegalStateException if name already set */ @@ -136,7 +149,8 @@ public Event name(String eventName) { /** * Sets event description if passed {@code description} is not blank. - * This property value will be appended to the end of event name and added into event body in the {@link #toProtoEvent(String)} and {@link #toProtoEvents(String)} methods if this property isn't set + * This property value will be appended to the end of event name and added into event body in the {@link #toProto(com.exactpro.th2.common.grpc.EventID)} and {@link #toListProto(com.exactpro.th2.common.grpc.EventID)} methods if this property isn't set + * * @return current event * @throws IllegalStateException if description already set */ @@ -153,7 +167,8 @@ public Event description(String description) { /** * Sets event type if passed {@code eventType} is not blank. - * The {@link #UNKNOWN_EVENT_TYPE} value will be used as default in the {@link #toProtoEvent(String)} and {@link #toProtoEvents(String)} methods if this property isn't set + * The {@link #UNKNOWN_EVENT_TYPE} value will be used as default in the {@link #toProto(com.exactpro.th2.common.grpc.EventID)} and {@link #toListProto(com.exactpro.th2.common.grpc.EventID)} methods if this property isn't set + * * @return current event * @throws IllegalStateException if type already set */ @@ -170,6 +185,7 @@ public Event type(String eventType) { /** * Sets event status if passed {@code eventStatus} isn't null. * The default value is {@link Status#PASSED} + * * @return current event */ public Event status(Status eventStatus) { @@ -181,6 +197,7 @@ public Event status(Status eventStatus) { /** * Creates and adds a new event with the same start / end time as the current event + * * @return created event */ @SuppressWarnings("NonBooleanMethodNameMayNotStartWithQuestion") @@ -190,6 +207,7 @@ public Event addSubEventWithSamePeriod() { /** * Adds passed event as a sub event + * * @return passed event * @throws NullPointerException if {@code subEvent} is null */ @@ -201,6 +219,7 @@ public Event addSubEvent(Event subEvent) { /** * Adds passed body data bodyData + * * @return current event */ public Event bodyData(IBodyData bodyData) { @@ -210,6 +229,7 @@ public Event bodyData(IBodyData bodyData) { /** * Adds passed collection of body data + * * @return current event */ public Event bodyData(Collection bodyDataCollection) { @@ -219,6 +239,7 @@ public Event bodyData(Collection bodyDataCollection) { /** * Adds the passed exception and optionally all the causes to the body data as a series of messages + * * @param includeCauses if `true` attache messages for the caused of throwable * @return current event */ @@ -233,35 +254,97 @@ public Event exception(@NotNull Throwable throwable, boolean includeCauses) { /** * Adds message id as linked + * * @return current event */ public Event messageID(MessageID attachedMessageID) { - attachedMessageIDS.add(requireNonNull(attachedMessageID, "Attached message id can't be null")); + requireNonNull(attachedMessageID, "Attached message id can't be null"); + if (MessageUtils.isValid(attachedMessageID)) { + attachedMessageIDS.add(attachedMessageID); + } else { + throw new IllegalArgumentException("Attached " + MessageUtils.toJson(attachedMessageID) + " message id"); + } return this; } - /** - * @deprecated prefer to use full object instead of part of them, use the {@link #toListProto(EventID)} method - */ - @Deprecated - public List toProtoEvents(@Nullable String parentID) throws JsonProcessingException { - return toListProto(toEventID(parentID)); + public List toListProto(@NotNull EventID parentId) throws IOException { + return toListProto( + new ArrayList<>(), + requireNonNullParentId(parentId), + requireNonBlankBookName(parentId.getBookName()), + parentId.getScope() + ); + } + + public List toListProto(@NotNull String bookName) throws IOException { + return toListProto( + new ArrayList<>(), + null, + requireNonBlankBookName(bookName), + null + ); + } + + public List toListProto( + @NotNull String bookName, + @NotNull String scope + ) throws IOException { + return toListProto( + new ArrayList<>(), + null, + requireNonBlankBookName(bookName), + requireNonBlankScope(scope) + ); + } + + private List toListProto( + List protoEvents, + @Nullable EventID parentId, + @NotNull String bookName, + @Nullable String scope + ) throws IOException { + protoEvents.add(toProto(parentId, bookName, scope)); // collect current level + for (Event subEvent : subEvents) { + EventID eventId = isBlank(scope) + ? toEventID(startTimestamp, bookName, id) + : toEventID(startTimestamp, bookName, scope, id); + subEvent.toListProto(protoEvents, eventId, bookName, scope); // collect sub level + } + return protoEvents; } + public com.exactpro.th2.common.grpc.Event toProto(@NotNull EventID parentId) throws IOException { + return toProto( + requireNonNullParentId(parentId), + requireNonBlankBookName(parentId.getBookName()), + parentId.getScope() + ); + } - public List toListProto(@Nullable EventID parentID) throws JsonProcessingException { - return collectSubEvents(new ArrayList<>(), parentID); + public com.exactpro.th2.common.grpc.Event toProto(@NotNull String bookName) throws IOException { + return toProto( + null, + requireNonBlankBookName(bookName), + null + ); } - /** - * @deprecated prefer to use full object instead of part of them, use the {@link #toProto(EventID)} method - */ - @Deprecated - public com.exactpro.th2.common.grpc.Event toProtoEvent(@Nullable String parentID) throws JsonProcessingException { - return toProto(toEventID(parentID)); + public com.exactpro.th2.common.grpc.Event toProto( + @NotNull String bookName, + @Nullable String scope + ) throws IOException { + return toProto( + null, + requireNonBlankBookName(bookName), + scope + ); } - public com.exactpro.th2.common.grpc.Event toProto(@Nullable EventID parentID) throws JsonProcessingException { + private com.exactpro.th2.common.grpc.Event toProto( + @Nullable EventID parentId, + @NotNull String bookName, + @Nullable String scope + ) throws IOException { if (endTimestamp == null) { endTimestamp(); } @@ -270,16 +353,18 @@ public com.exactpro.th2.common.grpc.Event toProto(@Nullable EventID parentID) th nameBuilder.append(" - ") .append(description); } + EventID eventId = isBlank(scope) + ? toEventID(startTimestamp, bookName, id) + : toEventID(startTimestamp, bookName, scope, id); var eventBuilder = com.exactpro.th2.common.grpc.Event.newBuilder() - .setId(toEventID(id)) + .setId(eventId) .setName(nameBuilder.toString()) .setType(defaultIfBlank(type, UNKNOWN_EVENT_TYPE)) - .setStartTimestamp(toTimestamp(startTimestamp)) .setEndTimestamp(toTimestamp(endTimestamp)) .setStatus(getAggregatedStatus().eventStatus) .setBody(ByteString.copyFrom(buildBody())); - if (parentID != null) { - eventBuilder. setParentId(parentID); + if (parentId != null) { + eventBuilder.setParentId(parentId); } for (MessageID messageID : attachedMessageIDS) { eventBuilder.addAttachedMessageIds(messageID); @@ -287,49 +372,159 @@ public com.exactpro.th2.common.grpc.Event toProto(@Nullable EventID parentID) th return eventBuilder.build(); } - public EventBatch toBatchProto(@Nullable EventID parentID) throws JsonProcessingException { - List events = toListProto(parentID); + public EventBatch toBatchProto(@NotNull EventID parentId) throws IOException { + return toBatchProto( + requireNonNullParentId(parentId), + requireNonBlankBookName(parentId.getBookName()), + parentId.getScope() + ); + } + + public EventBatch toBatchProto(@NotNull String bookName) throws IOException { + return toBatchProto( + null, + requireNonBlankBookName(bookName), + null + ); + } + + public EventBatch toBatchProto( + @NotNull String bookName, + @NotNull String scope + ) throws IOException { + return toBatchProto( + null, + requireNonBlankBookName(bookName), + requireNonBlankScope(scope) + ); + } + + private EventBatch toBatchProto( + @Nullable EventID parentId, + @NotNull String bookName, + @Nullable String scope + ) throws IOException { + List events = toListProto(new ArrayList<>(), parentId, bookName, scope); EventBatch.Builder builder = EventBatch.newBuilder() - .addAllEvents(events); + .addAllEvents(events); - if (parentID != null && events.size() != 1) { - builder.setParentEventId(parentID); + if (parentId != null && events.size() != 1) { + builder.setParentEventId(parentId); } return builder.build(); } + public List toBatchesProtoWithLimit( + int maxEventBatchContentSize, + @NotNull EventID parentId + ) throws IOException { + return toBatchesProtoWithLimit( + maxEventBatchContentSize, + requireNonNullParentId(parentId), + requireNonBlankBookName(parentId.getBookName()), + parentId.getScope() + ); + } + + public List toBatchesProtoWithLimit( + int maxEventBatchContentSize, + @NotNull String bookName + ) throws IOException { + return toBatchesProtoWithLimit( + maxEventBatchContentSize, + null, + requireNonBlankBookName(bookName), + null + ); + } + + public List toBatchesProtoWithLimit( + int maxEventBatchContentSize, + @NotNull String bookName, + @NotNull String scope + ) throws IOException { + return toBatchesProtoWithLimit( + maxEventBatchContentSize, + null, + requireNonBlankBookName(bookName), + requireNonBlankScope(scope) + ); + } + /** * Converts the event with all child events to a sequence of the th2 events then organizes them into batches according to event tree structure and value of max event batch content size argent. * Splitting to batch executes by principles: * * Events with children are put into distinct batches because events can't be a child of an event from another batch. * * Events without children are collected into batches according to the max size. For example, little child events can be put into one batch; big child events can be put into separate batches. + * * @param maxEventBatchContentSize - the maximum size of useful content in one batch which is calculated as the sum of the size of all event bodies in the batch - * @param parentID - reference to parent event for the current event tree. It may be null if the current event is root. + * @param parentId - reference to parent event for the current event tree. It may be null if the current event is root, in this case {@code bookName} is required. + * @param bookName - book name for the current event tree. It may not be null. + * @param scope - scope for the current event tree. It may be null. */ - public List toBatchesProtoWithLimit(int maxEventBatchContentSize, @Nullable EventID parentID) throws JsonProcessingException { + private List toBatchesProtoWithLimit( + int maxEventBatchContentSize, + @Nullable EventID parentId, + @NotNull String bookName, + @Nullable String scope + ) throws IOException { if (maxEventBatchContentSize <= 0) { throw new IllegalArgumentException("'maxEventBatchContentSize' should be greater than zero, actual: " + maxEventBatchContentSize); } - List events = toListProto(parentID); + List events = toListProto(new ArrayList<>(), parentId, bookName, scope); List result = new ArrayList<>(); Map> eventGroups = events.stream().collect(Collectors.groupingBy(com.exactpro.th2.common.grpc.Event::getParentId)); - batch(maxEventBatchContentSize, result, eventGroups, parentID); + batch(maxEventBatchContentSize, result, eventGroups, parentId); return result; } + public List toListBatchProto(@NotNull EventID parentId) throws IOException { + return toListBatchProto( + requireNonNullParentId(parentId), + requireNonBlankBookName(parentId.getBookName()), + parentId.getScope() + ); + } + + public List toListBatchProto(@NotNull String bookName) throws IOException { + return toListBatchProto( + null, + requireNonBlankBookName(bookName), + null + ); + } + + public List toListBatchProto( + @NotNull String bookName, + @NotNull String scope + ) throws IOException { + return toListBatchProto( + null, + requireNonBlankBookName(bookName), + requireNonBlankScope(scope) + ); + } + /** * Converts the event with all child events to a sequence of the th2 events then organizes them into batches according to event tree structure. * Splitting to batch executes by principles: * * Events with children are put into distinct batches because events can't be a child of an event from another batch. * * Events without children are collected into batches. - * @param parentID - reference to parent event for the current event tree. It may be null if the current event is root. + * + * @param parentId - reference to parent event for the current event tree. It may be null if the current event is root, in this case {@code bookName} is required. + * @param bookName - book name for the current event tree. It may not be null. + * @param scope - scope for the current event tree. It may be null. */ - public List toListBatchProto(@Nullable EventID parentID) throws JsonProcessingException { - return toBatchesProtoWithLimit(Integer.MAX_VALUE, parentID); + private List toListBatchProto( + @Nullable EventID parentId, + @NotNull String bookName, + @Nullable String scope + ) throws IOException { + return toBatchesProtoWithLimit(Integer.MAX_VALUE, parentId, bookName, scope); } public String getId() { @@ -344,23 +539,7 @@ public Instant getEndTimestamp() { return endTimestamp; } - /** - * @deprecated prefer to use full object instead of part of them, use the {@link #collectSubEvents(List, EventID)} method - */ - @Deprecated - protected List collectSubEvents(List protoEvents, @Nullable String parentID) throws JsonProcessingException { - return collectSubEvents(protoEvents, toEventID(parentID)); - } - - protected List collectSubEvents(List protoEvents, @Nullable EventID parentID) throws JsonProcessingException { - protoEvents.add(toProto(parentID)); // collect current level - for (Event subEvent : subEvents) { - subEvent.collectSubEvents(protoEvents, toEventID(id)); // collect sub level - } - return protoEvents; - } - - protected byte[] buildBody() throws JsonProcessingException { + protected byte[] buildBody() throws IOException { return OBJECT_MAPPER.get().writeValueAsBytes(body); } @@ -368,14 +547,6 @@ protected String formatStateException(String fieldName, Object value) { return fieldName + " in event '" + id + "' already sed with value '" + value + '\''; } - /** - * @deprecated use {@link #getAggregatedStatus} instead - */ - @Deprecated(forRemoval = true) - protected Status getAggrigatedStatus() { - return getAggregatedStatus(); - } - @NotNull protected Status getAggregatedStatus() { if (status == Status.PASSED) { @@ -386,11 +557,11 @@ protected Status getAggregatedStatus() { return Status.FAILED; } - private void batch(int maxEventBatchContentSize, List result, Map> eventGroups, @Nullable EventID eventID) throws JsonProcessingException { + private void batch(int maxEventBatchContentSize, List result, Map> eventGroups, @Nullable EventID eventID) throws IOException { eventID = requireNonNullElse(eventID, DEFAULT_EVENT_ID); EventBatch.Builder builder = setParentId(EventBatch.newBuilder(), eventID); - List events = Objects.requireNonNull(eventGroups.get(eventID), + List events = requireNonNull(eventGroups.get(eventID), eventID == DEFAULT_EVENT_ID ? "Neither of events is root event" : "Neither of events refers to " + shortDebugString(eventID)); @@ -399,13 +570,13 @@ private void batch(int maxEventBatchContentSize, List result, Map 0 + if (builder.getEventsCount() > 0 && getContentSize(builder) + getContentSize(checkedProtoEvent) > maxEventBatchContentSize) { result.add(checkAndBuild(maxEventBatchContentSize, builder)); builder = setParentId(EventBatch.newBuilder(), eventID); @@ -414,21 +585,21 @@ && getContentSize(builder) + getContentSize(checkedProtoEvent) > maxEventBatchCo } } - if(builder.getEventsCount() > 0) { + if (builder.getEventsCount() > 0) { result.add(checkAndBuild(maxEventBatchContentSize, builder)); } } private EventBatch checkAndBuild(int maxEventBatchContentSize, EventBatch.Builder builder) { int contentSize = getContentSize(builder); - if(contentSize > maxEventBatchContentSize) { + if (contentSize > maxEventBatchContentSize) { throw new IllegalStateException("The smallest batch size exceeds the max event batch content size, max " + maxEventBatchContentSize + ", actual " + contentSize); } return builder.build(); } - private com.exactpro.th2.common.grpc.Event checkAndRebuild(int maxEventBatchContentSize, com.exactpro.th2.common.grpc.Event event) throws JsonProcessingException { + private com.exactpro.th2.common.grpc.Event checkAndRebuild(int maxEventBatchContentSize, com.exactpro.th2.common.grpc.Event event) throws IOException { int contentSize = getContentSize(event); if (contentSize > maxEventBatchContentSize) { return com.exactpro.th2.common.grpc.Event.newBuilder(event) diff --git a/src/main/java/com/exactpro/th2/common/event/EventUtils.java b/src/main/java/com/exactpro/th2/common/event/EventUtils.java index 8e58fdbad..be452af47 100644 --- a/src/main/java/com/exactpro/th2/common/event/EventUtils.java +++ b/src/main/java/com/exactpro/th2/common/event/EventUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2020 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2021 Exactpro (Exactpro Systems Limited) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,10 @@ */ package com.exactpro.th2.common.event; +import java.time.Instant; + import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import com.exactpro.th2.common.event.bean.Message; @@ -23,10 +26,15 @@ import com.exactpro.th2.common.grpc.EventID; import com.fasterxml.uuid.Generators; import com.fasterxml.uuid.NoArgGenerator; +import com.google.protobuf.Timestamp; + +import static java.util.Objects.requireNonNull; +import static org.apache.commons.lang3.StringUtils.isBlank; @SuppressWarnings("ClassNamePrefixedWithPackageName") public class EventUtils { public static final NoArgGenerator TIME_BASED_UUID_GENERATOR = Generators.timeBasedGenerator(); + public static final String DEFAULT_SCOPE = "th2-scope"; public static String generateUUID() { return TIME_BASED_UUID_GENERATOR.generate().toString(); @@ -36,13 +44,80 @@ public static Message createMessageBean(String text) { return new MessageBuilder().text(text).build(); } - @Contract("null -> null; !null -> !null") - public static @Nullable EventID toEventID(@Nullable String id) { - if (id == null) { - return null; + @Contract("_, _, null -> !null; _, _, !null -> !null") + public static @NotNull EventID toEventID( + @NotNull Instant startTimestamp, + @NotNull String bookName, + @Nullable String id + ) { + return internalToEventID( + startTimestamp, + requireNonBlankBookName(bookName), + DEFAULT_SCOPE, + id + ); + } + + @Contract("_, _, _, null -> !null; _, _, _, !null -> !null") + public static @NotNull EventID toEventID( + @NotNull Instant startTimestamp, + @NotNull String bookName, + @NotNull String scope, + @Nullable String id + ) { + return internalToEventID( + startTimestamp, + requireNonBlankBookName(bookName), + requireNonBlankScope(scope), + id + ); + } + + private static @NotNull EventID internalToEventID( + @NotNull Instant startTimestamp, + @NotNull String bookName, + @NotNull String scope, + @Nullable String id + ) { + EventID.Builder builder = EventID + .newBuilder() + .setStartTimestamp(toTimestamp(startTimestamp)) + .setBookName(bookName) + .setScope(scope); + if (id != null) { + builder.setId(id); } - return EventID.newBuilder() - .setId(id) + return builder.build(); + } + + public static @NotNull Timestamp toTimestamp(@NotNull Instant timestamp) { + requireNonNullTimestamp(timestamp); + return Timestamp + .newBuilder() + .setSeconds(timestamp.getEpochSecond()) + .setNanos(timestamp.getNano()) .build(); } + + public static Instant requireNonNullTimestamp(Instant timestamp) { + return requireNonNull(timestamp, "Timestamp cannot be null"); + } + + public static EventID requireNonNullParentId(EventID parentId) { + return requireNonNull(parentId, "Parent id cannot be null"); + } + + public static String requireNonBlankBookName(String bookName) { + if (isBlank(bookName)) { + throw new IllegalArgumentException("Book name cannot be null or blank"); + } + return bookName; + } + + public static String requireNonBlankScope(String scope) { + if (isBlank(scope)) { + throw new IllegalArgumentException("Scope cannot be null or blank"); + } + return scope; + } } diff --git a/src/main/java/com/exactpro/th2/common/event/bean/builder/CollectionBuilder.java b/src/main/java/com/exactpro/th2/common/event/bean/builder/CollectionBuilder.java index d13c1a97e..74163ee65 100644 --- a/src/main/java/com/exactpro/th2/common/event/bean/builder/CollectionBuilder.java +++ b/src/main/java/com/exactpro/th2/common/event/bean/builder/CollectionBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2020 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2022 Exactpro (Exactpro Systems Limited) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,8 @@ */ package com.exactpro.th2.common.event.bean.builder; -import com.exactpro.th2.common.event.bean.*; +import com.exactpro.th2.common.event.bean.Collection; +import com.exactpro.th2.common.event.bean.TreeTableEntry; import java.util.HashMap; import java.util.Map; diff --git a/src/main/java/com/exactpro/th2/common/event/bean/builder/MessageBuilder.java b/src/main/java/com/exactpro/th2/common/event/bean/builder/MessageBuilder.java index c18d57aa5..3ad7c5b98 100644 --- a/src/main/java/com/exactpro/th2/common/event/bean/builder/MessageBuilder.java +++ b/src/main/java/com/exactpro/th2/common/event/bean/builder/MessageBuilder.java @@ -20,7 +20,7 @@ import static java.util.Objects.requireNonNull; public class MessageBuilder { - public final static String MESSAGE_TYPE = "message"; + public static final String MESSAGE_TYPE = "message"; private String text; diff --git a/src/main/java/com/exactpro/th2/common/schema/box/configuration/BoxConfiguration.java b/src/main/java/com/exactpro/th2/common/schema/box/configuration/BoxConfiguration.java index bd412e935..d35a2db58 100644 --- a/src/main/java/com/exactpro/th2/common/schema/box/configuration/BoxConfiguration.java +++ b/src/main/java/com/exactpro/th2/common/schema/box/configuration/BoxConfiguration.java @@ -18,12 +18,20 @@ import com.exactpro.th2.common.schema.configuration.Configuration; import com.fasterxml.jackson.annotation.JsonProperty; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import static com.exactpro.th2.common.event.EventUtils.requireNonBlankBookName; + public class BoxConfiguration extends Configuration { + public static final String DEFAULT_BOOK_NAME = "test_book"; + @JsonProperty private String boxName = null; + @JsonProperty + private String bookName = DEFAULT_BOOK_NAME; + @Nullable public String getBoxName() { return boxName; @@ -32,4 +40,13 @@ public String getBoxName() { public void setBoxName(@Nullable String boxName) { this.boxName = boxName; } + + @NotNull + public String getBookName() { + return bookName; + } + + public void setBookName(@NotNull String bookName) { + this.bookName = requireNonBlankBookName(bookName); + } } diff --git a/src/main/java/com/exactpro/th2/common/schema/event/EventBatchRouter.java b/src/main/java/com/exactpro/th2/common/schema/event/EventBatchRouter.java index c4f25c878..9dbc1e937 100644 --- a/src/main/java/com/exactpro/th2/common/schema/event/EventBatchRouter.java +++ b/src/main/java/com/exactpro/th2/common/schema/event/EventBatchRouter.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2023 Exactpro (Exactpro Systems Limited) * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -17,11 +17,11 @@ import java.util.Set; +import com.exactpro.th2.common.schema.message.ConfirmationListener; import org.apache.commons.collections4.SetUtils; import org.jetbrains.annotations.NotNull; import com.exactpro.th2.common.grpc.EventBatch; -import com.exactpro.th2.common.schema.message.FilterFunction; import com.exactpro.th2.common.schema.message.MessageSender; import com.exactpro.th2.common.schema.message.MessageSubscriber; import com.exactpro.th2.common.schema.message.QueueAttribute; @@ -59,23 +59,28 @@ protected Set getRequiredSubscribeAttributes() { @NotNull @Override - protected MessageSender createSender(QueueConfiguration queueConfiguration, @NotNull String pinName) { + protected MessageSender createSender(QueueConfiguration queueConfiguration, @NotNull String pinName, @NotNull String bookName) { return new EventBatchSender( getConnectionManager(), queueConfiguration.getExchange(), queueConfiguration.getRoutingKey(), - pinName + pinName, + bookName ); } @NotNull @Override - protected MessageSubscriber createSubscriber(QueueConfiguration queueConfiguration, @NotNull String pinName) { + protected MessageSubscriber createSubscriber( + QueueConfiguration queueConfiguration, + @NotNull String pinName, + @NotNull ConfirmationListener listener + ) { return new EventBatchSubscriber( getConnectionManager(), queueConfiguration.getQueue(), - FilterFunction.DEFAULT_FILTER_FUNCTION, - pinName + pinName, + listener ); } diff --git a/src/main/java/com/exactpro/th2/common/schema/event/EventBatchSender.java b/src/main/java/com/exactpro/th2/common/schema/event/EventBatchSender.java index 661d4ab70..810e5dc8a 100644 --- a/src/main/java/com/exactpro/th2/common/schema/event/EventBatchSender.java +++ b/src/main/java/com/exactpro/th2/common/schema/event/EventBatchSender.java @@ -20,6 +20,7 @@ import org.jetbrains.annotations.NotNull; +import com.exactpro.th2.common.grpc.Event; import com.exactpro.th2.common.grpc.EventBatch; import com.exactpro.th2.common.message.MessageUtils; import com.exactpro.th2.common.schema.message.impl.rabbitmq.AbstractRabbitSender; @@ -27,6 +28,7 @@ import static com.exactpro.th2.common.metrics.CommonMetrics.TH2_PIN_LABEL; import static com.exactpro.th2.common.schema.event.EventBatchRouter.EVENT_TYPE; + import io.prometheus.client.Counter; public class EventBatchSender extends AbstractRabbitSender { @@ -40,9 +42,10 @@ public EventBatchSender( @NotNull ConnectionManager connectionManager, @NotNull String exchangeName, @NotNull String routingKey, - @NotNull String th2Pin + @NotNull String th2Pin, + @NotNull String bookName ) { - super(connectionManager, exchangeName, routingKey, th2Pin, EVENT_TYPE); + super(connectionManager, exchangeName, routingKey, th2Pin, EVENT_TYPE, bookName); } @Override @@ -50,7 +53,19 @@ public void send(EventBatch value) throws IOException { EVENT_PUBLISH_TOTAL .labels(th2Pin) .inc(value.getEventsCount()); - super.send(value); + if (value.getEventsList().stream().anyMatch(event -> event.getId().getBookName().isEmpty())) { + EventBatch.Builder eventBatchBuilder = EventBatch.newBuilder(); + value.getEventsList().forEach(event -> { + Event.Builder eventBuilder = event.toBuilder(); + if (event.getId().getBookName().isEmpty()) { + eventBuilder.getIdBuilder().setBookName(bookName); + } + eventBatchBuilder.addEvents(eventBuilder); + }); + super.send(eventBatchBuilder.build()); + } else { + super.send(value); + } } @Override diff --git a/src/main/java/com/exactpro/th2/common/schema/event/EventBatchSubscriber.java b/src/main/java/com/exactpro/th2/common/schema/event/EventBatchSubscriber.java index e2336da8b..78642b061 100644 --- a/src/main/java/com/exactpro/th2/common/schema/event/EventBatchSubscriber.java +++ b/src/main/java/com/exactpro/th2/common/schema/event/EventBatchSubscriber.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2023 Exactpro (Exactpro Systems Limited) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,20 +17,21 @@ package com.exactpro.th2.common.schema.event; import com.exactpro.th2.common.grpc.EventBatch; -import com.exactpro.th2.common.schema.message.FilterFunction; +import com.exactpro.th2.common.schema.message.ConfirmationListener; +import com.exactpro.th2.common.schema.message.DeliveryMetadata; +import com.exactpro.th2.common.schema.message.ManualAckDeliveryCallback.Confirmation; import com.exactpro.th2.common.schema.message.impl.rabbitmq.AbstractRabbitSubscriber; import com.exactpro.th2.common.schema.message.impl.rabbitmq.connection.ConnectionManager; -import com.exactpro.th2.common.schema.message.ManualAckDeliveryCallback.Confirmation; import com.rabbitmq.client.Delivery; - -import static com.exactpro.th2.common.metrics.CommonMetrics.TH2_PIN_LABEL; -import static com.exactpro.th2.common.schema.event.EventBatchRouter.EVENT_TYPE; import io.prometheus.client.Counter; - import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.io.IOException; + import static com.exactpro.th2.common.message.MessageUtils.toJson; +import static com.exactpro.th2.common.metrics.CommonMetrics.TH2_PIN_LABEL; +import static com.exactpro.th2.common.schema.event.EventBatchRouter.EVENT_TYPE; public class EventBatchSubscriber extends AbstractRabbitSubscriber { private static final Counter EVENT_SUBSCRIBE_TOTAL = Counter.build() @@ -42,10 +43,10 @@ public class EventBatchSubscriber extends AbstractRabbitSubscriber { public EventBatchSubscriber( @NotNull ConnectionManager connectionManager, @NotNull String queue, - @NotNull FilterFunction filterFunc, - @NotNull String th2Pin + @NotNull String th2Pin, + @NotNull ConfirmationListener listener ) { - super(connectionManager, queue, filterFunc, th2Pin, EVENT_TYPE); + super(connectionManager, queue, th2Pin, EVENT_TYPE, listener); } @Override @@ -70,11 +71,11 @@ protected EventBatch filter(EventBatch eventBatch) throws Exception { } @Override - protected void handle(String consumeTag, Delivery delivery, EventBatch value, - Confirmation confirmation) { + protected void handle(DeliveryMetadata deliveryMetadata, Delivery delivery, EventBatch value, + Confirmation confirmation) throws IOException { EVENT_SUBSCRIBE_TOTAL .labels(th2Pin) .inc(value.getEventsCount()); - super.handle(consumeTag, delivery, value, confirmation); + super.handle(deliveryMetadata, delivery, value, confirmation); } } diff --git a/src/main/java/com/exactpro/th2/common/schema/factory/AbstractCommonFactory.java b/src/main/java/com/exactpro/th2/common/schema/factory/AbstractCommonFactory.java index 24b9f8896..227ffc859 100644 --- a/src/main/java/com/exactpro/th2/common/schema/factory/AbstractCommonFactory.java +++ b/src/main/java/com/exactpro/th2/common/schema/factory/AbstractCommonFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2023 Exactpro (Exactpro Systems Limited) * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -17,13 +17,15 @@ import com.exactpro.cradle.CradleManager; import com.exactpro.cradle.cassandra.CassandraCradleManager; -import com.exactpro.cradle.cassandra.connection.CassandraConnection; +import com.exactpro.cradle.cassandra.CassandraStorageSettings; import com.exactpro.cradle.cassandra.connection.CassandraConnectionSettings; import com.exactpro.cradle.utils.CradleStorageException; import com.exactpro.th2.common.event.Event; import com.exactpro.th2.common.grpc.EventBatch; +import com.exactpro.th2.common.grpc.EventID; import com.exactpro.th2.common.grpc.MessageBatch; import com.exactpro.th2.common.grpc.MessageGroupBatch; +import com.exactpro.th2.common.grpc.MessageID; import com.exactpro.th2.common.grpc.RawMessageBatch; import com.exactpro.th2.common.metrics.CommonMetrics; import com.exactpro.th2.common.metrics.MetricMonitor; @@ -31,18 +33,16 @@ import com.exactpro.th2.common.schema.box.configuration.BoxConfiguration; import com.exactpro.th2.common.schema.configuration.ConfigurationManager; import com.exactpro.th2.common.schema.cradle.CradleConfidentialConfiguration; -import com.exactpro.th2.common.schema.cradle.CradleConfiguration; import com.exactpro.th2.common.schema.cradle.CradleNonConfidentialConfiguration; import com.exactpro.th2.common.schema.dictionary.DictionaryType; -import com.exactpro.th2.common.schema.event.EventBatchRouter; import com.exactpro.th2.common.schema.exception.CommonFactoryException; import com.exactpro.th2.common.schema.grpc.configuration.GrpcConfiguration; import com.exactpro.th2.common.schema.grpc.configuration.GrpcRouterConfiguration; import com.exactpro.th2.common.schema.grpc.router.GrpcRouter; -import com.exactpro.th2.common.schema.grpc.router.impl.DefaultGrpcRouter; import com.exactpro.th2.common.schema.message.MessageRouter; import com.exactpro.th2.common.schema.message.MessageRouterContext; import com.exactpro.th2.common.schema.message.MessageRouterMonitor; +import com.exactpro.th2.common.schema.message.NotificationRouter; import com.exactpro.th2.common.schema.message.QueueAttribute; import com.exactpro.th2.common.schema.message.configuration.MessageRouterConfiguration; import com.exactpro.th2.common.schema.message.impl.context.DefaultMessageRouterContext; @@ -54,21 +54,19 @@ import com.exactpro.th2.common.schema.message.impl.rabbitmq.connection.ConnectionManager; import com.exactpro.th2.common.schema.message.impl.rabbitmq.custom.MessageConverter; import com.exactpro.th2.common.schema.message.impl.rabbitmq.custom.RabbitCustomRouter; -import com.exactpro.th2.common.schema.message.impl.rabbitmq.group.RabbitMessageGroupBatchRouter; -import com.exactpro.th2.common.schema.message.impl.rabbitmq.parsed.RabbitParsedBatchRouter; -import com.exactpro.th2.common.schema.message.impl.rabbitmq.raw.RabbitRawBatchRouter; +import com.exactpro.th2.common.schema.message.impl.rabbitmq.transport.GroupBatch; +import com.exactpro.th2.common.schema.message.impl.rabbitmq.transport.TransportGroupBatchRouter; import com.exactpro.th2.common.schema.strategy.route.json.RoutingStrategyModule; import com.exactpro.th2.common.schema.util.Log4jConfigUtils; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.module.kotlin.KotlinFeature; import com.fasterxml.jackson.module.kotlin.KotlinModule; import io.prometheus.client.exporter.HTTPServer; import io.prometheus.client.hotspot.DefaultExports; import org.apache.commons.lang3.StringUtils; import org.apache.commons.text.StringSubstitutor; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -76,54 +74,52 @@ import java.io.IOException; import java.io.InputStream; import java.lang.reflect.InvocationTargetException; -import java.net.URL; import java.nio.file.Path; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; -import java.util.Spliterators; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicReference; -import java.util.jar.Attributes.Name; -import java.util.jar.JarFile; -import java.util.jar.Manifest; -import java.util.stream.StreamSupport; - -import static com.exactpro.cradle.cassandra.CassandraStorageSettings.DEFAULT_MAX_EVENT_BATCH_SIZE; -import static com.exactpro.cradle.cassandra.CassandraStorageSettings.DEFAULT_MAX_MESSAGE_BATCH_SIZE; -import static java.util.Collections.emptyMap; + +import static com.exactpro.cradle.CradleStorage.DEFAULT_MAX_MESSAGE_BATCH_SIZE; +import static com.exactpro.cradle.CradleStorage.DEFAULT_MAX_TEST_EVENT_BATCH_SIZE; +import static com.exactpro.cradle.cassandra.CassandraStorageSettings.DEFAULT_COUNTER_PERSISTENCE_INTERVAL_MS; +import static com.exactpro.cradle.cassandra.CassandraStorageSettings.DEFAULT_MAX_UNCOMPRESSED_TEST_EVENT_SIZE; +import static com.exactpro.cradle.cassandra.CassandraStorageSettings.DEFAULT_RESULT_PAGE_SIZE; import static java.util.Objects.requireNonNull; import static org.apache.commons.lang3.StringUtils.defaultIfBlank; /** - * * Class for load JSON schema configuration and create {@link GrpcRouter} and {@link MessageRouter} * * @see CommonFactory */ public abstract class AbstractCommonFactory implements AutoCloseable { - protected static final String DEFAULT_CRADLE_INSTANCE_NAME = "infra"; - protected static final String EXACTPRO_IMPLEMENTATION_VENDOR = "Exactpro Systems LLC"; - - /** @deprecated please use {@link #LOG4J_PROPERTIES_DEFAULT_PATH} */ + /** + * @deprecated please use {@link #LOG4J_PROPERTIES_DEFAULT_PATH} + */ @Deprecated - protected static final String LOG4J_PROPERTIES_DEFAULT_PATH_OLD = "/home/etc"; - protected static final String LOG4J_PROPERTIES_DEFAULT_PATH = "/var/th2/config"; + protected static final Path LOG4J_PROPERTIES_DEFAULT_PATH_OLD = Path.of("/home/etc"); + protected static final Path LOG4J_PROPERTIES_DEFAULT_PATH = Path.of("/var/th2/config"); protected static final String LOG4J2_PROPERTIES_NAME = "log4j2.properties"; - protected static final String LOG4J_PROPERTIES_NAME = "log4j.properties"; - protected static final ObjectMapper MAPPER = new ObjectMapper(); + public static final ObjectMapper MAPPER = new ObjectMapper(); - static { + static { MAPPER.registerModules( - new KotlinModule(), + new KotlinModule.Builder() + .withReflectionCacheSize(512) + .configure(KotlinFeature.NullToEmptyCollection, false) + .configure(KotlinFeature.NullToEmptyMap, false) + .configure(KotlinFeature.NullIsSameAsDefault, false) + .configure(KotlinFeature.SingletonSupport, false) + .configure(KotlinFeature.StrictNullChecks, false) + .build(), new RoutingStrategyModule(MAPPER), new JavaTimeModule() ); @@ -137,13 +133,16 @@ public abstract class AbstractCommonFactory implements AutoCloseable { private final Class> messageRouterMessageGroupBatchClass; private final Class> eventBatchRouterClass; private final Class grpcRouterClass; + private final Class> notificationEventBatchRouterClass; private final AtomicReference rabbitMqConnectionManager = new AtomicReference<>(); private final AtomicReference routerContext = new AtomicReference<>(); private final AtomicReference> messageRouterParsedBatch = new AtomicReference<>(); private final AtomicReference> messageRouterRawBatch = new AtomicReference<>(); private final AtomicReference> messageRouterMessageGroupBatch = new AtomicReference<>(); + private final AtomicReference> transportGroupBatchRouter = new AtomicReference<>(); private final AtomicReference> eventBatchRouter = new AtomicReference<>(); - private final AtomicReference rootEventId = new AtomicReference<>(); + private final AtomicReference> notificationEventBatchRouter = new AtomicReference<>(); + private final AtomicReference rootEventId = new AtomicReference<>(); private final AtomicReference grpcRouter = new AtomicReference<>(); private final AtomicReference prometheusExporter = new AtomicReference<>(); private final AtomicReference cradleManager = new AtomicReference<>(); @@ -154,50 +153,19 @@ public abstract class AbstractCommonFactory implements AutoCloseable { configureLogger(); } - /** - * Create factory with default implementation schema classes - */ - public AbstractCommonFactory() { - this(RabbitParsedBatchRouter.class, RabbitRawBatchRouter.class, RabbitMessageGroupBatchRouter.class, EventBatchRouter.class, DefaultGrpcRouter.class); - } - - /** - * Create factory with non-default implementations schema classes - * @param messageRouterParsedBatchClass Class for {@link MessageRouter} which work with {@link MessageBatch} - * @param messageRouterRawBatchClass Class for {@link MessageRouter} which work with {@link RawMessageBatch} - * @param eventBatchRouterClass Class for {@link MessageRouter} which work with {@link EventBatch} - * @param grpcRouterClass Class for {@link GrpcRouter} - */ - public AbstractCommonFactory(@NotNull Class> messageRouterParsedBatchClass, - @NotNull Class> messageRouterRawBatchClass, - @NotNull Class> messageRouterMessageGroupBatchClass, - @NotNull Class> eventBatchRouterClass, - @NotNull Class grpcRouterClass) { - this(messageRouterParsedBatchClass, messageRouterRawBatchClass, messageRouterMessageGroupBatchClass, - eventBatchRouterClass, grpcRouterClass, emptyMap()); - } - /** * Create factory with non-default implementations schema classes * - * @param messageRouterParsedBatchClass Class for {@link MessageRouter} which work with {@link MessageBatch} - * @param messageRouterRawBatchClass Class for {@link MessageRouter} which work with {@link RawMessageBatch} - * @param eventBatchRouterClass Class for {@link MessageRouter} which work with {@link EventBatch} - * @param grpcRouterClass Class for {@link GrpcRouter} - * @param environmentVariables map with additional environment variables + * @param settings {@link FactorySettings} */ - protected AbstractCommonFactory(@NotNull Class> messageRouterParsedBatchClass, - @NotNull Class> messageRouterRawBatchClass, - @NotNull Class> messageRouterMessageGroupBatchClass, - @NotNull Class> eventBatchRouterClass, - @NotNull Class grpcRouterClass, - @NotNull Map environmentVariables) { - this.messageRouterParsedBatchClass = messageRouterParsedBatchClass; - this.messageRouterRawBatchClass = messageRouterRawBatchClass; - this.messageRouterMessageGroupBatchClass = messageRouterMessageGroupBatchClass; - this.eventBatchRouterClass = eventBatchRouterClass; - this.grpcRouterClass = grpcRouterClass; - this.stringSubstitutor = new StringSubstitutor(key -> defaultIfBlank(environmentVariables.get(key), System.getenv(key))); + public AbstractCommonFactory(FactorySettings settings) { + messageRouterParsedBatchClass = settings.getMessageRouterParsedBatchClass(); + messageRouterRawBatchClass = settings.getMessageRouterRawBatchClass(); + messageRouterMessageGroupBatchClass = settings.getMessageRouterMessageGroupBatchClass(); + eventBatchRouterClass = settings.getEventBatchRouterClass(); + grpcRouterClass = settings.getGrpcRouterClass(); + notificationEventBatchRouterClass = settings.getNotificationEventBatchRouterClass(); + stringSubstitutor = new StringSubstitutor(key -> defaultIfBlank(settings.getVariables().get(key), System.getenv(key))); } public void start() { @@ -229,7 +197,8 @@ public MessageRouter getMessageRouterParsedBatch() { try { router = messageRouterParsedBatchClass.getConstructor().newInstance(); router.init(getMessageRouterContext(), getMessageRouterMessageGroupBatch()); - } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | + NoSuchMethodException e) { throw new CommonFactoryException("Can not create parsed message router", e); } } @@ -249,7 +218,8 @@ public MessageRouter getMessageRouterRawBatch() { try { router = messageRouterRawBatchClass.getConstructor().newInstance(); router.init(getMessageRouterContext(), getMessageRouterMessageGroupBatch()); - } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | + NoSuchMethodException e) { throw new CommonFactoryException("Can not create raw message router", e); } } @@ -258,6 +228,21 @@ public MessageRouter getMessageRouterRawBatch() { }); } + /** + * @return Initialized {@link MessageRouter} which works with {@link GroupBatch} + * @throws IllegalStateException if can not read configuration + */ + public MessageRouter getTransportGroupBatchRouter() { + return transportGroupBatchRouter.updateAndGet(router -> { + if (router == null) { + router = new TransportGroupBatchRouter(); + router.init(getMessageRouterContext()); + } + + return router; + }); + } + /** * @return Initialized {@link MessageRouter} which works with {@link MessageGroupBatch} * @throws CommonFactoryException if can not call default constructor from class @@ -269,7 +254,8 @@ public MessageRouter getMessageRouterMessageGroupBatch() { try { router = messageRouterMessageGroupBatchClass.getConstructor().newInstance(); router.init(getMessageRouterContext()); - } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | + NoSuchMethodException e) { throw new CommonFactoryException("Can not create group message router", e); } } @@ -288,8 +274,14 @@ public MessageRouter getEventBatchRouter() { if (router == null) { try { router = eventBatchRouterClass.getConstructor().newInstance(); - router.init(new DefaultMessageRouterContext(getRabbitMqConnectionManager(), MessageRouterMonitor.DEFAULT_MONITOR, getMessageRouterConfiguration())); - } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + router.init(new DefaultMessageRouterContext( + getRabbitMqConnectionManager(), + MessageRouterMonitor.DEFAULT_MONITOR, + getMessageRouterConfiguration(), + getBoxConfiguration() + )); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | + NoSuchMethodException e) { throw new CommonFactoryException("Can not create event batch router", e); } } @@ -308,7 +300,8 @@ public GrpcRouter getGrpcRouter() { try { router = grpcRouterClass.getConstructor().newInstance(); router.init(getGrpcConfiguration(), getGrpcRouterConfiguration()); - } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | + NoSuchMethodException e) { throw new CommonFactoryException("Can not create GRPC router", e); } } @@ -317,9 +310,28 @@ public GrpcRouter getGrpcRouter() { }); } + /** + * @return Initialized {@link NotificationRouter} which works with {@link EventBatch} + * @throws CommonFactoryException if cannot call default constructor from class + * @throws IllegalStateException if cannot read configuration + */ + public NotificationRouter getNotificationEventBatchRouter() { + return notificationEventBatchRouter.updateAndGet(router -> { + if (router == null) { + try { + router = notificationEventBatchRouterClass.getConstructor().newInstance(); + router.init(getMessageRouterContext()); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | + NoSuchMethodException e) { + throw new CommonFactoryException("Can not create notification router", e); + } + } + return router; + }); + } + /** * Registers custom message router. - * * Unlike the {@link #registerCustomMessageRouter(Class, MessageConverter, Set, Set, String...)} the registered router won't have any additional pins attributes * except {@link QueueAttribute#SUBSCRIBE} for subscribe methods and {@link QueueAttribute#PUBLISH} for send methods * @@ -335,11 +347,11 @@ public void registerCustomMessageRouter( /** * Registers message router for custom type that is passed via {@code messageClass} parameter.
* - * @param messageClass custom message class - * @param messageConverter converter that will be used to convert message to bytes and vice versa - * @param defaultSendAttributes set of attributes for sending. A pin must have all of them to be selected for sending the message + * @param messageClass custom message class + * @param messageConverter converter that will be used to convert message to bytes and vice versa + * @param defaultSendAttributes set of attributes for sending. A pin must have all of them to be selected for sending the message * @param defaultSubscribeAttributes set of attributes for subscription. A pin must have all of them to be selected for receiving messages - * @param custom message type + * @param custom message type * @throws IllegalStateException if the router for {@code messageClass} is already registered */ public void registerCustomMessageRouter( @@ -366,12 +378,12 @@ public void registerCustomMessageRouter( /** * Returns previously registered message router for message of {@code messageClass} type. - * * If the router for that type is not registered yet ,it throws {@link IllegalArgumentException} + * * @param messageClass custom message class - * @param custom message type - * @throws IllegalArgumentException if router for specified type is not registered + * @param custom message type * @return the previously registered router for specified type + * @throws IllegalArgumentException if router for specified type is not registered */ @SuppressWarnings("unchecked") @NotNull @@ -381,7 +393,7 @@ public MessageRouter getCustomMessageRouter(Class messageClass) { throw new IllegalArgumentException( "Router for class " + messageClass.getCanonicalName() + "is not registered. Call 'registerCustomMessageRouter' first"); } - return (MessageRouter)router; + return (MessageRouter) router; } /** @@ -394,8 +406,9 @@ public T getConfiguration(Path configPath, Class configClass, ObjectMappe /** * Load configuration, save and return. If already loaded return saved configuration. + * * @param configClass configuration class - * @param optional creates an instance of a configuration class via the default constructor if this option is true and the config file doesn't exist or empty + * @param optional creates an instance of a configuration class via the default constructor if this option is true and the config file doesn't exist or empty * @return configuration object */ protected T getConfigurationOrLoad(Class configClass, boolean optional) { @@ -426,22 +439,16 @@ public BoxConfiguration getBoxConfiguration() { return getConfigurationOrLoad(BoxConfiguration.class, true); } - protected CradleConfidentialConfiguration getCradleConfidentialConfiguration() { + private CradleConfidentialConfiguration getCradleConfidentialConfiguration() { return getConfigurationOrLoad(CradleConfidentialConfiguration.class, false); } - protected CradleNonConfidentialConfiguration getCradleNonConfidentialConfiguration() { + private CradleNonConfidentialConfiguration getCradleNonConfidentialConfiguration() { return getConfigurationOrLoad(CradleNonConfidentialConfiguration.class, true); } - /** - * @return Schema cradle configuration - * @throws IllegalStateException if cannot read configuration - * @deprecated please use {@link #getCradleManager()} - */ - @Deprecated - public CradleConfiguration getCradleConfiguration() { - return new CradleConfiguration(getCradleConfidentialConfiguration(), getCradleNonConfidentialConfiguration()); + private CassandraStorageSettings getCassandraStorageSettings() { + return getConfigurationOrLoad(CassandraStorageSettings.class, true); } /** @@ -453,42 +460,46 @@ public CradleManager getCradleManager() { if (manager == null) { try { CradleConfidentialConfiguration confidentialConfiguration = getCradleConfidentialConfiguration(); - CradleNonConfidentialConfiguration nonConfidentialConfiguration = getCradleNonConfidentialConfiguration(); - CassandraConnectionSettings cassandraConnectionSettings = new CassandraConnectionSettings( - confidentialConfiguration.getDataCenter(), confidentialConfiguration.getHost(), confidentialConfiguration.getPort(), - confidentialConfiguration.getKeyspace()); - + confidentialConfiguration.getDataCenter() + ); if (StringUtils.isNotEmpty(confidentialConfiguration.getUsername())) { cassandraConnectionSettings.setUsername(confidentialConfiguration.getUsername()); } - if (StringUtils.isNotEmpty(confidentialConfiguration.getPassword())) { cassandraConnectionSettings.setPassword(confidentialConfiguration.getPassword()); } - if (nonConfidentialConfiguration.getTimeout() > 0) { - cassandraConnectionSettings.setTimeout(nonConfidentialConfiguration.getTimeout()); - } + // Deserialize on config by two different beans for backward compatibility + CradleNonConfidentialConfiguration nonConfidentialConfiguration = getCradleNonConfidentialConfiguration(); + // FIXME: this approach should be replaced to module structure in future + CassandraStorageSettings cassandraStorageSettings = getCassandraStorageSettings(); + cassandraStorageSettings.setKeyspace(confidentialConfiguration.getKeyspace()); - if (nonConfidentialConfiguration.getPageSize() > 0) { - cassandraConnectionSettings.setResultPageSize(nonConfidentialConfiguration.getPageSize()); + if (cassandraStorageSettings.getResultPageSize() == DEFAULT_RESULT_PAGE_SIZE && nonConfidentialConfiguration.getPageSize() > 0) { + cassandraStorageSettings.setResultPageSize(nonConfidentialConfiguration.getPageSize()); + } + if (cassandraStorageSettings.getMaxMessageBatchSize() == DEFAULT_MAX_MESSAGE_BATCH_SIZE && nonConfidentialConfiguration.getCradleMaxMessageBatchSize() > 0) { + cassandraStorageSettings.setMaxMessageBatchSize((int) nonConfidentialConfiguration.getCradleMaxMessageBatchSize()); + } + if (cassandraStorageSettings.getMaxTestEventBatchSize() == DEFAULT_MAX_TEST_EVENT_BATCH_SIZE && nonConfidentialConfiguration.getCradleMaxEventBatchSize() > 0) { + cassandraStorageSettings.setMaxTestEventBatchSize((int) nonConfidentialConfiguration.getCradleMaxEventBatchSize()); + } + if (cassandraStorageSettings.getCounterPersistenceInterval() == DEFAULT_COUNTER_PERSISTENCE_INTERVAL_MS && nonConfidentialConfiguration.getStatisticsPersistenceIntervalMillis() >= 0) { + cassandraStorageSettings.setCounterPersistenceInterval((int) nonConfidentialConfiguration.getStatisticsPersistenceIntervalMillis()); + } + if (cassandraStorageSettings.getMaxUncompressedTestEventSize() == DEFAULT_MAX_UNCOMPRESSED_TEST_EVENT_SIZE && nonConfidentialConfiguration.getMaxUncompressedEventBatchSize() > 0) { + cassandraStorageSettings.setMaxUncompressedTestEventSize((int) nonConfidentialConfiguration.getMaxUncompressedEventBatchSize()); } - manager = new CassandraCradleManager(new CassandraConnection(cassandraConnectionSettings)); - manager.init( - defaultIfBlank(confidentialConfiguration.getCradleInstanceName(), DEFAULT_CRADLE_INSTANCE_NAME), - nonConfidentialConfiguration.getPrepareStorage(), - nonConfidentialConfiguration.getCradleMaxMessageBatchSize() > 0 - ? nonConfidentialConfiguration.getCradleMaxMessageBatchSize() - : DEFAULT_MAX_MESSAGE_BATCH_SIZE, - nonConfidentialConfiguration.getCradleMaxEventBatchSize() > 0 - ? nonConfidentialConfiguration.getCradleMaxEventBatchSize() - : DEFAULT_MAX_EVENT_BATCH_SIZE + manager = new CassandraCradleManager( + cassandraConnectionSettings, + cassandraStorageSettings, + nonConfidentialConfiguration.getPrepareStorage() ); - } catch (CradleStorageException | RuntimeException e) { + } catch (CradleStorageException | RuntimeException | IOException e) { throw new CommonFactoryException("Cannot create Cradle manager", e); } } @@ -511,7 +522,8 @@ public T getCustomConfiguration(Class confClass, ObjectMapper customObjec if (!configFile.exists()) { try { return confClass.getConstructor().newInstance(); - } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | + NoSuchMethodException e) { return null; } } @@ -533,6 +545,7 @@ public T getCustomConfiguration(Class confClass) { /** * Read first and only one dictionary + * * @return Dictionary as {@link InputStream} * @throws IllegalStateException if can not read dictionary or found more than one target */ @@ -553,6 +566,7 @@ public T getCustomConfiguration(Class confClass) { /** * Read dictionary of {@link DictionaryType#MAIN} type + * * @return Dictionary as {@link InputStream} * @throws IllegalStateException if can not read dictionary */ @@ -560,42 +574,41 @@ public T getCustomConfiguration(Class confClass) { public abstract InputStream readDictionary(); /** - * @deprecated Dictionary types will be removed in future releases of infra, use alias instead * @param dictionaryType desired type of dictionary * @return Dictionary as {@link InputStream} * @throws IllegalStateException if can not read dictionary + * @deprecated Dictionary types will be removed in future releases of infra, use alias instead */ @Deprecated(since = "3.33.0", forRemoval = true) public abstract InputStream readDictionary(DictionaryType dictionaryType); /** - * If root event does not exist, it creates root event with its name = box name and timestamp + * If root event does not exist, it creates root event with its book name = box book name and name = box name and timestamp + * * @return root event id */ - @Nullable - public String getRootEventId() { + @NotNull + public EventID getRootEventId() { return rootEventId.updateAndGet(id -> { if (id == null) { try { - String boxName = getBoxConfiguration().getBoxName(); - if (boxName == null) { - return null; - } - - com.exactpro.th2.common.grpc.Event rootEvent = Event.start().endTimestamp() - .name(boxName + " " + Instant.now()) + BoxConfiguration boxConfiguration = getBoxConfiguration(); + com.exactpro.th2.common.grpc.Event rootEvent = Event + .start() + .endTimestamp() + .name(boxConfiguration.getBoxName() + " " + Instant.now()) .description("Root event") .status(Event.Status.PASSED) .type("Microservice") - .toProtoEvent(null); + .toProto(boxConfiguration.getBookName(), boxConfiguration.getBoxName()); try { getEventBatchRouter().sendAll(EventBatch.newBuilder().addEvents(rootEvent).build()); - return rootEvent.getId().getId(); + return rootEvent.getId(); } catch (IOException e) { throw new CommonFactoryException("Can not send root event", e); } - } catch (JsonProcessingException e) { + } catch (IOException e) { throw new CommonFactoryException("Can not create root event", e); } } @@ -629,23 +642,27 @@ public String getRootEventId() { */ protected MessageRouterContext getMessageRouterContext() { return routerContext.updateAndGet(ctx -> { - if (ctx == null) { - try { - - MessageRouterMonitor contextMonitor; - String rootEventId = getRootEventId(); - if (rootEventId == null) { - contextMonitor = new LogMessageRouterMonitor(); - } else { - contextMonitor = new BroadcastMessageRouterMonitor(new LogMessageRouterMonitor(), new EventMessageRouterMonitor(getEventBatchRouter(), rootEventId)); - } - - return new DefaultMessageRouterContext(getRabbitMqConnectionManager(), contextMonitor, getMessageRouterConfiguration()); - } catch (Exception e) { - throw new CommonFactoryException("Can not create message router context", e); - } - } - return ctx; + if (ctx == null) { + try { + MessageRouterMonitor contextMonitor = new BroadcastMessageRouterMonitor( + new LogMessageRouterMonitor(), + new EventMessageRouterMonitor( + getEventBatchRouter(), + getRootEventId() + ) + ); + + return new DefaultMessageRouterContext( + getRabbitMqConnectionManager(), + contextMonitor, + getMessageRouterConfiguration(), + getBoxConfiguration() + ); + } catch (Exception e) { + throw new CommonFactoryException("Can not create message router context", e); + } + } + return ctx; }); } @@ -666,6 +683,16 @@ protected ConnectionManager getRabbitMqConnectionManager() { }); } + public MessageID.Builder newMessageIDBuilder() { + return MessageID.newBuilder() + .setBookName(getBoxConfiguration().getBookName()); + } + + public EventID.Builder newEventIDBuilder() { + return EventID.newBuilder() + .setBookName(getBoxConfiguration().getBookName()); + } + @Override public void close() { LOGGER.info("Closing common factory"); @@ -740,9 +767,9 @@ public void close() { cradleManager.getAndUpdate(manager -> { if (manager != null) { try { - manager.dispose(); + manager.close(); } catch (Exception e) { - LOGGER.error("Failed to dispose Cradle manager", e); + LOGGER.error("Failed to close Cradle manager", e); } } @@ -752,7 +779,7 @@ public void close() { prometheusExporter.updateAndGet(server -> { if (server != null) { try { - server.stop(); + server.close(); } catch (Exception e) { LOGGER.error("Failed to close Prometheus exporter", e); } @@ -763,35 +790,13 @@ public void close() { LOGGER.info("Common factory has been closed"); } - protected static void configureLogger(String... paths) { - List listPath = new ArrayList<>(); + protected static void configureLogger(Path... paths) { + List listPath = new ArrayList<>(); listPath.add(LOG4J_PROPERTIES_DEFAULT_PATH); listPath.add(LOG4J_PROPERTIES_DEFAULT_PATH_OLD); listPath.addAll(Arrays.asList(requireNonNull(paths, "Paths can't be null"))); Log4jConfigUtils log4jConfigUtils = new Log4jConfigUtils(); - log4jConfigUtils.configure(listPath, LOG4J_PROPERTIES_NAME, LOG4J2_PROPERTIES_NAME); - loggingManifests(); - } - - private static void loggingManifests() { - try { - Iterator urlIterator = Thread.currentThread().getContextClassLoader().getResources(JarFile.MANIFEST_NAME).asIterator(); - StreamSupport.stream(Spliterators.spliteratorUnknownSize(urlIterator, 0), false) - .map(url -> { - try (InputStream inputStream = url.openStream()) { - return new Manifest(inputStream); - } catch (IOException e) { - LOGGER.warn("Manifest '{}' loading failere", url, e); - return null; - } - }) - .filter(Objects::nonNull) - .map(Manifest::getMainAttributes) - .filter(attributes -> EXACTPRO_IMPLEMENTATION_VENDOR.equals(attributes.getValue(Name.IMPLEMENTATION_VENDOR))) - .forEach(attributes -> LOGGER.info("Manifest title {}, version {}" - , attributes.getValue(Name.IMPLEMENTATION_TITLE), attributes.getValue(Name.IMPLEMENTATION_VERSION))); - } catch (IOException e) { - LOGGER.warn("Manifest searching failure", e); - } + log4jConfigUtils.configure(listPath, LOG4J2_PROPERTIES_NAME); + ExactproMetaInf.logging(); } } diff --git a/src/main/java/com/exactpro/th2/common/schema/factory/CommonFactory.java b/src/main/java/com/exactpro/th2/common/schema/factory/CommonFactory.java index 399eb21f8..8fe4353c3 100644 --- a/src/main/java/com/exactpro/th2/common/schema/factory/CommonFactory.java +++ b/src/main/java/com/exactpro/th2/common/schema/factory/CommonFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2023 Exactpro (Exactpro Systems Limited) * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -15,28 +15,20 @@ package com.exactpro.th2.common.schema.factory; -import com.exactpro.th2.common.grpc.EventBatch; -import com.exactpro.th2.common.grpc.MessageBatch; -import com.exactpro.th2.common.grpc.MessageGroupBatch; -import com.exactpro.th2.common.grpc.RawMessageBatch; +import com.exactpro.cradle.cassandra.CassandraStorageSettings; import com.exactpro.th2.common.metrics.PrometheusConfiguration; import com.exactpro.th2.common.schema.box.configuration.BoxConfiguration; import com.exactpro.th2.common.schema.configuration.ConfigurationManager; import com.exactpro.th2.common.schema.cradle.CradleConfidentialConfiguration; import com.exactpro.th2.common.schema.cradle.CradleNonConfidentialConfiguration; import com.exactpro.th2.common.schema.dictionary.DictionaryType; -import com.exactpro.th2.common.schema.event.EventBatchRouter; import com.exactpro.th2.common.schema.grpc.configuration.GrpcConfiguration; import com.exactpro.th2.common.schema.grpc.configuration.GrpcRouterConfiguration; import com.exactpro.th2.common.schema.grpc.router.GrpcRouter; -import com.exactpro.th2.common.schema.grpc.router.impl.DefaultGrpcRouter; import com.exactpro.th2.common.schema.message.MessageRouter; import com.exactpro.th2.common.schema.message.configuration.MessageRouterConfiguration; import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.ConnectionManagerConfiguration; import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.RabbitMQConfiguration; -import com.exactpro.th2.common.schema.message.impl.rabbitmq.group.RabbitMessageGroupBatchRouter; -import com.exactpro.th2.common.schema.message.impl.rabbitmq.parsed.RabbitParsedBatchRouter; -import com.exactpro.th2.common.schema.message.impl.rabbitmq.raw.RabbitRawBatchRouter; import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ConfigMapList; import io.fabric8.kubernetes.api.model.Secret; @@ -65,11 +57,13 @@ import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Arrays; import java.util.Base64; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -77,7 +71,7 @@ import static com.exactpro.th2.common.schema.util.ArchiveUtils.getGzipBase64StringDecoder; import static java.util.Collections.emptyMap; import static java.util.Objects.requireNonNull; -import static java.util.Objects.requireNonNullElse; +import static java.util.Objects.requireNonNullElseGet; import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; /** @@ -85,23 +79,25 @@ */ public class CommonFactory extends AbstractCommonFactory { - private static final Path CONFIG_DEFAULT_PATH = Path.of("/var/th2/config/"); - - private static final String RABBIT_MQ_FILE_NAME = "rabbitMQ.json"; - private static final String ROUTER_MQ_FILE_NAME = "mq.json"; - private static final String GRPC_FILE_NAME = "grpc.json"; - private static final String ROUTER_GRPC_FILE_NAME = "grpc_router.json"; - private static final String CRADLE_CONFIDENTIAL_FILE_NAME = "cradle.json"; - private static final String PROMETHEUS_FILE_NAME = "prometheus.json"; - private static final String CUSTOM_FILE_NAME = "custom.json"; - private static final String BOX_FILE_NAME = "box.json"; - private static final String CONNECTION_MANAGER_CONF_FILE_NAME = "mq_router.json"; - private static final String CRADLE_NON_CONFIDENTIAL_FILE_NAME = "cradle_manager.json"; + public static final String TH2_COMMON_SYSTEM_PROPERTY = "th2.common"; + public static final String TH2_COMMON_CONFIGURATION_DIRECTORY_SYSTEM_PROPERTY = TH2_COMMON_SYSTEM_PROPERTY + '.' + "configuration-directory"; + static final Path CONFIG_DEFAULT_PATH = Path.of("/var/th2/config/"); + + static final String RABBIT_MQ_FILE_NAME = "rabbitMQ.json"; + static final String ROUTER_MQ_FILE_NAME = "mq.json"; + static final String GRPC_FILE_NAME = "grpc.json"; + static final String ROUTER_GRPC_FILE_NAME = "grpc_router.json"; + static final String CRADLE_CONFIDENTIAL_FILE_NAME = "cradle.json"; + static final String PROMETHEUS_FILE_NAME = "prometheus.json"; + static final String CUSTOM_FILE_NAME = "custom.json"; + static final String BOX_FILE_NAME = "box.json"; + static final String CONNECTION_MANAGER_CONF_FILE_NAME = "mq_router.json"; + static final String CRADLE_NON_CONFIDENTIAL_FILE_NAME = "cradle_manager.json"; /** @deprecated please use {@link #DICTIONARY_ALIAS_DIR_NAME} */ @Deprecated - private static final String DICTIONARY_TYPE_DIR_NAME = "dictionary"; - private static final String DICTIONARY_ALIAS_DIR_NAME = "dictionaries"; + static final String DICTIONARY_TYPE_DIR_NAME = "dictionary"; + static final String DICTIONARY_ALIAS_DIR_NAME = "dictionaries"; private static final String RABBITMQ_SECRET_NAME = "rabbitmq"; private static final String CASSANDRA_SECRET_NAME = "cassandra"; @@ -114,101 +110,25 @@ public class CommonFactory extends AbstractCommonFactory { private static final String GENERATED_CONFIG_DIR_NAME = "generated_configs"; private static final String RABBIT_MQ_EXTERNAL_APP_CONFIG_MAP = "rabbit-mq-external-app-config"; private static final String CRADLE_EXTERNAL_MAP = "cradle-external"; + private static final String CRADLE_MANAGER_CONFIG_MAP = "cradle-manager"; private static final String LOGGING_CONFIG_MAP = "logging-config"; private final Path custom; private final Path dictionaryTypesDir; private final Path dictionaryAliasesDir; private final Path oldDictionariesDir; - private final ConfigurationManager configurationManager; + final ConfigurationManager configurationManager; private static final Logger LOGGER = LoggerFactory.getLogger(CommonFactory.class.getName()); - protected CommonFactory(Class> messageRouterParsedBatchClass, - Class> messageRouterRawBatchClass, - Class> messageRouterMessageGroupBatchClass, - Class> eventBatchRouterClass, - Class grpcRouterClass, - @Nullable Path custom, - @Nullable Path dictionaryTypesDir, - @Nullable Path dictionaryAliasesDir, - @Nullable Path oldDictionariesDir, - Map environmentVariables, - ConfigurationManager configurationManager) { - super(messageRouterParsedBatchClass, messageRouterRawBatchClass, messageRouterMessageGroupBatchClass, eventBatchRouterClass, grpcRouterClass, environmentVariables); - - this.custom = defaultPathIfNull(custom, CUSTOM_FILE_NAME); - this.dictionaryTypesDir = defaultPathIfNull(dictionaryTypesDir, DICTIONARY_TYPE_DIR_NAME); - this.dictionaryAliasesDir = defaultPathIfNull(dictionaryAliasesDir, DICTIONARY_ALIAS_DIR_NAME); - this.oldDictionariesDir = requireNonNullElse(oldDictionariesDir, CONFIG_DEFAULT_PATH); - this.configurationManager = configurationManager; - - start(); - } - public CommonFactory(FactorySettings settings) { - this(settings.getMessageRouterParsedBatchClass(), - settings.getMessageRouterRawBatchClass(), - settings.getMessageRouterMessageGroupBatchClass(), - settings.getEventBatchRouterClass(), - settings.getGrpcRouterClass(), - settings.getCustom(), - settings.getDictionaryTypesDir(), - settings.getDictionaryAliasesDir(), - settings.getOldDictionariesDir(), - settings.getVariables(), - createConfigurationManager(settings)); - } - - - /** - * @deprecated Please use {@link CommonFactory#CommonFactory(FactorySettings)} - */ - @Deprecated(since = "3.10.0", forRemoval = true) - public CommonFactory(Class> messageRouterParsedBatchClass, - Class> messageRouterRawBatchClass, - Class> messageRouterMessageGroupBatchClass, - Class> eventBatchRouterClass, - Class grpcRouterClass, - Path rabbitMQ, Path routerMQ, Path routerGRPC, Path cradle, Path custom, Path prometheus, Path dictionariesDir, Path boxConfiguration) { - - this(new FactorySettings(messageRouterParsedBatchClass, - messageRouterRawBatchClass, - messageRouterMessageGroupBatchClass, - eventBatchRouterClass, - grpcRouterClass, - rabbitMQ, - routerMQ, - null, - routerGRPC, - null, - cradle, - null, - prometheus, - boxConfiguration, - custom, - dictionariesDir)); - } - - /** - * @deprecated Please use {@link CommonFactory#CommonFactory(FactorySettings)} - */ - @Deprecated(since = "3.10.0", forRemoval = true) - public CommonFactory(Path rabbitMQ, Path routerMQ, Path routerGRPC, Path cradle, Path custom, Path prometheus, Path dictionariesDir, Path boxConfiguration) { - this(RabbitParsedBatchRouter.class, RabbitRawBatchRouter.class, RabbitMessageGroupBatchRouter.class, EventBatchRouter.class, DefaultGrpcRouter.class, - rabbitMQ ,routerMQ ,routerGRPC ,cradle ,custom ,dictionariesDir ,prometheus ,boxConfiguration); - } - - /** - * @deprecated Please use {@link CommonFactory#CommonFactory(FactorySettings)} - */ - @Deprecated(since = "3.10.0", forRemoval = true) - public CommonFactory(Class> messageRouterParsedBatchClass, - Class> messageRouterRawBatchClass, - Class> messageRouterMessageGroupBatchClass, - Class> eventBatchRouterClass, - Class grpcRouterClass) { - this(new FactorySettings(messageRouterParsedBatchClass, messageRouterRawBatchClass, messageRouterMessageGroupBatchClass, eventBatchRouterClass, grpcRouterClass)); + super(settings); + custom = defaultPathIfNull(settings.getCustom(), CUSTOM_FILE_NAME); + dictionaryTypesDir = defaultPathIfNull(settings.getDictionaryTypesDir(), DICTIONARY_TYPE_DIR_NAME); + dictionaryAliasesDir = defaultPathIfNull(settings.getDictionaryAliasesDir(), DICTIONARY_ALIAS_DIR_NAME); + oldDictionariesDir = requireNonNullElseGet(settings.getOldDictionariesDir(), CommonFactory::getConfigPath); + configurationManager = createConfigurationManager(settings); + start(); } public CommonFactory() { @@ -268,7 +188,7 @@ protected ConfigurationManager getConfigurationManager() { *

* --connectionManagerConfiguration - path to json file with for {@link ConnectionManagerConfiguration} *

- * --cradleManagerConfiguration - path to json file with for {@link CradleNonConfidentialConfiguration} + * --cradleManagerConfiguration - path to json file with for {@link CradleNonConfidentialConfiguration} and {@link CassandraStorageSettings} *

* --namespace - namespace in Kubernetes to find config maps related to the target *

@@ -316,7 +236,7 @@ public static CommonFactory createFromArguments(String... args) { try { CommandLine cmd = new DefaultParser().parse(options, args); - String configs = cmd.getOptionValue(configOption.getLongOpt()); + Path configs = getConfigPath(cmd.getOptionValue(configOption.getLongOpt())); if (cmd.hasOption(namespaceOption.getLongOpt()) && cmd.hasOption(boxNameOption.getLongOpt())) { String namespace = cmd.getOptionValue(namespaceOption.getLongOpt()); @@ -347,7 +267,7 @@ public static CommonFactory createFromArguments(String... args) { return createFromKubernetes(namespace, boxName, contextName, dictionaries); } - if (configs != null) { + if (!CONFIG_DEFAULT_PATH.equals(configs)) { configureLogger(configs); } FactorySettings settings = new FactorySettings(); @@ -364,7 +284,7 @@ public static CommonFactory createFromArguments(String... args) { settings.setDictionaryTypesDir(calculatePath(cmd, dictionariesDirOption, configs, DICTIONARY_TYPE_DIR_NAME)); settings.setDictionaryAliasesDir(calculatePath(cmd, dictionariesDirOption, configs, DICTIONARY_ALIAS_DIR_NAME)); String oldDictionariesDir = cmd.getOptionValue(dictionariesDirOption.getLongOpt()); - settings.setOldDictionariesDir(oldDictionariesDir == null ? (configs == null ? CONFIG_DEFAULT_PATH : Path.of(configs)) : Path.of(oldDictionariesDir)); + settings.setOldDictionariesDir(oldDictionariesDir == null ? configs : Path.of(oldDictionariesDir)); return new CommonFactory(settings); } catch (ParseException e) { @@ -443,19 +363,22 @@ public static CommonFactory createFromKubernetes(String namespace, String boxNam throw new IllegalArgumentException("Failed to find config maps by boxName " + boxName); } Resource rabbitMqConfigMapResource = configMaps.inNamespace(namespace).withName(RABBIT_MQ_EXTERNAL_APP_CONFIG_MAP); - Resource cradleConfigMapResource = configMaps.inNamespace(namespace).withName(CRADLE_EXTERNAL_MAP); + Resource cradleConfidentialConfigMapResource = configMaps.inNamespace(namespace).withName(CRADLE_EXTERNAL_MAP); + Resource cradleNonConfidentialConfigMapResource = configMaps.inNamespace(namespace).withName(CRADLE_MANAGER_CONFIG_MAP); Resource loggingConfigMapResource = configMaps.inNamespace(namespace).withName(LOGGING_CONFIG_MAP); ConfigMap boxConfigMap = boxConfigMapResource.require(); ConfigMap rabbitMqConfigMap = rabbitMqConfigMapResource.require(); - ConfigMap cradleConfigMap = cradleConfigMapResource.require(); + ConfigMap cradleConfidentialConfigmap = cradleConfidentialConfigMapResource.require(); + ConfigMap cradleNonConfidentialConfigmap = cradleNonConfidentialConfigMapResource.require(); @Nullable ConfigMap loggingConfigMap = loggingConfigMapResource.get(); Map boxData = boxConfigMap.getData(); Map rabbitMqData = rabbitMqConfigMap.getData(); - Map cradleConfigData = cradleConfigMap.getData(); - @Nullable String loggingData = boxData.getOrDefault(LOG4J_PROPERTIES_NAME, - loggingConfigMap == null ? null : loggingConfigMap.getData().get(LOG4J_PROPERTIES_NAME) + Map cradleConfidential = cradleConfidentialConfigmap.getData(); + Map cradleNonConfidential = cradleNonConfidentialConfigmap.getData(); + @Nullable String loggingData = boxData.getOrDefault(LOG4J2_PROPERTIES_NAME, + loggingConfigMap == null ? null : loggingConfigMap.getData().get(LOG4J2_PROPERTIES_NAME) ); File generatedConfigsDirFile = configPath.toFile(); @@ -471,8 +394,8 @@ public static CommonFactory createFromKubernetes(String namespace, String boxNam box.setBoxName(boxName); if (loggingData != null) { - writeFile(configPath.resolve(LOG4J_PROPERTIES_NAME), loggingData); - configureLogger(configPath.toString()); + writeFile(configPath.resolve(LOG4J2_PROPERTIES_NAME), loggingData); + configureLogger(configPath); } settings.setRabbitMQ(writeFile(configPath, RABBIT_MQ_FILE_NAME, rabbitMqData)); @@ -480,8 +403,8 @@ public static CommonFactory createFromKubernetes(String namespace, String boxNam settings.setConnectionManagerSettings(writeFile(configPath, CONNECTION_MANAGER_CONF_FILE_NAME, boxData)); settings.setGrpc(writeFile(configPath, GRPC_FILE_NAME, boxData)); settings.setRouterGRPC(writeFile(configPath, ROUTER_GRPC_FILE_NAME, boxData)); - settings.setCradleConfidential(writeFile(configPath, CRADLE_CONFIDENTIAL_FILE_NAME, cradleConfigData)); - settings.setCradleNonConfidential(writeFile(configPath, CRADLE_NON_CONFIDENTIAL_FILE_NAME, boxData)); + settings.setCradleConfidential(writeFile(configPath, CRADLE_CONFIDENTIAL_FILE_NAME, cradleConfidential)); + settings.setCradleNonConfidential(writeFile(configPath, CRADLE_NON_CONFIDENTIAL_FILE_NAME, cradleNonConfidential)); settings.setPrometheus(writeFile(configPath, PROMETHEUS_FILE_NAME, boxData)); settings.setCustom(writeFile(configPath, CUSTOM_FILE_NAME, boxData)); @@ -497,7 +420,7 @@ public static CommonFactory createFromKubernetes(String namespace, String boxNam writeFile(boxConfigurationPath, boxConfig); } - writeDictionaries(boxName, configPath, dictionaryTypePath, dictionaries, configMaps.list()); + writeDictionaries(dictionaryTypePath, dictionaryAliasPath, dictionaries, configMaps.list()); } return new CommonFactory(settings); @@ -627,57 +550,102 @@ public InputStream readDictionary(DictionaryType dictionaryType) { } } + static @NotNull Path getConfigPath() { + String pathString = System.getProperty(TH2_COMMON_CONFIGURATION_DIRECTORY_SYSTEM_PROPERTY); + if (pathString != null) { + Path path = Paths.get(pathString); + if (Files.exists(path) && Files.isDirectory(path)) { + return path; + } + LOGGER.warn("'{}' config directory passed via '{}' system property doesn't exist or it is not a directory", + pathString, + TH2_COMMON_CONFIGURATION_DIRECTORY_SYSTEM_PROPERTY); + } else { + LOGGER.debug("Skipped blank environment variable path for configs directory"); + } + + if (!Files.exists(CONFIG_DEFAULT_PATH)) { + LOGGER.error("'{}' default config directory doesn't exist", CONFIG_DEFAULT_PATH); + } + return CONFIG_DEFAULT_PATH; + } + + static @NotNull Path getConfigPath(@Nullable String cmdPath) { + String pathString = StringUtils.trim(cmdPath); + if (pathString != null) { + Path path = Paths.get(pathString); + if (Files.exists(path) && Files.isDirectory(path)) { + return path; + } + LOGGER.warn("'{}' config directory passed via CMD doesn't exist or it is not a directory", cmdPath); + } else { + LOGGER.debug("Skipped blank CMD path for configs directory"); + } + + return getConfigPath(); + } + private static Path writeFile(Path configPath, String fileName, Map configMap) throws IOException { Path file = configPath.resolve(fileName); writeFile(file, configMap.get(fileName)); return file; } - private static void writeDictionaries(String boxName, Path oldDictionariesDir, Path dictionariesDir, Map dictionaries, ConfigMapList configMapList) throws IOException { + private static void writeDictionaries(Path oldDictionariesDir, Path dictionariesDir, Map dictionaries, ConfigMapList configMapList) throws IOException { + createDirectory(dictionariesDir); + for(ConfigMap configMap : configMapList.getItems()) { String configMapName = configMap.getMetadata().getName(); - if(configMapName.startsWith(boxName) && configMapName.endsWith("-dictionary")) { - configMap.getData().forEach((fileName, base64) -> { - try { - writeFile(oldDictionariesDir.resolve(fileName), base64); - } catch (IOException e) { - LOGGER.error("Can not write dictionary '{}' from config map with name '{}'", fileName, configMapName); - } - }); - } - } - for (Map.Entry entry : dictionaries.entrySet()) { - DictionaryType type = entry.getKey(); - String dictionaryName = entry.getValue(); - for (ConfigMap dictionaryConfigMap : configMapList.getItems()) { - String configName = dictionaryConfigMap.getMetadata().getName(); - if (configName.endsWith("-dictionary") && configName.substring(0, configName.lastIndexOf('-')).equals(dictionaryName)) { - Path dictionaryTypeDir = type.getDictionary(dictionariesDir); - - if (Files.notExists(dictionaryTypeDir)) { - Files.createDirectories(dictionaryTypeDir); - } else if (!Files.isDirectory(dictionaryTypeDir)) { - throw new IllegalStateException( - String.format("Can not save dictionary '%s' with type '%s', because '%s' is not directory", dictionaryName, type, dictionaryTypeDir) - ); - } + if (!configMapName.endsWith("-dictionary")) { + continue; + } - Set fileNameSet = dictionaryConfigMap.getData().keySet(); + String dictionaryName = configMapName.substring(0, configMapName.lastIndexOf('-')); - if (fileNameSet.size() != 1) { - throw new IllegalStateException( - String.format("Can not save dictionary '%s' with type '%s', because can not find dictionary data in config map", dictionaryName, type) - ); - } + dictionaries.entrySet().stream() + .filter(entry -> Objects.equals(entry.getValue(), dictionaryName)) + .forEach(entry -> { + DictionaryType type = entry.getKey(); + try { + Path dictionaryTypeDir = type.getDictionary(oldDictionariesDir); + createDirectory(dictionaryTypeDir); + + if (configMap.getData().size() != 1) { + throw new IllegalStateException( + String.format("Can not save dictionary '%s' with type '%s', because can not find dictionary data in config map", dictionaryName, type) + ); + } + + downloadFiles(dictionariesDir, configMap); + downloadFiles(dictionaryTypeDir, configMap); + } catch (Exception e) { + throw new IllegalStateException("Loading the " + dictionaryName + " dictionary with type " + type + " failures", e); + } + }); + } + } - String fileName = fileNameSet.stream().findFirst().orElse(null); - Path dictionaryPath = dictionaryTypeDir.resolve(fileName); - writeFile(dictionaryPath, dictionaryConfigMap.getData().get(fileName)); - LOGGER.debug("Dictionary written in folder: " + dictionaryPath); - break; + private static void downloadFiles(Path baseDir, ConfigMap configMap) { + String configMapName = configMap.getMetadata().getName(); + configMap.getData().forEach((fileName, base64) -> { + try { + Path path = baseDir.resolve(fileName); + if (!Files.exists(path)) { + writeFile(path, base64); + LOGGER.info("The '{}' config has been downloaded from the '{}' config map to the '{}' path", fileName, configMapName, path); } + } catch (IOException e) { + LOGGER.error("Can not download the '{}' file from the '{}' config map", fileName, configMapName); } + }); + } + + private static void createDirectory(Path dir) throws IOException { + if (Files.notExists(dir)) { + Files.createDirectories(dir); + } else if (!Files.isDirectory(dir)) { + throw new IllegalStateException("Can not save dictionary '" + dir + "' because the '" + dir + "' has already exist and isn't a directory"); } } @@ -690,13 +658,14 @@ private static ConfigurationManager createConfigurationManager(FactorySettings s paths.put(GrpcRouterConfiguration.class, defaultPathIfNull(settings.getRouterGRPC(), ROUTER_GRPC_FILE_NAME)); paths.put(CradleConfidentialConfiguration.class, defaultPathIfNull(settings.getCradleConfidential(), CRADLE_CONFIDENTIAL_FILE_NAME)); paths.put(CradleNonConfidentialConfiguration.class, defaultPathIfNull(settings.getCradleNonConfidential(), CRADLE_NON_CONFIDENTIAL_FILE_NAME)); + paths.put(CassandraStorageSettings.class, defaultPathIfNull(settings.getCradleNonConfidential(), CRADLE_NON_CONFIDENTIAL_FILE_NAME)); paths.put(PrometheusConfiguration.class, defaultPathIfNull(settings.getPrometheus(), PROMETHEUS_FILE_NAME)); paths.put(BoxConfiguration.class, defaultPathIfNull(settings.getBoxConfiguration(), BOX_FILE_NAME)); return new ConfigurationManager(paths); } private static Path defaultPathIfNull(Path path, String name) { - return path == null ? CONFIG_DEFAULT_PATH.resolve(name) : path; + return path == null ? getConfigPath().resolve(name) : path; } private static void writeFile(Path path, String data) throws IOException { @@ -718,15 +687,15 @@ private static Option createLongOption(Options options, String optionName) { return option; } - private static Path calculatePath(String path, String configsPath, String fileName) { - return path != null ? Path.of(path) : (configsPath != null ? Path.of(configsPath, fileName) : CONFIG_DEFAULT_PATH.resolve(fileName)); + private static Path calculatePath(String path, @NotNull Path configsPath, String fileName) { + return path != null ? Path.of(path) : configsPath.resolve(fileName); } - private static Path calculatePath(CommandLine cmd, Option option, String configs, String fileName) { + private static Path calculatePath(CommandLine cmd, Option option, @NotNull Path configs, String fileName) { return calculatePath(cmd.getOptionValue(option.getLongOpt()), configs, fileName); } - private static Path calculatePath(CommandLine cmd, Option current, Option deprecated, String configs, String fileName) { + private static Path calculatePath(CommandLine cmd, Option current, Option deprecated, @NotNull Path configs, String fileName) { return calculatePath(defaultIfNull(cmd.getOptionValue(current.getLongOpt()), cmd.getOptionValue(deprecated.getLongOpt())), configs, fileName); } } diff --git a/src/main/java/com/exactpro/th2/common/schema/filter/strategy/impl/AbstractFilterStrategy.java b/src/main/java/com/exactpro/th2/common/schema/filter/strategy/impl/AbstractFilterStrategy.java index fee14236e..7523d6208 100644 --- a/src/main/java/com/exactpro/th2/common/schema/filter/strategy/impl/AbstractFilterStrategy.java +++ b/src/main/java/com/exactpro/th2/common/schema/filter/strategy/impl/AbstractFilterStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2023 Exactpro (Exactpro Systems Limited) * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -52,7 +52,7 @@ public boolean verify(T message, List routerFilters) { protected abstract Map getFields(T message); private boolean checkValues(Map messageFields, MultiValuedMap fieldFilters) { - return fieldFilters.isEmpty() || fieldFilters.keys().stream().anyMatch(fieldName -> { + return fieldFilters.isEmpty() || fieldFilters.keys().stream().allMatch(fieldName -> { String messageValue = messageFields.get(fieldName); Collection filters = fieldFilters.get(fieldName); return !filters.isEmpty() && filters.stream().allMatch(filter -> FieldValueChecker.checkFieldValue(filter, messageValue)); diff --git a/src/main/java/com/exactpro/th2/common/schema/filter/strategy/impl/AbstractTh2MsgFilterStrategy.java b/src/main/java/com/exactpro/th2/common/schema/filter/strategy/impl/AbstractTh2MsgFilterStrategy.java index 2d0fe5964..672a3a9a1 100644 --- a/src/main/java/com/exactpro/th2/common/schema/filter/strategy/impl/AbstractTh2MsgFilterStrategy.java +++ b/src/main/java/com/exactpro/th2/common/schema/filter/strategy/impl/AbstractTh2MsgFilterStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2023 Exactpro (Exactpro Systems Limited) * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -15,7 +15,6 @@ package com.exactpro.th2.common.schema.filter.strategy.impl; - import com.exactpro.th2.common.grpc.Message; import com.exactpro.th2.common.grpc.MessageID; import com.exactpro.th2.common.grpc.MessageMetadata; @@ -23,12 +22,14 @@ import java.util.Map; import java.util.stream.Collectors; - public abstract class AbstractTh2MsgFilterStrategy extends AbstractFilterStrategy { + public static final String BOOK_KEY = "book"; + public static final String SESSION_GROUP_KEY = "session_group"; public static final String SESSION_ALIAS_KEY = "session_alias"; public static final String MESSAGE_TYPE_KEY = "message_type"; public static final String DIRECTION_KEY = "direction"; + public static final String PROTOCOL_KEY = "protocol"; @Override public Map getFields(com.google.protobuf.Message message) { @@ -41,10 +42,15 @@ public Map getFields(com.google.protobuf.Message message) { .map(entry -> Map.entry(entry.getKey(), entry.getValue().getSimpleValue())) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + String sessionGroup = messageID.getConnectionId().getSessionGroup(); + String sessionAlias = messageID.getConnectionId().getSessionAlias(); var metadataMsgFields = Map.of( - SESSION_ALIAS_KEY, messageID.getConnectionId().getSessionAlias(), + BOOK_KEY, messageID.getBookName(), + SESSION_GROUP_KEY, sessionGroup.isEmpty() ? sessionAlias : sessionGroup, + SESSION_ALIAS_KEY, sessionAlias, MESSAGE_TYPE_KEY, metadata.getMessageType(), - DIRECTION_KEY, messageID.getDirection().name() + DIRECTION_KEY, messageID.getDirection().name(), + PROTOCOL_KEY, metadata.getProtocol() ); messageFields.putAll(metadataMsgFields); @@ -53,5 +59,4 @@ public Map getFields(com.google.protobuf.Message message) { } public abstract Message parseMessage(com.google.protobuf.Message message); - -} +} \ No newline at end of file diff --git a/src/main/java/com/exactpro/th2/common/schema/grpc/router/AbstractGrpcRouter.java b/src/main/java/com/exactpro/th2/common/schema/grpc/router/AbstractGrpcRouter.java index 2cce694b1..1ee5fca67 100644 --- a/src/main/java/com/exactpro/th2/common/schema/grpc/router/AbstractGrpcRouter.java +++ b/src/main/java/com/exactpro/th2/common/schema/grpc/router/AbstractGrpcRouter.java @@ -39,6 +39,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.concurrent.ExecutorService; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -49,8 +50,6 @@ /** * Abstract implementation for {@link GrpcRouter} *

- * Implement {@link GrpcRouter#init(GrpcRouterConfiguration)} - *

* Implement {@link GrpcRouter#init(GrpcConfiguration, GrpcRouterConfiguration)} *

* Implement {@link GrpcRouter#startServer(BindableService...)} @@ -68,6 +67,12 @@ public abstract class AbstractGrpcRouter implements GrpcRouter { protected GrpcRouterConfiguration routerConfiguration; protected GrpcConfiguration configuration; + // Metrics below are collectd to maps to improve getting .Child performance. + // Prometheus metric has the `labels` tread-safe method for getting .Child but its logic includes: + // * array String creation + // * covert the array to list + // * search .Child by the list + protected static final Counter GRPC_INVOKE_CALL_TOTAL = Counter.build() .name("th2_grpc_invoke_call_total") .labelNames(CommonMetrics.TH2_PIN_LABEL, CommonMetrics.GRPC_SERVICE_NAME_LABEL, CommonMetrics.GRPC_METHOD_NAME_LABEL) @@ -116,11 +121,6 @@ public abstract class AbstractGrpcRouter implements GrpcRouter { protected static final Map GRPC_RECEIVE_CALL_RESPONSE_SIZE_MAP = new ConcurrentHashMap<>(); - @Override - public void init(GrpcRouterConfiguration configuration) { - init(new GrpcConfiguration(), configuration); - } - @Override public void init(@NotNull GrpcConfiguration configuration, @NotNull GrpcRouterConfiguration routerConfiguration) { failIfInitialized(); diff --git a/src/main/java/com/exactpro/th2/common/schema/grpc/router/GrpcRouter.java b/src/main/java/com/exactpro/th2/common/schema/grpc/router/GrpcRouter.java index 8190ab725..47e959d6f 100644 --- a/src/main/java/com/exactpro/th2/common/schema/grpc/router/GrpcRouter.java +++ b/src/main/java/com/exactpro/th2/common/schema/grpc/router/GrpcRouter.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2023 Exactpro (Exactpro Systems Limited) * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -26,11 +26,6 @@ * @see AbstractGrpcRouter */ public interface GrpcRouter extends AutoCloseable { - /** - * Initialization router - */ - @Deprecated(since = "3.9.0", forRemoval = true) - void init(GrpcRouterConfiguration configuration); void init(@NotNull GrpcConfiguration configuration, @NotNull GrpcRouterConfiguration routerConfiguration); diff --git a/src/main/java/com/exactpro/th2/common/schema/grpc/router/impl/DefaultGrpcRouter.java b/src/main/java/com/exactpro/th2/common/schema/grpc/router/impl/DefaultGrpcRouter.java index 29a7ca4dc..2e7df256c 100644 --- a/src/main/java/com/exactpro/th2/common/schema/grpc/router/impl/DefaultGrpcRouter.java +++ b/src/main/java/com/exactpro/th2/common/schema/grpc/router/impl/DefaultGrpcRouter.java @@ -180,7 +180,7 @@ protected List> getServiceConfig(Cla }) .collect(Collectors.toList()); if (result.isEmpty()) { - throw new IllegalStateException("No services matching the provided class were found in the configuration: " + throw new IllegalStateException("No services matching the provided class were found in the configuration: " + proxyService.getName()); } @@ -215,9 +215,9 @@ protected Channel getOrCreateChannel(String endpointName, Map.Entry AbstractStub createStubInstance(Class stubClass, Channel channel) { try { - var constr = stubClass.getDeclaredConstructor(Channel.class, CallOptions.class); - constr.setAccessible(true); - return constr.newInstance(channel, CallOptions.DEFAULT); + var constructor = stubClass.getDeclaredConstructor(Channel.class, CallOptions.class); + constructor.setAccessible(true); + return constructor.newInstance(channel, CallOptions.DEFAULT); } catch (NoSuchMethodException e) { throw new InitGrpcRouterException("Could not find constructor " + "'(Channel,CallOptions)' in the provided stub class: " + stubClass, e); @@ -225,4 +225,4 @@ protected AbstractStub createStubInstance(Class stub throw new InitGrpcRouterException("Something went wrong while creating stub instance: " + stubClass, e); } } -} \ No newline at end of file +} diff --git a/src/main/java/com/exactpro/th2/common/schema/grpc/router/impl/DefaultStubStorage.java b/src/main/java/com/exactpro/th2/common/schema/grpc/router/impl/DefaultStubStorage.java index 2d29b89fc..dc30ebfcb 100644 --- a/src/main/java/com/exactpro/th2/common/schema/grpc/router/impl/DefaultStubStorage.java +++ b/src/main/java/com/exactpro/th2/common/schema/grpc/router/impl/DefaultStubStorage.java @@ -146,4 +146,4 @@ public T getStub(@NotNull Message message, @NotNull AbstractStub.StubFactory private boolean isAllPropertiesMatch(List filterProp, Map properties) { return filterProp.stream().allMatch(it -> FieldValueChecker.checkFieldValue(it, properties.get(it.getFieldName()))); } -} \ No newline at end of file +} diff --git a/src/main/java/com/exactpro/th2/common/schema/message/MessageListener.java b/src/main/java/com/exactpro/th2/common/schema/message/MessageListener.java index 0b1baefe1..b0912165d 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/MessageListener.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/MessageListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2020 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2022 Exactpro (Exactpro Systems Limited) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,8 +21,7 @@ */ public interface MessageListener { - void handler(String consumerTag, T message) throws Exception; + void handle(DeliveryMetadata deliveryMetadata, T message) throws Exception; default void onClose() {} - } diff --git a/src/main/java/com/exactpro/th2/common/schema/message/MessageRouter.java b/src/main/java/com/exactpro/th2/common/schema/message/MessageRouter.java index a7de45b68..c7c781de9 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/MessageRouter.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/MessageRouter.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2022 Exactpro (Exactpro Systems Limited) * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -16,10 +16,10 @@ package com.exactpro.th2.common.schema.message; import com.exactpro.th2.common.grpc.MessageGroupBatch; +import com.exactpro.th2.common.schema.box.configuration.BoxConfiguration; import com.exactpro.th2.common.schema.message.configuration.MessageRouterConfiguration; import com.exactpro.th2.common.schema.message.impl.context.DefaultMessageRouterContext; import com.exactpro.th2.common.schema.message.impl.rabbitmq.connection.ConnectionManager; - import org.jetbrains.annotations.NotNull; import java.io.IOException; @@ -33,15 +33,13 @@ public interface MessageRouter extends AutoCloseable { /** * Initialization message router - * @param connectionManager * @param configuration message router configuration */ @Deprecated(since = "3.2.2", forRemoval = true) default void init(@NotNull ConnectionManager connectionManager, @NotNull MessageRouterConfiguration configuration) { Objects.requireNonNull(connectionManager, "Connection owner can not be null"); Objects.requireNonNull(configuration, "Configuration cannot be null"); - - init(new DefaultMessageRouterContext(connectionManager, MessageRouterMonitor.DEFAULT_MONITOR, configuration)); + init(new DefaultMessageRouterContext(connectionManager, MessageRouterMonitor.DEFAULT_MONITOR, configuration, new BoxConfiguration())); } default void init(@NotNull MessageRouterContext context, @NotNull MessageRouter groupBatchRouter) { @@ -55,68 +53,71 @@ default void init(@NotNull MessageRouterContext context, @NotNull MessageRouter< void init(@NotNull MessageRouterContext context); /** - * Listen ONE RabbitMQ queue by intersection schemas queues attributes - * @param callback listener - * @param queueAttr queues attributes - * @throws IllegalStateException when more than 1 queue is found - * @return {@link SubscriberMonitor} it start listening. Returns null if can not listen to this queue + * Creates a new exclusive queue and subscribes to it. Only declaring connection can use this queue. + * Please note Exclusive queues are deleted when their declaring connection is closed or gone (e.g. due to underlying TCP connection loss). + * They, therefore, are only suitable for client-specific transient states. + * @return {@link ExclusiveSubscriberMonitor} object to manage subscription. */ - SubscriberMonitor subscribe(MessageListener callback, String... queueAttr); + ExclusiveSubscriberMonitor subscribeExclusive(MessageListener callback); /** - * Listen ALL RabbitMQ queues in configurations + * Listen ONE RabbitMQ queue by intersection schemas queues attributes. + * Restrictions: + * You can create only one subscription to th2 pin using any subscribe* functions. + * Internal state: + * Router uses external Connection Manage to interact with RabbitMQ, which holds one connection and one channel per th2 pin in general. + * This rule exception is re-connect to RabbitMQ when the manager establishes new connection and creates new channels. * @param callback listener - * @return {@link SubscriberMonitor} it start listening. Returns null if can not listen to this queue + * @param queueAttr queues attributes + * @throws IllegalStateException when more than 1 queue is found + * @throws RuntimeException when the th2 pin is matched by passed attributes already has active subscription + * @return {@link SubscriberMonitor} it start listening. */ - default SubscriberMonitor subscribeAll(MessageListener callback) { - return subscribeAll(callback, QueueAttribute.SUBSCRIBE.toString()); - } + SubscriberMonitor subscribe(MessageListener callback, String... queueAttr); /** * Listen SOME RabbitMQ queues by intersection schemas queues attributes + * @see #subscribe(MessageListener, String...) * @param callback listener * @param queueAttr queues attributes - * @return {@link SubscriberMonitor} it start listening. Returns null if can not listen to this queue + * @return {@link SubscriberMonitor} it start listening. */ SubscriberMonitor subscribeAll(MessageListener callback, String... queueAttr); /** * Listen ONE RabbitMQ queue by intersection schemas queues attributes + * @see #subscribe(MessageListener, String...) * @param queueAttr queues attributes * @param callback listener with manual confirmation * @throws IllegalStateException when more than 1 queue is found - * @return {@link SubscriberMonitor} it start listening. Returns null if can not listen to this queue + * @return {@link SubscriberMonitor} it start listening. */ - default SubscriberMonitor subscribeWithManualAck(ConfirmationMessageListener callback, String... queueAttr) { + default SubscriberMonitor subscribeWithManualAck(ManualConfirmationListener callback, String... queueAttr) { // TODO: probably should not have default implementation throw new UnsupportedOperationException("The subscription with manual confirmation is not supported"); } - /** - * Listen ALL RabbitMQ queues in configurations - * @param callback listener with manual confirmation - * @return {@link SubscriberMonitor} it start listening. Returns null if can not listen to this queue - */ - default SubscriberMonitor subscribeAllWithManualAck(ConfirmationMessageListener callback) { - // TODO: probably should not have default implementation - return subscribeAllWithManualAck(callback, QueueAttribute.SUBSCRIBE.toString()); - } - /** * Listen SOME RabbitMQ queues by intersection schemas queues attributes + * @see #subscribe(MessageListener, String...) * @param callback listener with manual confirmation * @param queueAttr queues attributes - * @return {@link SubscriberMonitor} it start listening. Returns null if can not listen to this queue + * @return {@link SubscriberMonitor} it start listening. */ - default SubscriberMonitor subscribeAllWithManualAck(ConfirmationMessageListener callback, String... queueAttr) { + default SubscriberMonitor subscribeAllWithManualAck(ManualConfirmationListener callback, String... queueAttr) { // TODO: probably should not have default implementation throw new UnsupportedOperationException("The subscription with manual confirmation is not supported"); } + /** + * Send the message to the queue + * @throws IOException if router can not send message + */ + void sendExclusive(String queue, T message) throws IOException; + /** * Send message to SOME RabbitMQ queues which match the filter for this message - * @param message - * @throws IOException if can not send message + * @throws IOException if router can not send message */ default void send(T message) throws IOException { send(message, QueueAttribute.PUBLISH.toString()); @@ -124,7 +125,6 @@ default void send(T message) throws IOException { /** * Send message to ONE RabbitMQ queue by intersection schemas queues attributes - * @param message * @param queueAttr schemas queues attributes * @throws IOException if can not send message * @throws IllegalStateException when more than 1 queue is found @@ -133,7 +133,6 @@ default void send(T message) throws IOException { /** * Send message to SOME RabbitMQ queue by intersection schemas queues attributes - * @param message * @param queueAttr schemas queues attributes * @throws IOException if can not send message */ diff --git a/src/main/java/com/exactpro/th2/common/schema/message/MessageSubscriber.java b/src/main/java/com/exactpro/th2/common/schema/message/MessageSubscriber.java index 1b57f96bd..e4c2feb07 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/MessageSubscriber.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/MessageSubscriber.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2023 Exactpro (Exactpro Systems Limited) * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -15,27 +15,10 @@ package com.exactpro.th2.common.schema.message; -import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.SubscribeTarget; -import com.exactpro.th2.common.schema.message.impl.rabbitmq.connection.ConnectionManager; - -import org.jetbrains.annotations.NotNull; - -import javax.annotation.concurrent.NotThreadSafe; +import javax.annotation.concurrent.ThreadSafe; /** * Listen messages and transmit it to {@link MessageListener} */ -@NotThreadSafe -public interface MessageSubscriber extends AutoCloseable { - // Please use constructor for initialization - @Deprecated(since = "3.3.0", forRemoval = true) - void init(@NotNull ConnectionManager connectionManager, @NotNull String exchangeName, @NotNull SubscribeTarget subscribeTargets); - - // Please use constructor for initialization - @Deprecated - void init(@NotNull ConnectionManager connectionManager, @NotNull SubscribeTarget subscribeTarget, @NotNull FilterFunction filterFunc); - - void start() throws Exception; - - void addListener(ConfirmationMessageListener messageListener); -} +@ThreadSafe +public interface MessageSubscriber extends AutoCloseable { } diff --git a/src/main/java/com/exactpro/th2/common/schema/message/NotificationRouter.kt b/src/main/java/com/exactpro/th2/common/schema/message/NotificationRouter.kt new file mode 100644 index 000000000..2d2222bd2 --- /dev/null +++ b/src/main/java/com/exactpro/th2/common/schema/message/NotificationRouter.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2021-2021 Exactpro (Exactpro Systems Limited) + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.exactpro.th2.common.schema.message + +import com.exactpro.th2.common.schema.exception.RouterException + +/** + * Interface for send and receive RabbitMQ notification messages + * + * @param messages for send and receive + */ +interface NotificationRouter : AutoCloseable { + /** + * Initialization message router + * @param context router context + */ + fun init(context: MessageRouterContext) + + /** + * Send message to exclusive RabbitMQ queue + * + * @param message + * @throws com.exactpro.th2.common.schema.exception.RouterException if it cannot send message + */ + @Throws(RouterException::class) + fun send(message: T) + + /** + * Listen exclusive RabbitMQ queue + * + * @param callback listener + * @return SubscriberMonitor if starts listening + */ + fun subscribe(callback: MessageListener): SubscriberMonitor +} \ No newline at end of file diff --git a/src/main/java/com/exactpro/th2/common/schema/message/QueueAttribute.java b/src/main/java/com/exactpro/th2/common/schema/message/QueueAttribute.java index 612211024..50407db01 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/QueueAttribute.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/QueueAttribute.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2023 Exactpro (Exactpro Systems Limited) * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -12,6 +12,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.exactpro.th2.common.schema.message; public enum QueueAttribute { @@ -22,7 +23,8 @@ public enum QueueAttribute { RAW("raw"), PARSED("parsed"), STORE("store"), - EVENT("event"); + EVENT("event"), + TRANSPORT_GROUP("transport-group"); private final String value; diff --git a/src/main/java/com/exactpro/th2/common/schema/message/SubscriberMonitor.java b/src/main/java/com/exactpro/th2/common/schema/message/SubscriberMonitor.java index 1609018d5..86c882057 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/SubscriberMonitor.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/SubscriberMonitor.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2020 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2022 Exactpro (Exactpro Systems Limited) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,10 @@ package com.exactpro.th2.common.schema.message; +import java.io.IOException; + public interface SubscriberMonitor { - void unsubscribe() throws Exception; + void unsubscribe() throws IOException; } diff --git a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitSender.java b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitSender.java index 751bc4113..dfa769dce 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitSender.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitSender.java @@ -51,6 +51,7 @@ public abstract class AbstractRabbitSender implements MessageSender { .register(); protected final String th2Pin; + protected final String bookName; private final AtomicReference routingKey = new AtomicReference<>(); private final AtomicReference exchangeName = new AtomicReference<>(); private final AtomicReference connectionManager = new AtomicReference<>(); @@ -61,13 +62,15 @@ public AbstractRabbitSender( @NotNull String exchangeName, @NotNull String routingKey, @NotNull String th2Pin, - @NotNull String th2Type + @NotNull String th2Type, + @NotNull String bookName ) { this.connectionManager.set(requireNonNull(connectionManager, "Connection can not be null")); this.exchangeName.set(requireNonNull(exchangeName, "Exchange name can not be null")); this.routingKey.set(requireNonNull(routingKey, "Routing key can not be null")); this.th2Pin = requireNonNull(th2Pin, "TH2 pin can not be null"); this.th2Type = requireNonNull(th2Type, "TH2 type can not be null"); + this.bookName = requireNonNull(bookName, "Book name can not be null"); } @Deprecated diff --git a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitSubscriber.java b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitSubscriber.java index bf2723b4e..a2f45266c 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitSubscriber.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitSubscriber.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2023 Exactpro (Exactpro Systems Limited) * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -16,15 +16,14 @@ package com.exactpro.th2.common.schema.message.impl.rabbitmq; import com.exactpro.th2.common.metrics.HealthMetrics; -import com.exactpro.th2.common.schema.message.ConfirmationMessageListener; -import com.exactpro.th2.common.schema.message.FilterFunction; +import com.exactpro.th2.common.schema.message.ConfirmationListener; +import com.exactpro.th2.common.schema.message.DeliveryMetadata; import com.exactpro.th2.common.schema.message.ManualAckDeliveryCallback.Confirmation; import com.exactpro.th2.common.schema.message.MessageSubscriber; import com.exactpro.th2.common.schema.message.SubscriberMonitor; -import com.exactpro.th2.common.schema.message.configuration.RouterFilter; -import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.SubscribeTarget; import com.exactpro.th2.common.schema.message.impl.rabbitmq.connection.ConnectionManager; -import com.google.protobuf.Message; +import com.google.common.base.Suppliers; +import com.google.common.io.BaseEncoding; import com.rabbitmq.client.Delivery; import io.prometheus.client.Counter; import io.prometheus.client.Histogram; @@ -35,11 +34,10 @@ import org.slf4j.LoggerFactory; import java.io.IOException; -import java.util.List; import java.util.Objects; -import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; import static com.exactpro.th2.common.metrics.CommonMetrics.DEFAULT_BUCKETS; import static com.exactpro.th2.common.metrics.CommonMetrics.QUEUE_LABEL; @@ -47,9 +45,12 @@ import static com.exactpro.th2.common.metrics.CommonMetrics.TH2_TYPE_LABEL; import static java.util.Objects.requireNonNull; -public abstract class AbstractRabbitSubscriber implements MessageSubscriber { +public abstract class AbstractRabbitSubscriber implements MessageSubscriber { private static final Logger LOGGER = LoggerFactory.getLogger(AbstractRabbitSubscriber.class); + @SuppressWarnings("rawtypes") + private static final Supplier EMPTY_INITIALIZER = Suppliers.memoize(() -> null); + private static final Counter MESSAGE_SIZE_SUBSCRIBE_BYTES = Counter.build() .name("th2_rabbitmq_message_size_subscribe_bytes") .labelNames(TH2_PIN_LABEL, TH2_TYPE_LABEL, QUEUE_LABEL) @@ -65,137 +66,44 @@ public abstract class AbstractRabbitSubscriber implements MessageSubscriber> listeners = new CopyOnWriteArrayList<>(); + private final boolean manualConfirmation; + private final ConfirmationListener listener; private final String queue; - private final AtomicReference connectionManager = new AtomicReference<>(); - private final AtomicReference consumerMonitor = new AtomicReference<>(); - private final AtomicReference filterFunc = new AtomicReference<>(); + private final ConnectionManager connectionManager; + private final AtomicReference> consumerMonitor = new AtomicReference<>(emptySupplier()); + private final AtomicBoolean isAlive = new AtomicBoolean(true); private final String th2Type; - private final AtomicBoolean hasManualSubscriber = new AtomicBoolean(); - private final HealthMetrics healthMetrics = new HealthMetrics(this); + protected final String th2Pin; + public AbstractRabbitSubscriber( @NotNull ConnectionManager connectionManager, @NotNull String queue, - @NotNull FilterFunction filterFunc, @NotNull String th2Pin, - @NotNull String th2Type + @NotNull String th2Type, + @NotNull ConfirmationListener listener ) { - this.connectionManager.set(requireNonNull(connectionManager, "Connection can not be null")); + this.connectionManager = requireNonNull(connectionManager, "Connection can not be null"); this.queue = requireNonNull(queue, "Queue can not be null"); - this.filterFunc.set(requireNonNull(filterFunc, "Filter function can not be null")); - this.th2Pin = requireNonNull(th2Pin, "TH2 pin can not be null"); - this.th2Type = requireNonNull(th2Type, "TH2 type can not be null"); - } - - @Deprecated - @Override - public void init(@NotNull ConnectionManager connectionManager, @NotNull String exchangeName, @NotNull SubscribeTarget subscribeTargets) { - throw new UnsupportedOperationException("Method is deprecated, please use constructor"); - } - - @Deprecated - @Override - public void init(@NotNull ConnectionManager connectionManager, @NotNull SubscribeTarget subscribeTarget, @NotNull FilterFunction filterFunc) { - throw new UnsupportedOperationException("Method is deprecated, please use constructor"); - } - - @Override - public void start() throws Exception { - ConnectionManager connectionManager = this.connectionManager.get(); - if (connectionManager == null) { - throw new IllegalStateException("Subscriber is not initialized"); - } - - try { - consumerMonitor.updateAndGet(monitor -> { - if (monitor == null) { - try { - monitor = connectionManager.basicConsume( - queue, - (consumeTag, delivery, confirmation) -> { - Timer processTimer = MESSAGE_PROCESS_DURATION_SECONDS - .labels(th2Pin, th2Type, queue) - .startTimer(); - MESSAGE_SIZE_SUBSCRIBE_BYTES - .labels(th2Pin, th2Type, queue) - .inc(delivery.getBody().length); - try { - T value; - try { - value = valueFromBytes(delivery.getBody()); - } catch (Exception e) { - LOGGER.error("Couldn't parse delivery. Confirm message received", e); - confirmation.confirm(); - throw new IOException( - String.format( - "Can not extract value from bytes for envelope '%s', queue '%s', pin '%s'", - delivery.getEnvelope(), queue, th2Pin - ), - e - ); - } - handle(consumeTag, delivery, value, confirmation); - } finally { - processTimer.observeDuration(); - } - }, - this::canceled - ); - LOGGER.info("Start listening queue name='{}'", queue); - } catch (IOException e) { - throw new IllegalStateException("Can not start subscribe to queue = " + queue, e); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - LOGGER.error("Interrupted exception while consuming from queue '{}'", queue); - throw new IllegalStateException("Thread was interrupted while consuming", e); - } - } - - return monitor; - }); - } catch (Exception e) { - throw new IllegalStateException("Can not start listening", e); - } + this.th2Pin = requireNonNull(th2Pin, "th2 pin can not be null"); + this.th2Type = requireNonNull(th2Type, "th2 type can not be null"); + this.listener = requireNonNull(listener, "Listener can not be null"); + this.manualConfirmation = ConfirmationListener.isManual(listener); + subscribe(); } @Override - public void addListener(ConfirmationMessageListener messageListener) { - if (ConfirmationMessageListener.isManual(messageListener)) { - if (!hasManualSubscriber.compareAndSet(false, true)) { - throw new IllegalStateException("cannot subscribe listener " + messageListener - + " because only one listener with manual confirmation is allowed per queue"); - } + public void close() throws IOException { + if (!isAlive.getAndSet(false)) { + LOGGER.warn("Subscriber for '{}' pin is already closed", th2Pin); + return; } - listeners.add(messageListener); - } - @Override - public void close() throws Exception { - ConnectionManager connectionManager = this.connectionManager.get(); - if (connectionManager == null) { - throw new IllegalStateException("Subscriber is not initialized"); - } - - SubscriberMonitor monitor = consumerMonitor.getAndSet(null); - if (monitor != null) { - monitor.unsubscribe(); - } + SubscriberMonitor monitor = consumerMonitor.getAndSet(emptySupplier()).get(); + monitor.unsubscribe(); - listeners.forEach(ConfirmationMessageListener::onClose); - listeners.clear(); - } - - protected boolean callFilterFunction(Message message, List filters) { - FilterFunction filterFunction = this.filterFunc.get(); - if (filterFunction == null) { - throw new IllegalStateException("Subscriber is not initialized"); - } - - return filterFunction.apply(message, filters); + listener.onClose(); } protected abstract T valueFromBytes(byte[] body) throws Exception; @@ -207,7 +115,7 @@ protected boolean callFilterFunction(Message message, List "Received value from " + routingKey + " is null"); @@ -226,34 +134,39 @@ protected void handle(String consumeTag, Delivery delivery, T value, Confirmatio return; } - boolean hasManualConfirmation = false; - for (ConfirmationMessageListener listener : listeners) { - try { - listener.handle(consumeTag, filteredValue, confirmation); - if (!hasManualConfirmation) { - hasManualConfirmation = ConfirmationMessageListener.isManual(listener); - } - } catch (Exception listenerExc) { - LOGGER.warn("Message listener from class '{}' threw exception", listener.getClass(), listenerExc); - } + try { + listener.handle(deliveryMetadata, filteredValue, confirmation); + } catch (Exception listenerExc) { + LOGGER.warn("Message listener from class '{}' threw exception", listener.getClass(), listenerExc); } - if (!hasManualConfirmation) { + if (!manualConfirmation) { confirmation.confirm(); } } catch (Exception e) { - LOGGER.error("Can not parse value from delivery for: {}", consumeTag, e); - try { - confirmation.confirm(); - } catch (IOException ex) { - LOGGER.error("Cannot confirm delivery for {}", consumeTag, ex); - } + LOGGER.error("Can not parse value from delivery for: {}. Reject message received", deliveryMetadata, e); + confirmation.reject(); + } + } + + private void subscribe() { + try { + consumerMonitor.updateAndGet(previous -> previous == EMPTY_INITIALIZER + ? Suppliers.memoize(this::basicConsume) + : previous) + .get(); // initialize subscribtion + } catch (Exception e) { + throw new IllegalStateException("Can not start listening", e); } } private void resubscribe() { LOGGER.info("Try to resubscribe subscriber for queue name='{}'", queue); + if (!isAlive.get()) { + LOGGER.warn("Subscriber for '{}' pin is already closed", th2Pin); + return; + } - SubscriberMonitor monitor = consumerMonitor.getAndSet(null); + SubscriberMonitor monitor = consumerMonitor.getAndSet(emptySupplier()).get(); if (monitor != null) { try { monitor.unsubscribe(); @@ -263,16 +176,64 @@ private void resubscribe() { } try { - start(); + subscribe(); } catch (Exception e) { LOGGER.error("Can not resubscribe subscriber for queue name='{}'", queue); healthMetrics.disable(); } } + private SubscriberMonitor basicConsume() { + try { + LOGGER.info("Start listening queue name='{}', th2 pin='{}'", queue, th2Pin); + return connectionManager.basicConsume(queue, this::handle, this::canceled); + } catch (IOException e) { + throw new IllegalStateException("Can not subscribe to queue = " + queue, e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOGGER.error("Interrupted exception while consuming from queue '{}'", queue); + throw new IllegalStateException("Thread was interrupted while consuming", e); + } + } + + private void handle(DeliveryMetadata deliveryMetadata, + Delivery delivery, + Confirmation confirmProcessed) throws IOException { + try (Timer ignored = MESSAGE_PROCESS_DURATION_SECONDS + .labels(th2Pin, th2Type, queue) + .startTimer()) { + MESSAGE_SIZE_SUBSCRIBE_BYTES + .labels(th2Pin, th2Type, queue) + .inc(delivery.getBody().length); + + T value; + try { + value = valueFromBytes(delivery.getBody()); + } catch (Exception e) { + if (LOGGER.isErrorEnabled()) { + LOGGER.error("Couldn't parse delivery: {}. Reject message received", BaseEncoding.base16().encode(delivery.getBody()), e); + } + confirmProcessed.reject(); + throw new IOException( + String.format( + "Can not extract value from bytes for envelope '%s', queue '%s', pin '%s'", + delivery.getEnvelope(), queue, th2Pin + ), + e + ); + } + handle(deliveryMetadata, delivery, value, confirmProcessed); + } + } + private void canceled(String consumerTag) { LOGGER.warn("Consuming cancelled for: '{}'", consumerTag); healthMetrics.getReadinessMonitor().disable(); resubscribe(); } -} + + @SuppressWarnings("unchecked") + private static Supplier emptySupplier() { + return (Supplier) EMPTY_INITIALIZER; + } +} \ No newline at end of file diff --git a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java index 5721f3e99..4340260e8 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2023 Exactpro (Exactpro Systems Limited) * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -12,12 +12,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.exactpro.th2.common.schema.message.impl.rabbitmq.connection; import com.exactpro.th2.common.metrics.HealthMetrics; +import com.exactpro.th2.common.schema.message.DeliveryMetadata; +import com.exactpro.th2.common.schema.message.ExclusiveSubscriberMonitor; import com.exactpro.th2.common.schema.message.ManualAckDeliveryCallback; import com.exactpro.th2.common.schema.message.ManualAckDeliveryCallback.Confirmation; -import com.exactpro.th2.common.schema.message.SubscriberMonitor; import com.exactpro.th2.common.schema.message.impl.OnlyOnceConfirmation; import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.ConnectionManagerConfiguration; import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.RabbitMQConfiguration; @@ -71,7 +73,7 @@ import java.util.function.Supplier; public class ConnectionManager implements AutoCloseable { - + public static final String EMPTY_ROUTING_KEY = ""; private static final Logger LOGGER = LoggerFactory.getLogger(ConnectionManager.class); public final Connection connection; @@ -137,7 +139,7 @@ public ConnectionManager(@NotNull RabbitMQConfiguration rabbitMQConfiguration, @ factory.setPassword(password); } - if (connectionManagerConfiguration.getConnectionTimeout() > 0) { + if (connectionManagerConfiguration.getConnectionTimeout() > 0) { factory.setConnectionTimeout(connectionManagerConfiguration.getConnectionTimeout()); } @@ -182,7 +184,7 @@ public void handleTopologyRecoveryException(Connection conn, Channel ch, Topolog turnOffReadiness(exception); } - private void turnOffReadiness(Throwable exception){ + private void turnOffReadiness(Throwable exception) { metrics.getReadinessMonitor().disable(); LOGGER.debug("Set RabbitMQ readiness to false. RabbitMQ error", exception); } @@ -214,7 +216,8 @@ private void turnOffReadiness(Throwable exception){ factory.setSharedExecutor(sharedExecutor); try { - this.connection = factory.newConnection(); + connection = factory.newConnection(); + LOGGER.info("Created RabbitMQ connection {} [{}]", connection, connection.hashCode()); addShutdownListenerToConnection(this.connection); addBlockedListenersToConnection(this.connection); addRecoveryListenerToConnection(this.connection); @@ -251,8 +254,6 @@ private void addShutdownListenerToChannel(Channel channel, Boolean withRecovery) }); } - - private void recoverSubscriptionsOfChannel(Channel channel) { channelChecker.execute(() -> { try { @@ -354,11 +355,26 @@ public void close() { } public void basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body) throws InterruptedException { - ChannelHolder holder = getOrCreateChannelFor(PinId.forRoutingKey(routingKey)); + ChannelHolder holder = getOrCreateChannelFor(PinId.forRoutingKey(exchange, routingKey)); holder.retryingPublishWithLock(channel -> channel.basicPublish(exchange, routingKey, props, body), configuration); } - public SubscriberMonitor basicConsume(String queue, ManualAckDeliveryCallback deliverCallback, CancelCallback cancelCallback) throws IOException, InterruptedException { + public String queueDeclare() throws IOException { + ChannelHolder holder = new ChannelHolder(this::createChannel, this::waitForConnectionRecovery, configuration.getPrefetchCount()); + return holder.mapWithLock(channel -> { + String queue = channel.queueDeclare( + "", // queue name + false, // durable + true, // exclusive + false, // autoDelete + Collections.emptyMap()).getQueue(); + LOGGER.info("Declared exclusive '{}' queue", queue); + putChannelFor(PinId.forQueue(queue), holder); + return queue; + }); + } + + public ExclusiveSubscriberMonitor basicConsume(String queue, ManualAckDeliveryCallback deliverCallback, CancelCallback cancelCallback) throws IOException, InterruptedException { PinId pinId = PinId.forQueue(queue); ChannelHolder holder = getOrCreateChannelFor(pinId, new SubscriptionCallbacks(deliverCallback, cancelCallback)); String tag = holder.retryingConsumeWithLock(channel -> @@ -369,16 +385,38 @@ public SubscriberMonitor basicConsume(String queue, ManualAckDeliveryCallback de String routingKey = envelope.getRoutingKey(); LOGGER.trace("Received delivery {} from queue={} routing_key={}", deliveryTag, queue, routingKey); - Confirmation confirmation = OnlyOnceConfirmation.wrap("from " + routingKey + " to " + queue, () -> holder.withLock(ch -> { - try { - basicAck(ch, deliveryTag); - } catch (IOException | ShutdownSignalException e) { - LOGGER.warn("Error during basicAck of message with deliveryTag = {} inside channel #{}: {}", deliveryTag, ch.getChannelNumber(), e); - throw e; - } finally { - holder.release(() -> metrics.getReadinessMonitor().enable()); + Confirmation wrappedConfirmation = new Confirmation() { + @Override + public void reject() throws IOException { + holder.withLock(ch -> { + try { + basicReject(ch, deliveryTag); + } catch (IOException | ShutdownSignalException e) { + LOGGER.warn("Error during basicReject of message with deliveryTag = {} inside channel #{}: {}", deliveryTag, ch.getChannelNumber(), e); + throw e; + } finally { + holder.release(() -> metrics.getReadinessMonitor().enable()); + } + }); + } + + @Override + public void confirm() throws IOException { + holder.withLock(ch -> { + try { + basicAck(ch, deliveryTag); + } catch (IOException | ShutdownSignalException e) { + LOGGER.warn("Error during basicAck of message with deliveryTag = {} inside channel #{}: {}", deliveryTag, ch.getChannelNumber(), e); + throw e; + } finally { + holder.release(() -> metrics.getReadinessMonitor().enable()); + } + }); } - })); + }; + + Confirmation confirmation = OnlyOnceConfirmation.wrap("from " + routingKey + " to " + queue, wrappedConfirmation); + holder.withLock(() -> holder.acquireAndSubmitCheck(() -> channelChecker.schedule(() -> { @@ -392,13 +430,14 @@ public SubscriberMonitor basicConsume(String queue, ManualAckDeliveryCallback de return false; // to cast to Callable }, configuration.getConfirmationTimeout().toMillis(), TimeUnit.MILLISECONDS) )); - deliverCallback.handle(tagTmp, delivery, confirmation); + boolean redeliver = envelope.isRedeliver(); + deliverCallback.handle(new DeliveryMetadata(tagTmp, redeliver), delivery, confirmation); } catch (IOException | RuntimeException e) { LOGGER.error("Cannot handle delivery for tag {}: {}", tagTmp, e.getMessage(), e); } }, cancelCallback), configuration); - return new RabbitMqSubscriberMonitor(holder, tag, this::basicCancel); + return new RabbitMqSubscriberMonitor(holder, queue, tag, this::basicCancel); } boolean isReady() { @@ -413,6 +452,15 @@ private void basicCancel(Channel channel, String consumerTag) throws IOException channel.basicCancel(consumerTag); } + public String queueExclusiveDeclareAndBind(String exchange) throws IOException, TimeoutException { + try (Channel channel = createChannel()) { + String queue = channel.queueDeclare().getQueue(); + channel.queueBind(queue, exchange, EMPTY_ROUTING_KEY); + LOGGER.info("Declared the '{}' queue to listen to the '{}'", queue, exchange); + return queue; + } + } + private void shutdownExecutor(ExecutorService executor, int closeTimeout, String name) { executor.shutdown(); try { @@ -450,6 +498,13 @@ private ChannelHolder getOrCreateChannelFor(PinId pinId, SubscriptionCallbacks s }); } + private void putChannelFor(PinId pinId, ChannelHolder holder) { + ChannelHolder previous = channelsByPin.putIfAbsent(pinId, holder); + if (previous != null) { + throw new IllegalStateException("Channel holder for the '" + pinId + "' pinId has been already registered"); + } + } + private Channel createChannel() { return createChannelWithOptionalRecovery(false); } @@ -464,6 +519,7 @@ private Channel createChannelWithOptionalRecovery(Boolean withRecovery) { addShutdownListenerToChannel(channel, withRecovery); channel.addReturnListener(ret -> LOGGER.warn("Can not router message to exchange '{}', routing key '{}'. Reply code '{}' and text = {}", ret.getExchange(), ret.getRoutingKey(), ret.getReplyCode(), ret.getReplyText())); + LOGGER.info("Created new RabbitMQ channel {} via connection {}", channel.getChannelNumber(), connection.hashCode()); return channel; } catch (IOException e) { throw new IllegalStateException("Can not create channel", e); @@ -508,27 +564,39 @@ private boolean isConnectionRecovery(ShutdownNotifier notifier) { /** * @param channel pass channel witch used for basicConsume, because delivery tags are scoped per channel, * deliveries must be acknowledged on the same channel they were received on. - * @throws IOException */ private static void basicAck(Channel channel, long deliveryTag) throws IOException { channel.basicAck(deliveryTag, false); } - private class RabbitMqSubscriberMonitor implements SubscriberMonitor { + private static void basicReject(Channel channel, long deliveryTag) throws IOException { + channel.basicReject(deliveryTag, false); + } + + private class RabbitMqSubscriberMonitor implements ExclusiveSubscriberMonitor { private final ChannelHolder holder; + private final String queue; private final String tag; private final CancelAction action; - public RabbitMqSubscriberMonitor(ChannelHolder holder, String tag, + public RabbitMqSubscriberMonitor(ChannelHolder holder, + String queue, + String tag, CancelAction action) { this.holder = holder; + this.queue = queue; this.tag = tag; this.action = action; } @Override - public void unsubscribe() throws Exception { + public @NotNull String getQueue() { + return queue; + } + + @Override + public void unsubscribe() throws IOException { holder.withLock(false, channel -> { channelsByPin.values().remove(holder); action.execute(channel, tag); @@ -542,21 +610,23 @@ private interface CancelAction { } private static class PinId { + private final String exchange; private final String routingKey; private final String queue; - public static PinId forRoutingKey(String routingKey) { - return new PinId(routingKey, null); + public static PinId forRoutingKey(String exchange, String routingKey) { + return new PinId(exchange, routingKey, null); } public static PinId forQueue(String queue) { - return new PinId(null, queue); + return new PinId(null, null, queue); } - private PinId(String routingKey, String queue) { - if (routingKey == null && queue == null) { - throw new NullPointerException("Either routingKey or queue must be set"); + private PinId(String exchange, String routingKey, String queue) { + if ((exchange == null || routingKey == null) && queue == null) { + throw new NullPointerException("Either exchange and routingKey or queue must be set"); } + this.exchange = exchange; this.routingKey = routingKey; this.queue = queue; } @@ -569,17 +639,24 @@ public boolean equals(Object o) { PinId pinId = (PinId) o; - return new EqualsBuilder().append(routingKey, pinId.routingKey).append(queue, pinId.queue).isEquals(); + return new EqualsBuilder() + .append(exchange, pinId.exchange) + .append(routingKey, pinId.routingKey) + .append(queue, pinId.queue).isEquals(); } @Override public int hashCode() { - return new HashCodeBuilder(17, 37).append(routingKey).append(queue).toHashCode(); + return new HashCodeBuilder(17, 37) + .append(exchange) + .append(routingKey) + .append(queue).toHashCode(); } @Override public String toString() { return new ToStringBuilder(this, ToStringStyle.JSON_STYLE) + .append("exchange", exchange) .append("routingKey", routingKey) .append("queue", queue) .toString(); @@ -778,4 +855,4 @@ private interface ChannelMapper { private interface ChannelConsumer { void consume(Channel channel) throws IOException; } -} +} \ No newline at end of file diff --git a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/builders/GenericCollectionBuilder.java b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/builders/GenericCollectionBuilder.java new file mode 100644 index 000000000..04b3a82bb --- /dev/null +++ b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/builders/GenericCollectionBuilder.java @@ -0,0 +1,45 @@ +/* + * Copyright 2023 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.exactpro.th2.common.schema.message.impl.rabbitmq.transport.builders; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * This builder is created in order to support @AutoBuilder generation + * The generated builder for types that have generic requires {@code List build()} signature + * That is why there are two builders ({@link GenericCollectionBuilder} and {@link CollectionBuilder}) + * @param collection type + */ +public class GenericCollectionBuilder { + private final List elements = new ArrayList<>(); + + public GenericCollectionBuilder add(T el) { + elements.add(el); + return this; + } + + public GenericCollectionBuilder addAll(Collection els) { + elements.addAll(els); + return this; + } + + public List build() { + return elements; + } +} diff --git a/src/main/java/com/exactpro/th2/common/schema/util/Log4jConfigUtils.kt b/src/main/java/com/exactpro/th2/common/schema/util/Log4jConfigUtils.kt index d6e081c10..7562fa357 100644 --- a/src/main/java/com/exactpro/th2/common/schema/util/Log4jConfigUtils.kt +++ b/src/main/java/com/exactpro/th2/common/schema/util/Log4jConfigUtils.kt @@ -15,61 +15,43 @@ package com.exactpro.th2.common.schema.util -import org.apache.log4j.PropertyConfigurator -import org.apache.logging.log4j.LogManager -import org.apache.logging.log4j.core.LoggerContext -import org.slf4j.LoggerFactory import java.net.MalformedURLException import java.nio.file.Files import java.nio.file.Path +import org.apache.logging.log4j.core.LoggerContext +import org.slf4j.LoggerFactory class Log4jConfigUtils { - private val LOGGER = LoggerFactory.getLogger(Log4jConfigUtils::class.java) - fun configure( - pathList: List, - firstVersionFileName: String, - secondVersionFileName: String, + pathList: List, + fileName: String, ) { - val configPathList: Pair? = pathList.asSequence() - .flatMap { - listOf( - Pair(Path.of(it, firstVersionFileName), 1), - Pair(Path.of(it, secondVersionFileName), 2) - ) - } - .filter { Files.exists(it.first) } - .sortedByDescending { it.second } + pathList.asSequence() + .map { it.resolve(fileName) } + .filter(Files::exists) .firstOrNull() - - when (configPathList?.second) { - 1 -> configureFirstLog4j(configPathList.first) - 2 -> configureSecondLog4j(configPathList.first) - null -> { + ?.let { path -> + try { + LOGGER.info("Trying to apply logger config from {}. Expecting log4j syntax", path) + val loggerContext = LoggerContext.getContext(false) + loggerContext.configLocation = path.toUri() + loggerContext.reconfigure() + LOGGER.info("Logger configuration from {} file is applied", path) + } catch (e: MalformedURLException) { + LOGGER.error(e.message, e) + } + } + ?: run { LOGGER.info( - "Neither of {} paths contains config files {}, {}. Use default configuration", + "Neither of {} paths contains config file {}. Use default configuration", pathList, - firstVersionFileName, - secondVersionFileName + fileName ) } - } } - private fun configureFirstLog4j(path: Path) { - try { - PropertyConfigurator.configure(path.toUri().toURL()) - LOGGER.info("Logger configuration from {} file is applied", path) - } catch (e: MalformedURLException) { - e.printStackTrace() - } + companion object { + private val LOGGER = LoggerFactory.getLogger(Log4jConfigUtils::class.java) } - - private fun configureSecondLog4j(path: Path) { - val context = LogManager.getContext(false) as LoggerContext - context.configLocation = path.toUri() - LOGGER.info("Logger configuration from {} file is applied", path) - } - } \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/common/grpc/router/AbstractGrpcInterceptor.kt b/src/main/kotlin/com/exactpro/th2/common/grpc/router/AbstractGrpcInterceptor.kt index 167c065a5..95b08ac21 100644 --- a/src/main/kotlin/com/exactpro/th2/common/grpc/router/AbstractGrpcInterceptor.kt +++ b/src/main/kotlin/com/exactpro/th2/common/grpc/router/AbstractGrpcInterceptor.kt @@ -15,7 +15,7 @@ package com.exactpro.th2.common.grpc.router -import com.exactpro.th2.common.value.toValue +import com.google.protobuf.Message import io.grpc.MethodDescriptor import io.prometheus.client.Counter import mu.KotlinLogging @@ -39,8 +39,12 @@ abstract class AbstractGrpcInterceptor ( sizeBytesCounter: Counter.Child?, methodInvokeCounter: Counter.Child, ) { + require(this is Message) { + "Passed object of ${this::class.java} class is not implement the ${Message::class.java} interface" + } + val size: Int? = sizeBytesCounter?.let { - toValue().serializedSize.also { + serializedSize.also { sizeBytesCounter.inc(it.toDouble()) } } diff --git a/src/main/kotlin/com/exactpro/th2/common/message/MessageFilterUtils.kt b/src/main/kotlin/com/exactpro/th2/common/message/MessageFilterUtils.kt index 7aceeb6c3..a4bc7bc5f 100644 --- a/src/main/kotlin/com/exactpro/th2/common/message/MessageFilterUtils.kt +++ b/src/main/kotlin/com/exactpro/th2/common/message/MessageFilterUtils.kt @@ -40,26 +40,20 @@ import com.exactpro.th2.common.grpc.ValueFilter.KindCase.NULL_VALUE import com.exactpro.th2.common.grpc.ValueFilter.KindCase.SIMPLE_FILTER import com.exactpro.th2.common.value.emptyValueFilter import com.exactpro.th2.common.value.toValueFilter -import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonProperty +import java.util.Locale private val DEFAULT_TIME_PRECISION_REGEX = Regex("(\\d[HMS])(?!\$)") -@Deprecated( - message = "The message type from MessageFilter will be removed in the future", - replaceWith = ReplaceWith( - expression = "rootMessageFilter(messageType)" - ), - level = DeprecationLevel.WARNING -) -fun messageFilter(messageType: String): MessageFilter.Builder = MessageFilter.newBuilder().setMessageType(messageType) fun messageFilter(): MessageFilter.Builder = MessageFilter.newBuilder() -fun rootMessageFilter(messageType: String): RootMessageFilter.Builder = RootMessageFilter.newBuilder().setMessageType(messageType) +fun rootMessageFilter(messageType: String): RootMessageFilter.Builder = + RootMessageFilter.newBuilder().setMessageType(messageType) fun MessageFilter.getField(key: String): ValueFilter? = getFieldsOrDefault(key, null) fun MessageFilter.Builder.getField(key: String): ValueFilter? = getFieldsOrDefault(key, null) -fun MessageFilter.Builder.addField(key: String, value: Any?): MessageFilter.Builder = apply { putFields(key, value?.toValueFilter() ?: emptyValueFilter()) } +fun MessageFilter.Builder.addField(key: String, value: Any?): MessageFilter.Builder = + apply { putFields(key, value?.toValueFilter() ?: emptyValueFilter()) } /** * It accepts vararg with even size and splits it into pairs where the first value of a pair is used as a key while the second is used as a value @@ -70,20 +64,28 @@ fun MessageFilter.Builder.addFields(vararg fields: Any?): MessageFilter.Builder } } -fun MessageFilter.Builder.addFields(fields: Map?): MessageFilter.Builder = apply { fields?.forEach { addField(it.key, it.value) } } +fun MessageFilter.Builder.addFields(fields: Map?): MessageFilter.Builder = + apply { fields?.forEach { addField(it.key, it.value) } } -fun MessageFilter.Builder.copyField(message: MessageFilter.Builder, key: String): MessageFilter.Builder = apply { putFields(key, message.getField(key) ?: emptyValueFilter()) } -fun MessageFilter.Builder.copyField(message: MessageFilter.Builder, vararg key: String): MessageFilter.Builder = apply { key.forEach { putFields(it, message.getField(it) ?: emptyValueFilter()) } } -fun MessageFilter.Builder.copyField(message: MessageFilter, vararg key: String): MessageFilter.Builder = apply { key.forEach { putFields(it, message.getField(it) ?: emptyValueFilter()) } } +fun MessageFilter.Builder.copyField(message: MessageFilter.Builder, key: String): MessageFilter.Builder = + apply { putFields(key, message.getField(key) ?: emptyValueFilter()) } + +fun MessageFilter.Builder.copyField(message: MessageFilter.Builder, vararg key: String): MessageFilter.Builder = + apply { key.forEach { putFields(it, message.getField(it) ?: emptyValueFilter()) } } + +fun MessageFilter.Builder.copyField(message: MessageFilter, vararg key: String): MessageFilter.Builder = + apply { key.forEach { putFields(it, message.getField(it) ?: emptyValueFilter()) } } fun MessageFilter.copy(): MessageFilter.Builder = MessageFilter.newBuilder().putAllFields(fieldsMap) fun MessageFilter.Builder.copy(): MessageFilter.Builder = MessageFilter.newBuilder().putAllFields(fieldsMap) fun RootMessageFilter.toTreeTable(): TreeTable = TreeTableBuilder().apply { - row("message-type", RowBuilder() - .column(MessageTypeColumn(messageType)) - .build()) + row( + "message-type", RowBuilder() + .column(MessageTypeColumn(messageType)) + .build() + ) row("message-filter", messageFilter.toTreeTableEntry()) row("metadata-filter", metadataFilter.toTreeTableEntry()) row("comparison-settings", comparisonSettings.toTreeTableEntry()) @@ -126,23 +128,33 @@ private fun MetadataFilter.toTreeTableEntry(): TreeTableEntry = CollectionBuilde private fun RootComparisonSettings.toTreeTableEntry(): TreeTableEntry = CollectionBuilder().apply { row("ignore-fields", CollectionBuilder().apply { - ignoreFieldsList.forEachIndexed { index, nestedValue -> + ignoreFieldsList.forEachIndexed { index, nestedValue -> val nestedName = index.toString() - row(nestedName, RowBuilder() - .column(IgnoreFieldColumn(nestedValue)) - .build()) + row( + nestedName, RowBuilder() + .column(IgnoreFieldColumn(nestedValue)) + .build() + ) } }.build()) if (hasTimePrecision()) { val timePrecision = timePrecision.toJavaDuration().toString().substring(2) - row("time-precision", RowBuilder() - .column(IgnoreFieldColumn(DEFAULT_TIME_PRECISION_REGEX.replace(timePrecision, "$1 ").toLowerCase())) - .build()) + row( + "time-precision", RowBuilder() + .column( + IgnoreFieldColumn( + DEFAULT_TIME_PRECISION_REGEX.replace(timePrecision, "$1 ").lowercase(Locale.getDefault()) + ) + ) + .build() + ) } if (decimalPrecision.isNotBlank()) { - row("decimal-precision", RowBuilder() - .column(IgnoreFieldColumn(decimalPrecision)) - .build()) + row( + "decimal-precision", RowBuilder() + .column(IgnoreFieldColumn(decimalPrecision)) + .build() + ) } }.build() @@ -158,6 +170,7 @@ private fun SimpleFilter.toTreeTableEntry(): TreeTableEntry = when { filterValueCase == VALUE || operation == EMPTY || operation == NOT_EMPTY -> RowBuilder() .column(MessageFilterTableColumn(value, operation.toString(), key)) .build() + else -> error("Unsupported simple filter value: $filterValueCase") } @@ -168,15 +181,23 @@ private fun ValueFilter.toTreeTableEntry(): TreeTableEntry = when { kindCase == NULL_VALUE -> RowBuilder() .column(MessageFilterTableColumn(if (operation == EQUAL) "IS_NULL" else "IS_NOT_NULL", key)) .build() + kindCase == SIMPLE_FILTER || operation == EMPTY || operation == NOT_EMPTY -> RowBuilder() .column(MessageFilterTableColumn(simpleFilter, operation.toString(), key)) .build() + else -> error("Unsupported ValueFilter value: $kindCase") } private fun SimpleList.toTreeTableEntry(operation: FilterOperation, key: Boolean): TreeTableEntry { return RowBuilder() - .column(MessageFilterTableColumn(simpleValuesList.joinToString(prefix = "[", postfix = "]"), operation.toString(), key)) + .column( + MessageFilterTableColumn( + simpleValuesList.joinToString(prefix = "[", postfix = "]"), + operation.toString(), + key + ) + ) .build() } diff --git a/src/main/kotlin/com/exactpro/th2/common/message/MessageUtils.kt b/src/main/kotlin/com/exactpro/th2/common/message/MessageUtils.kt index 3ebb74611..ade4a6d75 100644 --- a/src/main/kotlin/com/exactpro/th2/common/message/MessageUtils.kt +++ b/src/main/kotlin/com/exactpro/th2/common/message/MessageUtils.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2023 Exactpro (Exactpro Systems Limited) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,7 @@ */ @file:JvmName("MessageUtils") +@file:Suppress("unused") package com.exactpro.th2.common.message @@ -40,6 +41,7 @@ import com.exactpro.th2.common.grpc.Message import com.exactpro.th2.common.grpc.MessageFilter import com.exactpro.th2.common.grpc.MessageGroup import com.exactpro.th2.common.grpc.MessageID +import com.exactpro.th2.common.grpc.MessageIDOrBuilder import com.exactpro.th2.common.grpc.MessageMetadata import com.exactpro.th2.common.grpc.MessageOrBuilder import com.exactpro.th2.common.grpc.MetadataFilter @@ -53,6 +55,7 @@ import com.exactpro.th2.common.grpc.Value.KindCase.NULL_VALUE import com.exactpro.th2.common.grpc.Value.KindCase.SIMPLE_VALUE import com.exactpro.th2.common.grpc.ValueFilter import com.exactpro.th2.common.grpc.ValueOrBuilder +import com.exactpro.th2.common.schema.message.impl.rabbitmq.BookName import com.exactpro.th2.common.value.getBigDecimal import com.exactpro.th2.common.value.getBigInteger import com.exactpro.th2.common.value.getDouble @@ -79,23 +82,25 @@ import java.time.LocalDateTime import java.time.ZoneOffset import java.util.Calendar import java.util.Date +import java.util.Locale import java.util.TimeZone typealias FieldValues = Map typealias FieldValueFilters = Map typealias JavaDuration = java.time.Duration -fun message() : Message.Builder = Message.newBuilder() -fun message(messageType: String): Message.Builder = Message.newBuilder().setMetadata(messageType) -fun message(messageType: String, direction: Direction, sessionAlias: String): Message.Builder = Message.newBuilder().setMetadata(messageType, direction, sessionAlias) +fun message(): Message.Builder = Message.newBuilder() +fun message(messageType: String): Message.Builder = Message.newBuilder().setMetadata(messageType = messageType) +fun message(bookName: String, messageType: String, direction: Direction, sessionAlias: String) = + Message.newBuilder().setMetadata(bookName, messageType, direction, sessionAlias) operator fun Message.get(key: String): Value? = getField(key) fun Message.getField(fieldName: String): Value? = getFieldsOrDefault(fieldName, null) operator fun Message.Builder.get(key: String): Value? = getField(key) fun Message.Builder.getField(fieldName: String): Value? = getFieldsOrDefault(fieldName, null) -fun Message.hasField(key: String) : Boolean = fieldsMap.containsKey(key) -fun Message.Builder.hasField(key: String) : Boolean = fieldsMap.containsKey(key); +fun Message.hasField(key: String): Boolean = fieldsMap.containsKey(key) +fun Message.Builder.hasField(key: String): Boolean = fieldsMap.containsKey(key) fun Message.getString(fieldName: String): String? = getField(fieldName)?.getString() fun Message.getInt(fieldName: String): Int? = getField(fieldName)?.getInt() @@ -119,20 +124,47 @@ fun Message.Builder.getList(fieldName: String): List? = getField(fieldNam operator fun Message.Builder.set(key: String, value: Any?): Message.Builder = apply { addField(key, value) } -fun Message.Builder.updateField(key: String, updateFunc: Value.Builder.() -> ValueOrBuilder?): Message.Builder = apply { set(key, updateFunc(getField(key)?.toBuilder() ?: throw NullPointerException("Can not find field with name $key"))) } -fun Message.Builder.updateList(key: String, updateFunc: ListValue.Builder.() -> ListValueOrBuilder) : Message.Builder = apply { updateField(key) { updateList(updateFunc) } } -fun Message.Builder.updateMessage(key: String, updateFunc: Message.Builder.() -> MessageOrBuilder) : Message.Builder = apply { updateField(key) { updateMessage(updateFunc) } } -fun Message.Builder.updateString(key: String, updateFunc: String.() -> String) : Message.Builder = apply { updateField(key) { updateString(updateFunc) } } +fun Message.Builder.updateField(key: String, updateFunc: Value.Builder.() -> ValueOrBuilder?): Message.Builder = apply { + set( + key, + updateFunc(getField(key)?.toBuilder() ?: throw NullPointerException("Can not find field with name $key")) + ) +} + +fun Message.Builder.updateList(key: String, updateFunc: ListValue.Builder.() -> ListValueOrBuilder): Message.Builder = + apply { updateField(key) { updateList(updateFunc) } } + +fun Message.Builder.updateMessage(key: String, updateFunc: Message.Builder.() -> MessageOrBuilder): Message.Builder = + apply { updateField(key) { updateMessage(updateFunc) } } + +fun Message.Builder.updateString(key: String, updateFunc: String.() -> String): Message.Builder = + apply { updateField(key) { updateString(updateFunc) } } + +fun Message.Builder.updateOrAddField(key: String, updateFunc: (Value.Builder?) -> ValueOrBuilder?): Message.Builder = + apply { set(key, updateFunc(getField(key)?.toBuilder())) } + +fun Message.Builder.updateOrAddList( + key: String, + updateFunc: (ListValue.Builder?) -> ListValueOrBuilder +): Message.Builder = apply { updateOrAddField(key) { it?.updateOrAddList(updateFunc) ?: updateFunc(null).toValue() } } + +fun Message.Builder.updateOrAddMessage( + key: String, + updateFunc: (Message.Builder?) -> MessageOrBuilder +): Message.Builder = + apply { updateOrAddField(key) { it?.updateOrAddMessage(updateFunc) ?: updateFunc(null).toValue() } } + +fun Message.Builder.updateOrAddString(key: String, updateFunc: (String?) -> String): Message.Builder = + apply { updateOrAddField(key) { it?.updateOrAddString(updateFunc) ?: updateFunc(null).toValue() } } -fun Message.Builder.updateOrAddField(key: String, updateFunc: (Value.Builder?) -> ValueOrBuilder?): Message.Builder = apply { set(key, updateFunc(getField(key)?.toBuilder())) } -fun Message.Builder.updateOrAddList(key: String, updateFunc: (ListValue.Builder?) -> ListValueOrBuilder) : Message.Builder = apply { updateOrAddField(key) { it?.updateOrAddList(updateFunc) ?: updateFunc(null)?.toValue() } } -fun Message.Builder.updateOrAddMessage(key: String, updateFunc: (Message.Builder?) -> MessageOrBuilder) : Message.Builder = apply { updateOrAddField(key) { it?.updateOrAddMessage(updateFunc) ?: updateFunc(null)?.toValue() } } -fun Message.Builder.updateOrAddString(key: String, updateFunc:(String?) -> String) : Message.Builder = apply { updateOrAddField(key) { it?.updateOrAddString(updateFunc) ?: updateFunc(null)?.toValue() } } +fun Message.Builder.addField(key: String, value: Any?): Message.Builder = + apply { putFields(key, value?.toValue() ?: nullValue()) } -fun Message.Builder.addField(key: String, value: Any?): Message.Builder = apply { putFields(key, value?.toValue() ?: nullValue()) } +fun Message.Builder.copyField(message: Message, key: String): Message.Builder = + apply { if (message.getField(key) != null) putFields(key, message.getField(key)) } -fun Message.Builder.copyField(message: Message, key: String) : Message.Builder = apply { if (message.getField(key) != null) putFields(key, message.getField(key)) } -fun Message.Builder.copyField(message: Message.Builder, key: String): Message.Builder = apply { if (message.getField(key) != null) putFields(key, message.getField(key)) } +fun Message.Builder.copyField(message: Message.Builder, key: String): Message.Builder = + apply { if (message.getField(key) != null) putFields(key, message.getField(key)) } /** @@ -144,23 +176,36 @@ fun Message.Builder.addFields(vararg fields: Any?): Message.Builder = apply { } } -fun Message.Builder.addFields(fields: Map?): Message.Builder = apply { fields?.forEach { addField(it.key, it.value?.toValue() ?: nullValue()) } } +fun Message.Builder.addFields(fields: Map?): Message.Builder = + apply { fields?.forEach { addField(it.key, it.value?.toValue() ?: nullValue()) } } -fun Message.Builder.copyFields(message: Message, vararg keys: String) : Message.Builder = apply { keys.forEach { copyField(message, it) } } -fun Message.Builder.copyFields(message: Message.Builder, vararg keys: String) : Message.Builder = apply { keys.forEach { copyField(message, it) } } +fun Message.Builder.copyFields(message: Message, vararg keys: String): Message.Builder = + apply { keys.forEach { copyField(message, it) } } -fun Message.copy(): Message.Builder = Message.newBuilder().setMetadata(metadata).putAllFields(fieldsMap).setParentEventId(parentEventId) +fun Message.Builder.copyFields(message: Message.Builder, vararg keys: String): Message.Builder = + apply { keys.forEach { copyField(message, it) } } -fun Message.Builder.copy(): Message.Builder = Message.newBuilder().setMetadata(metadata).putAllFields(fieldsMap).setParentEventId(parentEventId) +fun Message.copy(): Message.Builder = + Message.newBuilder().setMetadata(metadata).putAllFields(fieldsMap).setParentEventId(parentEventId) -fun Message.Builder.setMetadata(messageType: String? = null, direction: Direction? = null, sessionAlias: String? = null, sequence: Long? = null, timestamp: Instant? = null): Message.Builder = +fun Message.Builder.copy(): Message.Builder = + Message.newBuilder().setMetadata(metadata).putAllFields(fieldsMap).setParentEventId(parentEventId) + +fun Message.Builder.setMetadata( + bookName: String? = null, + messageType: String? = null, + direction: Direction? = null, + sessionAlias: String? = null, + sequence: Long? = null, + timestamp: Instant? = null +): Message.Builder = setMetadata(MessageMetadata.newBuilder().also { if (messageType != null) { it.messageType = messageType } - it.timestamp = (timestamp ?: Instant.now()).toTimestamp() if (direction != null || sessionAlias != null) { it.id = MessageID.newBuilder().apply { + this.timestamp = (timestamp ?: Instant.now()).toTimestamp() if (direction != null) { this.direction = direction } @@ -170,6 +215,9 @@ fun Message.Builder.setMetadata(messageType: String? = null, direction: Directio if (sequence != null) { this.sequence = sequence } + if (bookName != null) { + this.bookName = bookName + } }.build() } }) @@ -192,9 +240,9 @@ operator fun MessageGroup.Builder.plusAssign(rawMessage: RawMessage.Builder) { fun Instant.toTimestamp(): Timestamp = Timestamp.newBuilder().setSeconds(epochSecond).setNanos(nano).build() fun Date.toTimestamp(): Timestamp = toInstant().toTimestamp() -fun LocalDateTime.toTimestamp(zone: ZoneOffset) : Timestamp = toInstant(zone).toTimestamp() -fun LocalDateTime.toTimestamp() : Timestamp = toTimestamp(ZoneOffset.of(TimeZone.getDefault().id)) -fun Calendar.toTimestamp() : Timestamp = toInstant().toTimestamp() +fun LocalDateTime.toTimestamp(zone: ZoneOffset): Timestamp = toInstant(zone).toTimestamp() +fun LocalDateTime.toTimestamp(): Timestamp = toTimestamp(ZoneOffset.of(TimeZone.getDefault().id)) +fun Calendar.toTimestamp(): Timestamp = toInstant().toTimestamp() fun Duration.toJavaDuration(): JavaDuration = JavaDuration.ofSeconds(seconds, nanos.toLong()) fun JavaDuration.toProtoDuration(): Duration = Duration.newBuilder().setSeconds(seconds).setNanos(nano).build() @@ -265,17 +313,18 @@ fun Value.toValueFilter(isKey: Boolean): ValueFilter = when (val source = this) fun FieldValues.toFieldValueFilters(keyFields: List = listOf()): FieldValueFilters = mapValues { (name, value) -> value.toValueFilter(name in keyFields) } -fun Message.toMessageFilter(keyFields: List = listOf(), failUnexpected: FailUnexpected = NO): MessageFilter = when (val source = this) { - Message.getDefaultInstance() -> MessageFilter.getDefaultInstance() - else -> MessageFilter.newBuilder().apply { - putAllFields(source.fieldsMap.toFieldValueFilters(keyFields)) - if (failUnexpected.number != 0) { - comparisonSettingsBuilder.apply { - this.failUnexpected = failUnexpected +fun Message.toMessageFilter(keyFields: List = listOf(), failUnexpected: FailUnexpected = NO): MessageFilter = + when (val source = this) { + Message.getDefaultInstance() -> MessageFilter.getDefaultInstance() + else -> MessageFilter.newBuilder().apply { + putAllFields(source.fieldsMap.toFieldValueFilters(keyFields)) + if (failUnexpected.number != 0) { + comparisonSettingsBuilder.apply { + this.failUnexpected = failUnexpected + } } - } - }.build() -} + }.build() + } fun ListValue.toListValueFilter(): ListValueFilter { return if (ListValue.getDefaultInstance() == this) { @@ -287,6 +336,22 @@ fun ListValue.toListValueFilter(): ListValueFilter { } } +val Message.bookName + get(): String = metadata.id.bookName +var Message.Builder.bookName + get(): String = metadata.id.bookName + set(value) { + metadataBuilder.idBuilder.bookName = value + } + +val RawMessage.bookName + get(): String = metadata.id.bookName +var RawMessage.Builder.bookName + get(): String = metadata.id.bookName + set(value) { + metadataBuilder.idBuilder.bookName = value + } + val Message.messageType get(): String = metadata.messageType var Message.Builder.messageType @@ -384,10 +449,14 @@ var RawMessage.Builder.subsequence } val Message.logId: String - get() = "$sessionAlias:${direction.toString().toLowerCase()}:$sequence${subsequence.joinToString("") { ".$it" }}" + get() = "$sessionAlias:${ + direction.toString().lowercase(Locale.getDefault()) + }:$sequence${subsequence.joinToString("") { ".$it" }}" val RawMessage.logId: String - get() = "$sessionAlias:${direction.toString().toLowerCase()}:$sequence${subsequence.joinToString("") { ".$it" }}" + get() = "$sessionAlias:${ + direction.toString().lowercase(Locale.getDefault()) + }:$sequence${subsequence.joinToString("") { ".$it" }}" val AnyMessage.logId: String get() = when (kindCase) { @@ -396,7 +465,8 @@ val AnyMessage.logId: String else -> error("Cannot get log id from $kindCase message: ${toJson()}") } -fun getSessionAliasAndDirection(messageID: MessageID): Array = arrayOf(messageID.connectionId.sessionAlias, messageID.direction.name) +fun getSessionAliasAndDirection(messageID: MessageID): Array = + arrayOf(messageID.connectionId.sessionAlias, messageID.direction.name) fun getSessionAliasAndDirection(anyMessage: AnyMessage): Array = when { anyMessage.hasMessage() -> getSessionAliasAndDirection(anyMessage.message.metadata.id) @@ -411,42 +481,73 @@ val AnyMessage.sequence: Long else -> error("Message ${shortDebugString(this)} doesn't have message or rawMessage") } +val AnyMessage.bookName: BookName + get() = when { + hasMessage() -> message.metadata.id.bookName + hasRawMessage() -> rawMessage.metadata.id.bookName + else -> error("Message ${shortDebugString(this)} doesn't have message or rawMessage") + } + fun getDebugString(className: String, ids: List): String { val sessionAliasAndDirection = getSessionAliasAndDirection(ids[0]) val sequences = ids.joinToString { it.sequence.toString() } - return "$className: session_alias = ${sessionAliasAndDirection[0]}, direction = ${sessionAliasAndDirection[1]}, sequnces = $sequences" + return "$className: session_alias = ${sessionAliasAndDirection[0]}, direction = ${sessionAliasAndDirection[1]}, sequences = $sequences" } @JvmOverloads -fun com.google.protobuf.MessageOrBuilder.toJson(short: Boolean = true): String = JsonFormat.printer().includingDefaultValueFields().let { - (if (short) it.omittingInsignificantWhitespace() else it).print(this) -} +fun com.google.protobuf.MessageOrBuilder.toJson(short: Boolean = true): String = + JsonFormat.printer().includingDefaultValueFields().let { + (if (short) it.omittingInsignificantWhitespace() else it).print(this) + } -fun T.fromJson(json: String) : T = apply { +fun T.fromJson(json: String): T = apply { JsonFormat.parser().ignoringUnknownFields().merge(json, this) } +@Deprecated( + "Moved to common-utils-j/com.exactpro.th2.common.utils.message.MessageUtils", + replaceWith = ReplaceWith("toTreeTable()", "com.exactpro.th2.common.utils.message.toTreeTable") +) fun Message.toTreeTable(): TreeTable = TreeTableBuilder().apply { for ((key, value) in fieldsMap) { row(key, value.toTreeTableEntry()) } }.build() +val MessageIDOrBuilder.isValid: Boolean + get() = bookName.isNotBlank() + && hasConnectionId() && connectionId.sessionAlias.isNotBlank() + && hasTimestamp() && (timestamp.seconds > 0 || timestamp.nanos > 0) + && sequence > 0 + +@Deprecated( + "Moved to common-utils-j/com.exactpro.th2.common.utils.message.MessageUtils", + replaceWith = ReplaceWith("toTreeTable()", "com.exactpro.th2.common.utils.message.toTreeTableEntry") +) private fun Value.toTreeTableEntry(): TreeTableEntry = when { hasMessageValue() -> CollectionBuilder().apply { for ((key, value) in messageValue.fieldsMap) { row(key, value.toTreeTableEntry()) } }.build() + hasListValue() -> CollectionBuilder().apply { listValue.valuesList.forEachIndexed { index, nestedValue -> val nestedName = index.toString() row(nestedName, nestedValue.toTreeTableEntry()) } }.build() + else -> RowBuilder() .column(MessageTableColumn(simpleValue)) .build() } -internal data class MessageTableColumn(val fieldValue: String) : IColumn +@Deprecated( + "Moved to common-utils-j/com.exactpro.th2.common.utils.message.MessageUtils", + replaceWith = ReplaceWith( + "MessageTableColumn(fieldValue)", + "com.exactpro.th2.common.utils.message.MessageTableColumn" + ) +) +data class MessageTableColumn(val fieldValue: String) : IColumn diff --git a/src/main/kotlin/com/exactpro/th2/common/metrics/CommonMetrics.kt b/src/main/kotlin/com/exactpro/th2/common/metrics/CommonMetrics.kt index 42dfe2231..9a51e13c0 100644 --- a/src/main/kotlin/com/exactpro/th2/common/metrics/CommonMetrics.kt +++ b/src/main/kotlin/com/exactpro/th2/common/metrics/CommonMetrics.kt @@ -32,7 +32,9 @@ const val TH2_TYPE_LABEL = "th2_type" const val EXCHANGE_LABEL = "exchange" const val QUEUE_LABEL = "queue" const val ROUTING_KEY_LABEL = "routing_key" +const val BOOK_NAME_LABEL = "book_name" const val SESSION_ALIAS_LABEL = "session_alias" +const val SESSION_GROUP_LABEL = "session_group" const val DIRECTION_LABEL = "direction" const val MESSAGE_TYPE_LABEL = "message_type" diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/configuration/ConfigurationManager.kt b/src/main/kotlin/com/exactpro/th2/common/schema/configuration/ConfigurationManager.kt index cebcad3db..f7eb10d60 100644 --- a/src/main/kotlin/com/exactpro/th2/common/schema/configuration/ConfigurationManager.kt +++ b/src/main/kotlin/com/exactpro/th2/common/schema/configuration/ConfigurationManager.kt @@ -26,6 +26,8 @@ import java.util.concurrent.ConcurrentHashMap class ConfigurationManager(private val configurationPath: Map, Path>) { private val configurations: MutableMap, Any?> = ConcurrentHashMap() + operator fun get(clazz: Class<*>): Path? = configurationPath[clazz] + fun loadConfiguration( objectMapper: ObjectMapper, stringSubstitutor: StringSubstitutor, @@ -48,6 +50,7 @@ class ConfigurationManager(private val configurationPath: Map, Path>) { } } + @Suppress("UNCHECKED_CAST") fun getConfigurationOrLoad( objectMapper: ObjectMapper, stringSubstitutor: StringSubstitutor, diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/cradle/CradleConfiguration.kt b/src/main/kotlin/com/exactpro/th2/common/schema/cradle/CradleConfiguration.kt index 1c5d1e495..586d91d05 100644 --- a/src/main/kotlin/com/exactpro/th2/common/schema/cradle/CradleConfiguration.kt +++ b/src/main/kotlin/com/exactpro/th2/common/schema/cradle/CradleConfiguration.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2023 Exactpro (Exactpro Systems Limited) * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -15,58 +15,31 @@ package com.exactpro.th2.common.schema.cradle +import com.exactpro.cradle.CradleStorage import com.exactpro.cradle.cassandra.CassandraStorageSettings import com.exactpro.th2.common.schema.configuration.Configuration +import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonProperty -@Deprecated(message = "Please use CradleConfidentialConfiguration and CradleNonConfidentialConfiguration") -data class CradleConfiguration( - var dataCenter: String, - var host: String, - var keyspace: String, - var port: Int, - var username: String?, - var password: String?, - var cradleInstanceName: String?, - var timeout: Long, - var pageSize: Int, - var cradleMaxEventBatchSize: Long, - var cradleMaxMessageBatchSize: Long, - var prepareStorage: Boolean -) : Configuration() { - constructor( - cradleConfidentialConfiguration: CradleConfidentialConfiguration, - cradleNonConfidentialConfiguration: CradleNonConfidentialConfiguration - ) : this( - cradleConfidentialConfiguration.dataCenter, - cradleConfidentialConfiguration.host, - cradleConfidentialConfiguration.keyspace, - cradleConfidentialConfiguration.port, - cradleConfidentialConfiguration.username, - cradleConfidentialConfiguration.password, - cradleConfidentialConfiguration.cradleInstanceName, - cradleNonConfidentialConfiguration.timeout, - cradleNonConfidentialConfiguration.pageSize, - cradleNonConfidentialConfiguration.cradleMaxEventBatchSize, - cradleNonConfidentialConfiguration.cradleMaxMessageBatchSize, - cradleNonConfidentialConfiguration.prepareStorage - ) -} - data class CradleConfidentialConfiguration( @JsonProperty(required = true) var dataCenter: String, @JsonProperty(required = true) var host: String, @JsonProperty(required = true) var keyspace: String, var port: Int = 0, var username: String? = null, - var password: String? = null, - var cradleInstanceName: String? = null + var password: String? = null ) : Configuration() +@JsonIgnoreProperties(ignoreUnknown = true) data class CradleNonConfidentialConfiguration( - var timeout: Long = CassandraStorageSettings.DEFAULT_TIMEOUT, + var prepareStorage: Boolean = false, + @Deprecated("Please use CassandraStorageSettings.resultPageSize") var pageSize: Int = 5000, - var cradleMaxEventBatchSize: Long = CassandraStorageSettings.DEFAULT_MAX_EVENT_BATCH_SIZE, - var cradleMaxMessageBatchSize: Long = CassandraStorageSettings.DEFAULT_MAX_MESSAGE_BATCH_SIZE, - var prepareStorage: Boolean = false + var cradleMaxEventBatchSize: Long = CradleStorage.DEFAULT_MAX_TEST_EVENT_BATCH_SIZE.toLong(), + @Deprecated("Please use CassandraStorageSettings.maxMessageBatchSize") + var cradleMaxMessageBatchSize: Long = CradleStorage.DEFAULT_MAX_MESSAGE_BATCH_SIZE.toLong(), + @Deprecated("Please use CassandraStorageSettings.counterPersistenceInterval") + var statisticsPersistenceIntervalMillis: Long = CassandraStorageSettings.DEFAULT_COUNTER_PERSISTENCE_INTERVAL_MS.toLong(), + @Deprecated("Please use CassandraStorageSettings.maxUncompressedTestEventSize") + var maxUncompressedEventBatchSize: Long = CassandraStorageSettings.DEFAULT_MAX_UNCOMPRESSED_TEST_EVENT_SIZE.toLong(), ) : Configuration() \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/factory/ExactproMetaInf.kt b/src/main/kotlin/com/exactpro/th2/common/schema/factory/ExactproMetaInf.kt new file mode 100644 index 000000000..0295e3a23 --- /dev/null +++ b/src/main/kotlin/com/exactpro/th2/common/schema/factory/ExactproMetaInf.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2023 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.exactpro.th2.common.schema.factory + +import mu.KotlinLogging +import java.io.IOException +import java.net.URL +import java.nio.file.Path +import java.util.Properties +import java.util.jar.Attributes +import java.util.jar.JarFile +import java.util.jar.Manifest + +internal class ExactproMetaInf( + url: URL, + private val title: String, + private val version: String +) { + private val jarPath = Path.of(url.path).parent.parent + + private var gitEnriched = false + private var gitHash = "" + private var gitBranch = "" + private var gitRemoteUrl = "" + private var gitClosestTag = "" + + fun enrich(gitProperty: URL) { + try { + gitProperty.openStream().use { inputStream -> + val properties = Properties() + properties.load(inputStream) + gitHash = properties.getProperty(GIT_HASH_PROPERTY) + gitBranch = properties.getProperty(GIT_BRANCH_PROPERTY) + gitRemoteUrl = properties.getProperty(GIT_REMOTE_URL_PROPERTY) + gitClosestTag = properties.getProperty(GIT_CLOSEST_TAG_PROPERTY) + gitEnriched = true + } + } catch (e: IOException) { + K_LOGGER.warn(e) { "Git properties '$gitProperty' loading failure" } + } + } + + override fun toString(): String { + return "Manifest title: $title, version: $version , git { ${ + if (gitEnriched) { + "hash: $gitHash, branch: $gitBranch, repository: $gitRemoteUrl, closest tag: $gitClosestTag" + } else { + "'${jarPath.fileName}' jar doesn't contain '$GIT_PROPERTIES_FILE' resource, please use '$GRADLE_GIT_PROPERTIES_PLUGIN' plugin" + } + } }" + } + + companion object { + private val K_LOGGER = KotlinLogging.logger {} + private const val EXACTPRO_IMPLEMENTATION_VENDOR = "Exactpro Systems LLC" + private const val GRADLE_GIT_PROPERTIES_PLUGIN = "com.gorylenko.gradle-git-properties" + private const val GIT_PROPERTIES_FILE = "git.properties" + private const val GIT_HASH_PROPERTY = "git.commit.id" + private const val GIT_BRANCH_PROPERTY = "git.branch" + private const val GIT_REMOTE_URL_PROPERTY = "git.remote.origin.url" + private const val GIT_CLOSEST_TAG_PROPERTY = "git.closest.tag.name" + + @JvmStatic + fun logging() { + if (K_LOGGER.isInfoEnabled) { + try { + val map = Thread.currentThread().contextClassLoader + .getResources(JarFile.MANIFEST_NAME).asSequence() + .mapNotNull(::create) + .map { metaInf -> metaInf.jarPath to metaInf } + .toMap() + + Thread.currentThread().contextClassLoader + .getResources(GIT_PROPERTIES_FILE).asSequence() + .forEach { url -> map[Path.of(url.path).parent]?.enrich(url) } + + map.values.forEach { metaInf -> K_LOGGER.info { "$metaInf" } } + } catch (e: IOException) { + K_LOGGER.warn(e) { "Manifest searching failure" } + } + } + } + + private fun create(manifestUrl: URL): ExactproMetaInf? { + try { + manifestUrl.openStream().use { inputStream -> + val attributes = Manifest(inputStream).mainAttributes + return if (EXACTPRO_IMPLEMENTATION_VENDOR != attributes.getValue(Attributes.Name.IMPLEMENTATION_VENDOR)) { + null + } else ExactproMetaInf( + manifestUrl, + attributes.getValue(Attributes.Name.IMPLEMENTATION_TITLE), + attributes.getValue(Attributes.Name.IMPLEMENTATION_VERSION) + ) + } + } catch (e: IOException) { + K_LOGGER.warn(e) { "Manifest '$manifestUrl' loading failure" } + return null + } + } + } +} diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/factory/FactorySettings.kt b/src/main/kotlin/com/exactpro/th2/common/schema/factory/FactorySettings.kt index 1712e4f07..47c0c6566 100644 --- a/src/main/kotlin/com/exactpro/th2/common/schema/factory/FactorySettings.kt +++ b/src/main/kotlin/com/exactpro/th2/common/schema/factory/FactorySettings.kt @@ -23,18 +23,20 @@ import com.exactpro.th2.common.schema.event.EventBatchRouter import com.exactpro.th2.common.schema.grpc.router.GrpcRouter import com.exactpro.th2.common.schema.grpc.router.impl.DefaultGrpcRouter import com.exactpro.th2.common.schema.message.MessageRouter +import com.exactpro.th2.common.schema.message.NotificationRouter import com.exactpro.th2.common.schema.message.impl.rabbitmq.group.RabbitMessageGroupBatchRouter +import com.exactpro.th2.common.schema.message.impl.rabbitmq.notification.NotificationEventBatchRouter import com.exactpro.th2.common.schema.message.impl.rabbitmq.parsed.RabbitParsedBatchRouter import com.exactpro.th2.common.schema.message.impl.rabbitmq.raw.RabbitRawBatchRouter import java.nio.file.Path - data class FactorySettings @JvmOverloads constructor( var messageRouterParsedBatchClass: Class> = RabbitParsedBatchRouter::class.java, var messageRouterRawBatchClass: Class> = RabbitRawBatchRouter::class.java, var messageRouterMessageGroupBatchClass: Class> = RabbitMessageGroupBatchRouter::class.java, var eventBatchRouterClass: Class> = EventBatchRouter::class.java, var grpcRouterClass: Class = DefaultGrpcRouter::class.java, + var notificationEventBatchRouterClass: Class> = NotificationEventBatchRouter::class.java, var rabbitMQ: Path? = null, var routerMQ: Path? = null, var connectionManagerSettings: Path? = null, @@ -47,11 +49,110 @@ data class FactorySettings @JvmOverloads constructor( var custom: Path? = null, @Deprecated("Will be removed in future releases") var dictionaryTypesDir: Path? = null, var dictionaryAliasesDir: Path? = null, - @Deprecated("Will be removed in future releases") var oldDictionariesDir: Path? = null) { - private val _variables: MutableMap = HashMap() - val variables: Map = _variables + @Deprecated("Will be removed in future releases") var oldDictionariesDir: Path? = null, + var variables: MutableMap = HashMap() +) { + fun messageRouterParsedBatchClass(messageRouterParsedBatchClass: Class>): FactorySettings { + this.messageRouterParsedBatchClass = messageRouterParsedBatchClass + return this + } + + fun messageRouterRawBatchClass(messageRouterRawBatchClass: Class>): FactorySettings { + this.messageRouterRawBatchClass = messageRouterRawBatchClass + return this + } + + fun messageRouterMessageGroupBatchClass(messageRouterMessageGroupBatchClass: Class>): FactorySettings { + this.messageRouterMessageGroupBatchClass = messageRouterMessageGroupBatchClass + return this + } + + fun eventBatchRouterClass(eventBatchRouterClass: Class>): FactorySettings { + this.eventBatchRouterClass = eventBatchRouterClass + return this + } + + fun grpcRouterClass(grpcRouterClass: Class): FactorySettings { + this.grpcRouterClass = grpcRouterClass + return this + } + + fun notificationEventBatchRouterClass(notificationEventBatchRouterClass: Class>): FactorySettings { + this.notificationEventBatchRouterClass = notificationEventBatchRouterClass + return this + } + + fun rabbitMQ(rabbitMQ: Path): FactorySettings { + this.rabbitMQ = rabbitMQ + return this + } + + fun routerMQ(routerMQ: Path): FactorySettings { + this.routerMQ = routerMQ + return this + } + + fun connectionManagerSettings(connectionManagerSettings: Path): FactorySettings { + this.connectionManagerSettings = connectionManagerSettings + return this + } + + fun grpc(grpc: Path): FactorySettings { + this.grpc = grpc + return this + } + + fun routerGRPC(routerGRPC: Path): FactorySettings { + this.routerGRPC = routerGRPC + return this + } + + fun cradleConfidential(cradleConfidential: Path): FactorySettings { + this.cradleConfidential = cradleConfidential + return this + } + + fun cradleNonConfidential(cradleNonConfidential: Path): FactorySettings { + this.cradleNonConfidential = cradleNonConfidential + return this + } + + fun prometheus(prometheus: Path): FactorySettings { + this.prometheus = prometheus + return this + } + + fun boxConfiguration(boxConfiguration: Path): FactorySettings { + this.boxConfiguration = boxConfiguration + return this + } + + fun custom(custom: Path?): FactorySettings { + this.custom = custom + return this + } + + fun dictionaryTypesDir(dictionaryTypesDir: Path?): FactorySettings { + this.dictionaryTypesDir = dictionaryTypesDir + return this + } + + fun dictionaryAliasesDir(dictionaryAliasesDir: Path?): FactorySettings { + this.dictionaryAliasesDir = dictionaryAliasesDir + return this + } + + fun oldDictionariesDir(oldDictionariesDir: Path?): FactorySettings { + this.dictionaryTypesDir = oldDictionariesDir + return this + } + + fun variables(variables: MutableMap): FactorySettings { + this.variables = variables + return this + } fun putVariable(key: String, value: String): String? { - return _variables.put(key, value) + return variables.put(key, value) } } \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/filter/strategy/impl/AnyMessageFilterStrategy.kt b/src/main/kotlin/com/exactpro/th2/common/schema/filter/strategy/impl/AnyMessageFilterStrategy.kt index d39f229d1..44abeac2f 100644 --- a/src/main/kotlin/com/exactpro/th2/common/schema/filter/strategy/impl/AnyMessageFilterStrategy.kt +++ b/src/main/kotlin/com/exactpro/th2/common/schema/filter/strategy/impl/AnyMessageFilterStrategy.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021-2021 Exactpro (Exactpro Systems Limited) + * Copyright 2021-2023 Exactpro (Exactpro Systems Limited) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,26 +20,38 @@ import com.exactpro.th2.common.grpc.AnyMessage import com.exactpro.th2.common.message.toJson import com.google.protobuf.Message -class AnyMessageFilterStrategy : AbstractFilterStrategy() { +object AnyMessageFilterStrategy : AbstractFilterStrategy() { override fun getFields(message: Message): MutableMap { check(message is AnyMessage) { "Message is not an ${AnyMessage::class.qualifiedName}: ${message.toJson()}" } - val result = HashMap(); + val result = HashMap() when { message.hasMessage() -> { result.putAll(message.message.fieldsMap.mapValues { it.value.simpleValue }) val metadata = message.message.metadata - result[AbstractTh2MsgFilterStrategy.SESSION_ALIAS_KEY] = metadata.id.connectionId.sessionAlias + val sessionAlias = metadata.id.connectionId.sessionAlias + val sessionGroup = metadata.id.connectionId.sessionGroup + result.putAll(metadata.propertiesMap) + result[AbstractTh2MsgFilterStrategy.BOOK_KEY] = metadata.id.bookName + result[AbstractTh2MsgFilterStrategy.SESSION_GROUP_KEY] = sessionGroup.ifEmpty { sessionAlias } + result[AbstractTh2MsgFilterStrategy.SESSION_ALIAS_KEY] = sessionAlias result[AbstractTh2MsgFilterStrategy.MESSAGE_TYPE_KEY] = metadata.messageType result[AbstractTh2MsgFilterStrategy.DIRECTION_KEY] = metadata.id.direction.name + result[AbstractTh2MsgFilterStrategy.PROTOCOL_KEY] = metadata.protocol } message.hasRawMessage() -> { val metadata = message.rawMessage.metadata + val sessionAlias = metadata.id.connectionId.sessionAlias + val sessionGroup = metadata.id.connectionId.sessionGroup + result.putAll(metadata.propertiesMap) + result[AbstractTh2MsgFilterStrategy.BOOK_KEY] = metadata.id.bookName + result[AbstractTh2MsgFilterStrategy.SESSION_GROUP_KEY] = sessionGroup.ifEmpty { sessionAlias } result[AbstractTh2MsgFilterStrategy.SESSION_ALIAS_KEY] = metadata.id.connectionId.sessionAlias result[AbstractTh2MsgFilterStrategy.DIRECTION_KEY] = metadata.id.direction.name + result[AbstractTh2MsgFilterStrategy.PROTOCOL_KEY] = metadata.protocol } else -> throw IllegalStateException("Message has not messages: ${message.toJson()}") } diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/grpc/configuration/GrpcConfiguration.kt b/src/main/kotlin/com/exactpro/th2/common/schema/grpc/configuration/GrpcConfiguration.kt index 5886faf31..958edc756 100644 --- a/src/main/kotlin/com/exactpro/th2/common/schema/grpc/configuration/GrpcConfiguration.kt +++ b/src/main/kotlin/com/exactpro/th2/common/schema/grpc/configuration/GrpcConfiguration.kt @@ -20,10 +20,12 @@ import com.exactpro.th2.common.schema.message.configuration.FieldFilterConfigura import com.exactpro.th2.common.schema.strategy.route.RoutingStrategy import com.exactpro.th2.service.RetryPolicy import com.fasterxml.jackson.annotation.JsonProperty +import io.grpc.internal.GrpcUtil data class GrpcConfiguration( @JsonProperty var services: Map = emptyMap(), @JsonProperty(value = "server") var serverConfiguration: GrpcServerConfiguration = GrpcServerConfiguration(), + @JsonProperty var maxMessageSize: Int = GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE, @JsonProperty var retryConfiguration: GrpcRetryConfiguration = GrpcRetryConfiguration(), ) : Configuration() @@ -41,7 +43,7 @@ data class Filter( data class GrpcEndpointConfiguration( @JsonProperty(required = true) var host: String, @JsonProperty(required = true) var port: Int = 8080, - var attributes: List = emptyList() + var attributes: List = emptyList(), ) : Configuration() data class GrpcRetryConfiguration( diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/ConfirmationMessageListener.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/ConfirmationMessageListener.kt index 71e41fae7..50a078adb 100644 --- a/src/main/kotlin/com/exactpro/th2/common/schema/message/ConfirmationMessageListener.kt +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/ConfirmationMessageListener.kt @@ -16,42 +16,47 @@ package com.exactpro.th2.common.schema.message -interface ConfirmationMessageListener { - +interface ConfirmationListener { @Throws(Exception::class) - fun handle(consumerTag: String, message: T, confirmation: ManualAckDeliveryCallback.Confirmation) + fun handle(deliveryMetadata: DeliveryMetadata, message: T, confirmation: ManualAckDeliveryCallback.Confirmation) fun onClose() {} companion object { @JvmStatic - fun wrap(listener: MessageListener): ConfirmationMessageListener = DelegateListener(listener) + fun wrap(listener: MessageListener): ConfirmationListener = AutoConfirmationListener(listener) /** * @return `true` if the listener uses manual acknowledgment */ @JvmStatic - fun isManual(listener: ConfirmationMessageListener<*>): Boolean = listener is ManualConfirmationListener<*> + fun isManual(listener: ConfirmationListener<*>): Boolean = listener is ManualConfirmationListener<*> } } /** * The interface marker that indicates that acknowledge will be manually invoked by the listener itself */ -interface ManualConfirmationListener : ConfirmationMessageListener { +fun interface ManualConfirmationListener : ConfirmationListener { + /** * The listener must invoke the [confirmation] callback once it has processed the [message] - * @see ConfirmationMessageListener.handle + * @see ConfirmationListener.handle */ - override fun handle(consumerTag: String, message: T, confirmation: ManualAckDeliveryCallback.Confirmation) + @Throws(Exception::class) + override fun handle(deliveryMetadata: DeliveryMetadata, message: T, confirmation: ManualAckDeliveryCallback.Confirmation) } -private class DelegateListener( +private class AutoConfirmationListener( private val delegate: MessageListener, -) : ConfirmationMessageListener { +) : ConfirmationListener { - override fun handle(consumerTag: String, message: T, confirmation: ManualAckDeliveryCallback.Confirmation) { - delegate.handler(consumerTag, message) + override fun handle( + deliveryMetadata: DeliveryMetadata, + message: T, + confirmation: ManualAckDeliveryCallback.Confirmation + ) { + delegate.handle(deliveryMetadata, message) } override fun onClose() { @@ -59,4 +64,4 @@ private class DelegateListener( } override fun toString(): String = "Delegate($delegate)" -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/DeliveryMetadata.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/DeliveryMetadata.kt new file mode 100644 index 000000000..2cefaa325 --- /dev/null +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/DeliveryMetadata.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2022-2023 Exactpro (Exactpro Systems Limited) + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.exactpro.th2.common.schema.message + +data class DeliveryMetadata( + val consumerTag: String, + val isRedelivered: Boolean = false +) diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/ExclusiveSubscriberMonitor.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/ExclusiveSubscriberMonitor.kt new file mode 100644 index 000000000..2350bc91a --- /dev/null +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/ExclusiveSubscriberMonitor.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2022 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.exactpro.th2.common.schema.message + +interface ExclusiveSubscriberMonitor : SubscriberMonitor { + val queue: String +} \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/ManualAckDeliveryCallback.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/ManualAckDeliveryCallback.kt index 81d1d01bc..f9d6530b7 100644 --- a/src/main/kotlin/com/exactpro/th2/common/schema/message/ManualAckDeliveryCallback.kt +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/ManualAckDeliveryCallback.kt @@ -20,17 +20,20 @@ import com.rabbitmq.client.Delivery import java.io.IOException fun interface ManualAckDeliveryCallback { + /** * Called when a delivery from queue is received - * @param consumerTag the _consumer_ tag associated with the consumer + * @param deliveryMetadata contains the _consumer_ tag associated with the consumer and _isRedelivered_ flag. * @param delivery the delivered message * @param confirmProcessed the action that should be invoked when the message can be considered as processed */ @Throws(IOException::class) - fun handle(consumerTag: String, delivery: Delivery, confirmProcessed: Confirmation) + fun handle(deliveryMetadata: DeliveryMetadata, delivery: Delivery, confirmProcessed: Confirmation) - fun interface Confirmation { + interface Confirmation { @Throws(IOException::class) fun confirm() + @Throws(IOException::class) + fun reject() } } \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/MessageRouterContext.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/MessageRouterContext.kt index 915cc3f6f..d62f6da0d 100644 --- a/src/main/kotlin/com/exactpro/th2/common/schema/message/MessageRouterContext.kt +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/MessageRouterContext.kt @@ -15,13 +15,13 @@ package com.exactpro.th2.common.schema.message +import com.exactpro.th2.common.schema.box.configuration.BoxConfiguration import com.exactpro.th2.common.schema.message.configuration.MessageRouterConfiguration import com.exactpro.th2.common.schema.message.impl.rabbitmq.connection.ConnectionManager interface MessageRouterContext { - val connectionManager: ConnectionManager val routerMonitor: MessageRouterMonitor val configuration: MessageRouterConfiguration - + val boxConfiguration: BoxConfiguration } \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/MessageRouterUtils.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/MessageRouterUtils.kt index 7afd77a54..677218cf4 100644 --- a/src/main/kotlin/com/exactpro/th2/common/schema/message/MessageRouterUtils.kt +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/MessageRouterUtils.kt @@ -24,7 +24,9 @@ import com.exactpro.th2.common.event.Event.Status.PASSED import com.exactpro.th2.common.event.EventUtils import com.exactpro.th2.common.grpc.AnyMessage import com.exactpro.th2.common.grpc.EventBatch +import com.exactpro.th2.common.grpc.EventID import com.exactpro.th2.common.grpc.MessageGroupBatch +import com.exactpro.th2.common.grpc.MessageGroupBatchOrBuilder import com.exactpro.th2.common.message.logId import com.exactpro.th2.common.message.toJson import com.exactpro.th2.common.schema.message.QueueAttribute.EVENT @@ -34,14 +36,29 @@ import org.apache.commons.lang3.exception.ExceptionUtils fun MessageRouter.storeEvent( event: Event, - parentId: String? = null + parentId: EventID +) = storeEvent(event, event.toProto(parentId)) + +@JvmOverloads +fun MessageRouter.storeEvent( + event: Event, + bookName: String, + scope: String? = null +) = storeEvent(event, event.toProto(bookName, scope)) + +private fun MessageRouter.storeEvent( + event: Event, + protoEvent: com.exactpro.th2.common.grpc.Event ): Event = event.apply { - val batch = EventBatch.newBuilder().addEvents(toProtoEvent(parentId)).build() - sendAll(batch, PUBLISH.toString(), EVENT.toString()) + sendAll( + EventBatch.newBuilder().addEvents(protoEvent).build(), + PUBLISH.toString(), + EVENT.toString() + ) } fun MessageRouter.storeEvent( - parentId: String, + parentId: EventID, name: String, type: String, cause: Throwable? = null @@ -77,8 +94,13 @@ fun appendAttributes( } fun MessageGroupBatch.toShortDebugString(): String = buildString { - append("MessageGroupBatch(ids = ") + append("MessageGroupBatch ") + if (hasMetadata()) { + append("external user queue = ${metadata.externalQueue} ") + } + + append("(ids = ") groupsList.asSequence() .flatMap { it.messagesList.asSequence() } .map(AnyMessage::logId) @@ -86,4 +108,10 @@ fun MessageGroupBatch.toShortDebugString(): String = buildString { .apply { append(this) } append(')') +} + +fun MessageGroupBatchOrBuilder.toBuilderWithMetadata(): MessageGroupBatch.Builder = MessageGroupBatch.newBuilder().apply { + if (this@toBuilderWithMetadata.hasMetadata()) { + metadata = this@toBuilderWithMetadata.metadata + } } \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/configuration/MessageRouterConfiguration.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/configuration/MessageRouterConfiguration.kt index ed63c3fc2..fabed6f93 100644 --- a/src/main/kotlin/com/exactpro/th2/common/schema/message/configuration/MessageRouterConfiguration.kt +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/configuration/MessageRouterConfiguration.kt @@ -25,7 +25,10 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize import org.apache.commons.collections4.MultiMapUtils import org.apache.commons.collections4.MultiValuedMap -data class MessageRouterConfiguration(var queues: Map = emptyMap()) : Configuration() { +data class MessageRouterConfiguration( + var queues: Map = emptyMap(), + var globalNotification: GlobalNotificationConfiguration = GlobalNotificationConfiguration() +) : Configuration() { fun getQueueByAlias(queueAlias: String): QueueConfiguration? { return queues[queueAlias] @@ -79,8 +82,8 @@ data class FieldFilterConfigurationOld( ) : Configuration() data class FieldFilterConfiguration( - @JsonProperty(value = "fieldName", required = true) var fieldName: String, - @JsonProperty("expectedValue") @JsonAlias("value") var expectedValue: String?, + @JsonProperty(value = "fieldName", required = true) @JsonAlias("fieldName", "field-name") var fieldName: String, + @JsonProperty("expectedValue") @JsonAlias("value", "expected-value") var expectedValue: String?, @JsonProperty(required = true) var operation: FieldFilterOperation ) : Configuration() @@ -90,4 +93,8 @@ enum class FieldFilterOperation { EMPTY, NOT_EMPTY, WILDCARD -} \ No newline at end of file +} + +data class GlobalNotificationConfiguration( + @JsonProperty var exchange: String = "global-notification" +) : Configuration() \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/OnlyOnceConfirmation.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/OnlyOnceConfirmation.kt index 7603a39e7..c76daf479 100644 --- a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/OnlyOnceConfirmation.kt +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/OnlyOnceConfirmation.kt @@ -17,8 +17,8 @@ package com.exactpro.th2.common.schema.message.impl import com.exactpro.th2.common.schema.message.ManualAckDeliveryCallback -import mu.KotlinLogging import java.util.concurrent.atomic.AtomicBoolean +import mu.KotlinLogging class OnlyOnceConfirmation private constructor( private val id: String, @@ -30,7 +30,15 @@ class OnlyOnceConfirmation private constructor( if (called.compareAndSet(false, true)) { delegate.confirm() } else { - LOGGER.warn { "Confirmation '$id' invoked more that one time" } + LOGGER.warn { "Confirmation or rejection '$id' invoked more that one time" } + } + } + + override fun reject() { + if (called.compareAndSet(false, true)) { + delegate.reject() + } else { + LOGGER.warn { "Confirmation or rejection '$id' invoked more that one time" } } } diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/context/DefaultMessageRouterContext.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/context/DefaultMessageRouterContext.kt index 88915861d..4e19a1f4a 100644 --- a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/context/DefaultMessageRouterContext.kt +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/context/DefaultMessageRouterContext.kt @@ -15,6 +15,7 @@ package com.exactpro.th2.common.schema.message.impl.context +import com.exactpro.th2.common.schema.box.configuration.BoxConfiguration import com.exactpro.th2.common.schema.message.MessageRouterContext import com.exactpro.th2.common.schema.message.MessageRouterMonitor import com.exactpro.th2.common.schema.message.configuration.MessageRouterConfiguration @@ -23,5 +24,6 @@ import com.exactpro.th2.common.schema.message.impl.rabbitmq.connection.Connectio class DefaultMessageRouterContext( override val connectionManager: ConnectionManager, override val routerMonitor: MessageRouterMonitor, - override val configuration: MessageRouterConfiguration -) : MessageRouterContext {} \ No newline at end of file + override val configuration: MessageRouterConfiguration, + override val boxConfiguration: BoxConfiguration +) : MessageRouterContext \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/monitor/EventMessageRouterMonitor.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/monitor/EventMessageRouterMonitor.kt index 39991176b..0c90c3087 100644 --- a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/monitor/EventMessageRouterMonitor.kt +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/monitor/EventMessageRouterMonitor.kt @@ -18,11 +18,15 @@ package com.exactpro.th2.common.schema.message.impl.monitor import com.exactpro.th2.common.event.Event import com.exactpro.th2.common.event.bean.Message import com.exactpro.th2.common.grpc.EventBatch +import com.exactpro.th2.common.grpc.EventID import com.exactpro.th2.common.schema.message.MessageRouter import com.exactpro.th2.common.schema.message.MessageRouterMonitor import org.slf4j.helpers.MessageFormatter.arrayFormat -class EventMessageRouterMonitor(private val router: MessageRouter, private val parentEventID: String?) : +class EventMessageRouterMonitor( + private val router: MessageRouter, + private val parentEventId: EventID +) : MessageRouterMonitor { override fun onInfo(msg: String, vararg args: Any?) { @@ -37,17 +41,15 @@ class EventMessageRouterMonitor(private val router: MessageRouter, p router.send(createEventBatch("Error message in message router", arrayFormat(msg, args).message, Event.Status.FAILED)) } - private fun createEventBatch(name: String, msg: String, status: Event.Status): EventBatch = - EventBatch.newBuilder().apply { - addEvents( + private fun createEventBatch(name: String, msg: String, status: Event.Status) = + EventBatch.newBuilder().also { + it.addEvents( Event.start() .name(name) .bodyData(Message().apply { data = msg; type = "message" }) .status(status) .type("event") - .toProtoEvent(parentEventID) + .toProto(parentEventId) ) }.build() - - } \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractGroupBatchAdapterRouter.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractGroupBatchAdapterRouter.kt index f2a98ddcd..8cf77ab5d 100644 --- a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractGroupBatchAdapterRouter.kt +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractGroupBatchAdapterRouter.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021-2021 Exactpro (Exactpro Systems Limited) + * Copyright 2021-2022 Exactpro (Exactpro Systems Limited) * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -16,9 +16,12 @@ package com.exactpro.th2.common.schema.message.impl.rabbitmq import com.exactpro.th2.common.grpc.MessageGroupBatch +import com.exactpro.th2.common.schema.message.DeliveryMetadata +import com.exactpro.th2.common.schema.message.ManualConfirmationListener import com.exactpro.th2.common.schema.message.MessageListener import com.exactpro.th2.common.schema.message.MessageRouter import com.exactpro.th2.common.schema.message.MessageRouterContext +import com.exactpro.th2.common.schema.message.ExclusiveSubscriberMonitor import com.exactpro.th2.common.schema.message.SubscriberMonitor import com.exactpro.th2.common.schema.message.appendAttributes @@ -39,24 +42,64 @@ abstract class AbstractGroupBatchAdapterRouter : MessageRouter { this.groupBatchRouter = groupBatchRouter } - override fun subscribe(callback: MessageListener, vararg attributes: String): SubscriberMonitor? { - return groupBatchRouter.subscribe( - MessageListener { consumerTag: String, message: MessageGroupBatch -> - callback.handler(consumerTag, buildFromGroupBatch(message)) + override fun subscribeExclusive(callback: MessageListener): ExclusiveSubscriberMonitor { + return groupBatchRouter.subscribeExclusive { deliveryMetadata: DeliveryMetadata, message: MessageGroupBatch -> + callback.handle(deliveryMetadata, buildFromGroupBatch(message)) + } + } + + override fun subscribe(callback: MessageListener, vararg attributes: String): SubscriberMonitor { + return groupBatchRouter.subscribe({ deliveryMetadata: DeliveryMetadata, message: MessageGroupBatch -> + callback.handle(deliveryMetadata, buildFromGroupBatch(message)) }, *appendAttributes(*attributes) { getRequiredSubscribeAttributes() }.toTypedArray() ) } - override fun subscribeAll(callback: MessageListener, vararg attributes: String): SubscriberMonitor? { - return groupBatchRouter.subscribeAll( - MessageListener { consumerTag: String, message: MessageGroupBatch -> - callback.handler(consumerTag, buildFromGroupBatch(message)) + override fun subscribeAll(callback: MessageListener, vararg attributes: String): SubscriberMonitor { + return groupBatchRouter.subscribeAll({ deliveryMetadata: DeliveryMetadata, message: MessageGroupBatch -> + callback.handle(deliveryMetadata, buildFromGroupBatch(message)) }, *appendAttributes(*attributes) { getRequiredSubscribeAttributes() }.toTypedArray() ) } + override fun subscribeWithManualAck( + callback: ManualConfirmationListener, + vararg queueAttr: String + ): SubscriberMonitor { + val listener = + ManualConfirmationListener { deliveryMetadata, message, confirmation -> + callback.handle(deliveryMetadata, buildFromGroupBatch(message), confirmation) + } + + return groupBatchRouter.subscribeWithManualAck( + listener, + *appendAttributes(*queueAttr) { getRequiredSubscribeAttributes() }.toTypedArray() + ) + } + + override fun subscribeAllWithManualAck( + callback: ManualConfirmationListener, + vararg queueAttr: String + ): SubscriberMonitor { + val listener = + ManualConfirmationListener { deliveryMetadata, message, confirmation -> + callback.handle(deliveryMetadata, buildFromGroupBatch(message), confirmation) + } + + return groupBatchRouter.subscribeAllWithManualAck(listener, + *appendAttributes(*queueAttr) { getRequiredSubscribeAttributes() }.toTypedArray() + ) + } + + override fun sendExclusive(queue: String, messageBatch: T) { + groupBatchRouter.sendExclusive( + queue, + buildGroupBatch(messageBatch) + ) + } + override fun send(messageBatch: T, vararg attributes: String) { groupBatchRouter.send( buildGroupBatch(messageBatch), diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitRouter.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitRouter.kt index 4072d4ca5..911624556 100644 --- a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitRouter.kt +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitRouter.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021-2021 Exactpro (Exactpro Systems Limited) + * Copyright 2021-2022 Exactpro (Exactpro Systems Limited) * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -14,16 +14,14 @@ */ package com.exactpro.th2.common.schema.message.impl.rabbitmq +import com.exactpro.th2.common.schema.box.configuration.BoxConfiguration import com.exactpro.th2.common.schema.exception.RouterException -import com.exactpro.th2.common.schema.filter.strategy.FilterStrategy import com.exactpro.th2.common.schema.message.* import com.exactpro.th2.common.schema.message.QueueAttribute.PUBLISH import com.exactpro.th2.common.schema.message.QueueAttribute.SUBSCRIBE import com.exactpro.th2.common.schema.message.configuration.MessageRouterConfiguration import com.exactpro.th2.common.schema.message.configuration.QueueConfiguration -import com.exactpro.th2.common.schema.message.configuration.RouterFilter import com.exactpro.th2.common.schema.message.impl.rabbitmq.connection.ConnectionManager -import com.google.protobuf.Message import mu.KotlinLogging import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicReference @@ -32,6 +30,7 @@ typealias PinName = String typealias PinConfiguration = QueueConfiguration typealias Queue = String typealias RoutingKey = String +typealias BookName = String abstract class AbstractRabbitRouter : MessageRouter { private val _context = AtomicReference() @@ -47,21 +46,21 @@ abstract class AbstractRabbitRouter : MessageRouter { protected val connectionManager: ConnectionManager get() = context.connectionManager - private val subscribers = ConcurrentHashMap>() - private val senders = ConcurrentHashMap>() + private val boxConfiguration: BoxConfiguration + get() = context.boxConfiguration - private val filterStrategy = AtomicReference(getDefaultFilterStrategy()) + private val subscribers = ConcurrentHashMap() + private val senders = ConcurrentHashMap>() - protected open fun getDefaultFilterStrategy(): FilterStrategy { - return FilterStrategy.DEFAULT_FILTER_STRATEGY + override fun init(context: MessageRouterContext) { + this.context = context } - protected open fun filterMessage(msg: Message, filters: List): Boolean { - return filterStrategy.get().verify(msg, filters) - } + override fun sendExclusive(queue: String, message: T) { + val pinConfig = PinConfiguration(queue, "", "", isReadable = false, isWritable = true) - override fun init(context: MessageRouterContext) { - this.context = context + senders.getSender(queue, pinConfig) + .send(message) } override fun send(message: T, vararg attributes: String) { @@ -82,15 +81,42 @@ abstract class AbstractRabbitRouter : MessageRouter { } } + override fun subscribeExclusive(callback: MessageListener): ExclusiveSubscriberMonitor { + val queue = connectionManager.queueDeclare() + val pinConfig = PinConfiguration("", queue, "", isReadable = true, isWritable = false) + + val listener = ConfirmationListener.wrap(callback) + subscribers.registerSubscriber(queue, pinConfig, listener) + + return object: ExclusiveSubscriberMonitor { + override val queue: String = queue + + override fun unsubscribe() { + subscribers.unregisterSubscriber(queue, pinConfig) + } + } + } + override fun subscribe(callback: MessageListener, vararg attributes: String): SubscriberMonitor { - return subscribeWithManualAck(ConfirmationMessageListener.wrap(callback), *attributes) + val pintAttributes: Set = appendAttributes(*attributes) { getRequiredSubscribeAttributes() } + return subscribe(pintAttributes = pintAttributes, ConfirmationListener.wrap(callback)) { + check(size == 1) { + "Found incorrect number of pins ${map(PinInfo::pinName)} to subscribe operation by attributes $pintAttributes and filters, expected 1, actual $size" + } + } } override fun subscribeAll(callback: MessageListener, vararg attributes: String): SubscriberMonitor { - return subscribeAllWithManualAck(ConfirmationMessageListener.wrap(callback), *attributes) + val pintAttributes: Set = appendAttributes(*attributes) { getRequiredSubscribeAttributes() } + val listener = ConfirmationListener.wrap(callback) + return subscribe(pintAttributes = pintAttributes, listener) { + check(isNotEmpty()) { + "Found incorrect number of pins ${map(PinInfo::pinName)} to subscribe all operation by attributes $pintAttributes and filters, expected 1 or more, actual $size" + } + } } - override fun subscribeWithManualAck(callback: ConfirmationMessageListener, vararg attributes: String): SubscriberMonitor { + override fun subscribeWithManualAck(callback: ManualConfirmationListener, vararg attributes: String): SubscriberMonitor { val pintAttributes: Set = appendAttributes(*attributes) { getRequiredSubscribeAttributes() } return subscribe(pintAttributes, callback) { check(size == 1) { @@ -99,7 +125,7 @@ abstract class AbstractRabbitRouter : MessageRouter { } } - override fun subscribeAllWithManualAck(callback: ConfirmationMessageListener, vararg attributes: String): SubscriberMonitor { + override fun subscribeAllWithManualAck(callback: ManualConfirmationListener, vararg attributes: String): SubscriberMonitor { val pintAttributes: Set = appendAttributes(*attributes) { getRequiredSubscribeAttributes() } return subscribe(pintAttributes, callback) { check(isNotEmpty()) { @@ -141,10 +167,14 @@ abstract class AbstractRabbitRouter : MessageRouter { protected open fun getRequiredSubscribeAttributes() = REQUIRED_SUBSCRIBE_ATTRIBUTES //TODO: implement common sender - protected abstract fun createSender(pinConfig: PinConfiguration, pinName: PinName): MessageSender + protected abstract fun createSender(pinConfig: PinConfiguration, pinName: PinName, bookName: BookName): MessageSender //TODO: implement common subscriber - protected abstract fun createSubscriber(pinConfig: PinConfiguration, pinName: PinName): MessageSubscriber + protected abstract fun createSubscriber( + pinConfig: PinConfiguration, + pinName: PinName, + listener: ConfirmationListener + ): MessageSubscriber protected abstract fun T.toErrorString(): String @@ -179,7 +209,7 @@ abstract class AbstractRabbitRouter : MessageRouter { private fun subscribe( pintAttributes: Set, - messageListener: ConfirmationMessageListener, + listener: ConfirmationListener, check: List.() -> Unit ): SubscriberMonitor { val packages: List = configuration.queues.asSequence() @@ -193,15 +223,12 @@ abstract class AbstractRabbitRouter : MessageRouter { val monitors: MutableList = mutableListOf() packages.forEach { (pinName: PinName, pinConfig: PinConfiguration) -> runCatching { - subscribers.getSubscriber(pinName, pinConfig).apply { - addListener(messageListener) - start() //TODO: replace to lazy start on add listener(s) - } + subscribers.registerSubscriber(pinName, pinConfig, listener) }.onFailure { e -> LOGGER.error(e) { "Listener can't be subscribed via the $pinName pin" } exceptions[pinName] = e }.onSuccess { - monitors.add(SubscriberMonitor { close() }) + monitors.add(SubscriberMonitor { subscribers.unregisterSubscriber(pinName, pinConfig) }) } } @@ -231,18 +258,36 @@ abstract class AbstractRabbitRouter : MessageRouter { "The $pinName isn't writable, configuration: $pinConfig" } - return@computeIfAbsent createSender(pinConfig, pinName) + return@computeIfAbsent createSender(pinConfig, pinName, boxConfiguration.bookName) } - private fun ConcurrentHashMap>.getSubscriber( + private fun ConcurrentHashMap.registerSubscriber( pinName: PinName, - pinConfig: PinConfiguration - ): MessageSubscriber = computeIfAbsent(pinConfig.queue) { - check(pinConfig.isReadable) { - "The $pinName isn't readable, configuration: $pinConfig" + pinConfig: PinConfiguration, + listener: ConfirmationListener + ) { + compute(pinConfig.queue) { _, previous -> + check(previous == null) { + "The '$pinName' pin already has subscriber, configuration: $pinConfig" + } + check(pinConfig.isReadable) { + "The $pinName isn't readable, configuration: $pinConfig" + } + + createSubscriber(pinConfig, pinName, listener).also { + LOGGER.info { "Created subscriber for '$pinName' pin, configuration: $pinConfig" } + } } + } - return@computeIfAbsent createSubscriber(pinConfig, pinName) + private fun ConcurrentHashMap.unregisterSubscriber( + pinName: PinName, + pinConfig: PinConfiguration, + ) { + remove(pinConfig.queue)?.let { subscriber -> + subscriber.close() + LOGGER.info { "Removed subscriber for '$pinName' pin, configuration: $pinConfig" } + } } private open class PinInfo( diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/custom/RabbitCustomRouter.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/custom/RabbitCustomRouter.kt index 5c6296239..323c88887 100644 --- a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/custom/RabbitCustomRouter.kt +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/custom/RabbitCustomRouter.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2023 Exactpro (Exactpro Systems Limited) * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -15,13 +15,14 @@ package com.exactpro.th2.common.schema.message.impl.rabbitmq.custom -import com.exactpro.th2.common.schema.message.FilterFunction +import com.exactpro.th2.common.schema.message.ConfirmationListener import com.exactpro.th2.common.schema.message.MessageSender import com.exactpro.th2.common.schema.message.MessageSubscriber import com.exactpro.th2.common.schema.message.QueueAttribute import com.exactpro.th2.common.schema.message.impl.rabbitmq.AbstractRabbitRouter import com.exactpro.th2.common.schema.message.impl.rabbitmq.AbstractRabbitSender import com.exactpro.th2.common.schema.message.impl.rabbitmq.AbstractRabbitSubscriber +import com.exactpro.th2.common.schema.message.impl.rabbitmq.BookName import com.exactpro.th2.common.schema.message.impl.rabbitmq.PinConfiguration import com.exactpro.th2.common.schema.message.impl.rabbitmq.PinName import com.exactpro.th2.common.schema.message.impl.rabbitmq.connection.ConnectionManager @@ -58,25 +59,30 @@ class RabbitCustomRouter( return message } - override fun createSender(pinConfig: PinConfiguration, pinName: PinName): MessageSender { + override fun createSender(pinConfig: PinConfiguration, pinName: PinName, bookName: BookName): MessageSender { return Sender( connectionManager, pinConfig.exchange, pinConfig.routingKey, pinName, + bookName, customTag, converter ) } - override fun createSubscriber(pinConfig: PinConfiguration, pinName: PinName): MessageSubscriber { + override fun createSubscriber( + pinConfig: PinConfiguration, + pinName: PinName, + listener: ConfirmationListener + ): MessageSubscriber { return Subscriber( connectionManager, pinConfig.queue, - FilterFunction.DEFAULT_FILTER_FUNCTION, pinName, customTag, - converter + converter, + listener ) } @@ -89,9 +95,10 @@ class RabbitCustomRouter( exchangeName: String, routingKey: String, th2Pin: String, + bookName: BookName, customTag: String, private val converter: MessageConverter - ) : AbstractRabbitSender(connectionManager, exchangeName, routingKey, th2Pin, customTag) { + ) : AbstractRabbitSender(connectionManager, exchangeName, routingKey, th2Pin, customTag, bookName) { override fun valueToBytes(value: T): ByteArray = converter.toByteArray(value) override fun toShortTraceString(value: T): String = converter.toTraceString(value) @@ -102,11 +109,11 @@ class RabbitCustomRouter( private class Subscriber( connectionManager: ConnectionManager, queue: String, - filterFunction: FilterFunction, th2Pin: String, customTag: String, - private val converter: MessageConverter - ) : AbstractRabbitSubscriber(connectionManager, queue, filterFunction, th2Pin, customTag) { + private val converter: MessageConverter, + messageListener: ConfirmationListener + ) : AbstractRabbitSubscriber(connectionManager, queue, th2Pin, customTag, messageListener) { override fun valueFromBytes(body: ByteArray): T = converter.fromByteArray(body) override fun toShortTraceString(value: T): String = converter.toTraceString(value) diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/group/RabbitMessageGroupBatchRouter.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/group/RabbitMessageGroupBatchRouter.kt index d1baf2502..d6cce606a 100644 --- a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/group/RabbitMessageGroupBatchRouter.kt +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/group/RabbitMessageGroupBatchRouter.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2023 Exactpro (Exactpro Systems Limited) * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -16,29 +16,24 @@ package com.exactpro.th2.common.schema.message.impl.rabbitmq.group import com.exactpro.th2.common.grpc.MessageGroupBatch -import com.exactpro.th2.common.metrics.DIRECTION_LABEL -import com.exactpro.th2.common.metrics.SESSION_ALIAS_LABEL -import com.exactpro.th2.common.metrics.TH2_PIN_LABEL -import com.exactpro.th2.common.metrics.MESSAGE_TYPE_LABEL -import com.exactpro.th2.common.metrics.incrementDroppedMetrics -import com.exactpro.th2.common.schema.filter.strategy.impl.AbstractFilterStrategy +import com.exactpro.th2.common.metrics.* import com.exactpro.th2.common.schema.filter.strategy.impl.AnyMessageFilterStrategy -import com.exactpro.th2.common.schema.message.FilterFunction +import com.exactpro.th2.common.schema.message.ConfirmationListener import com.exactpro.th2.common.schema.message.MessageSender import com.exactpro.th2.common.schema.message.MessageSubscriber import com.exactpro.th2.common.schema.message.configuration.QueueConfiguration import com.exactpro.th2.common.schema.message.configuration.RouterFilter import com.exactpro.th2.common.schema.message.impl.rabbitmq.AbstractRabbitRouter +import com.exactpro.th2.common.schema.message.impl.rabbitmq.BookName +import com.exactpro.th2.common.schema.message.impl.rabbitmq.PinConfiguration import com.exactpro.th2.common.schema.message.impl.rabbitmq.PinName +import com.exactpro.th2.common.schema.message.toBuilderWithMetadata import com.google.protobuf.Message import com.google.protobuf.TextFormat import io.prometheus.client.Counter import org.jetbrains.annotations.NotNull class RabbitMessageGroupBatchRouter : AbstractRabbitRouter() { - override fun getDefaultFilterStrategy(): AbstractFilterStrategy { - return AnyMessageFilterStrategy() - } override fun splitAndFilter( message: MessageGroupBatch, @@ -49,9 +44,9 @@ class RabbitMessageGroupBatchRouter : AbstractRabbitRouter() return message } - val builder = MessageGroupBatch.newBuilder() + val builder = message.toBuilderWithMetadata() message.groupsList.forEach { group -> - if (group.messagesList.all { filterMessage(it, pinConfiguration.filters) }) { + if (group.messagesList.all { AnyMessageFilterStrategy.verify(it, pinConfiguration.filters) }) { builder.addGroups(group) } else { incrementDroppedMetrics( @@ -66,26 +61,33 @@ class RabbitMessageGroupBatchRouter : AbstractRabbitRouter() return if (builder.groupsCount > 0) builder.build() else null } - override fun createSender(pinConfig: QueueConfiguration, pinName: PinName): MessageSender { + override fun createSender( + pinConfig: QueueConfiguration, + pinName: PinName, + bookName: BookName + ): MessageSender { return RabbitMessageGroupBatchSender( connectionManager, pinConfig.exchange, pinConfig.routingKey, - pinName + pinName, + bookName ) } override fun createSubscriber( - pinConfig: QueueConfiguration, - pinName: PinName - ): MessageSubscriber { + pinConfig: PinConfiguration, + pinName: PinName, + listener: ConfirmationListener + ): MessageSubscriber { return RabbitMessageGroupBatchSubscriber( connectionManager, pinConfig.queue, - FilterFunction { msg: Message, filters: List -> filterMessage(msg, filters) }, + { msg: Message, filters: List -> AnyMessageFilterStrategy.verify(msg, filters) }, pinName, pinConfig.filters, - connectionManager.configuration.messageRecursionLimit + connectionManager.configuration.messageRecursionLimit, + listener ) } diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/group/RabbitMessageGroupBatchSender.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/group/RabbitMessageGroupBatchSender.kt index 48019f833..8bd12019c 100644 --- a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/group/RabbitMessageGroupBatchSender.kt +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/group/RabbitMessageGroupBatchSender.kt @@ -16,7 +16,9 @@ package com.exactpro.th2.common.schema.message.impl.rabbitmq.group +import com.exactpro.th2.common.grpc.MessageGroup import com.exactpro.th2.common.grpc.MessageGroupBatch +import com.exactpro.th2.common.message.bookName import com.exactpro.th2.common.message.toJson import com.exactpro.th2.common.metrics.DIRECTION_LABEL import com.exactpro.th2.common.metrics.MESSAGE_TYPE_LABEL @@ -24,6 +26,7 @@ import com.exactpro.th2.common.metrics.SESSION_ALIAS_LABEL import com.exactpro.th2.common.metrics.TH2_PIN_LABEL import com.exactpro.th2.common.metrics.incrementTotalMetrics import com.exactpro.th2.common.schema.message.impl.rabbitmq.AbstractRabbitSender +import com.exactpro.th2.common.schema.message.impl.rabbitmq.BookName import com.exactpro.th2.common.schema.message.impl.rabbitmq.connection.ConnectionManager import com.exactpro.th2.common.schema.message.impl.rabbitmq.group.RabbitMessageGroupBatchRouter.Companion.MESSAGE_GROUP_TYPE import com.exactpro.th2.common.schema.message.toShortDebugString @@ -34,8 +37,16 @@ class RabbitMessageGroupBatchSender( connectionManager: ConnectionManager, exchangeName: String, routingKey: String, - th2Pin: String -) : AbstractRabbitSender(connectionManager, exchangeName, routingKey, th2Pin, MESSAGE_GROUP_TYPE) { + th2Pin: String, + bookName: BookName +) : AbstractRabbitSender( + connectionManager, + exchangeName, + routingKey, + th2Pin, + MESSAGE_GROUP_TYPE, + bookName +) { override fun send(value: MessageGroupBatch) { incrementTotalMetrics( value, @@ -44,7 +55,27 @@ class RabbitMessageGroupBatchSender( MESSAGE_GROUP_PUBLISH_TOTAL, MESSAGE_GROUP_SEQUENCE_PUBLISH ) - super.send(value) + if (value.groupsList.any { group -> group.messagesList.any { message -> message.bookName.isEmpty() } }) { + val batchBuilder = MessageGroupBatch.newBuilder() + value.groupsList.forEach { messageGroup -> + val groupBuilder = MessageGroup.newBuilder() + messageGroup.messagesList.forEach { message -> + val messageBuilder = message.toBuilder() + if (message.bookName.isEmpty()) { + if (messageBuilder.hasMessage()) { + messageBuilder.messageBuilder.metadataBuilder.idBuilder.bookName = bookName + } else if (messageBuilder.hasRawMessage()) { + messageBuilder.rawMessageBuilder.metadataBuilder.idBuilder.bookName = bookName + } + } + groupBuilder.addMessages(messageBuilder) + } + batchBuilder.addGroups(groupBuilder) + } + super.send(batchBuilder.build()) + } else { + super.send(value) + } } override fun valueToBytes(value: MessageGroupBatch): ByteArray = value.toByteArray() diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/group/RabbitMessageGroupBatchSubscriber.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/group/RabbitMessageGroupBatchSubscriber.kt index 1d1b9be5e..194233f01 100644 --- a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/group/RabbitMessageGroupBatchSubscriber.kt +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/group/RabbitMessageGroupBatchSubscriber.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2023 Exactpro (Exactpro Systems Limited) * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -23,14 +23,18 @@ import com.exactpro.th2.common.metrics.SESSION_ALIAS_LABEL import com.exactpro.th2.common.metrics.TH2_PIN_LABEL import com.exactpro.th2.common.metrics.incrementDroppedMetrics import com.exactpro.th2.common.metrics.incrementTotalMetrics +import com.exactpro.th2.common.schema.message.ConfirmationListener +import com.exactpro.th2.common.schema.message.DeliveryMetadata import com.exactpro.th2.common.schema.message.FilterFunction +import com.exactpro.th2.common.schema.message.ManualAckDeliveryCallback import com.exactpro.th2.common.schema.message.configuration.RouterFilter import com.exactpro.th2.common.schema.message.impl.rabbitmq.AbstractRabbitSubscriber import com.exactpro.th2.common.schema.message.impl.rabbitmq.connection.ConnectionManager -import com.exactpro.th2.common.schema.message.ManualAckDeliveryCallback import com.exactpro.th2.common.schema.message.impl.rabbitmq.group.RabbitMessageGroupBatchRouter.Companion.MESSAGE_GROUP_TYPE +import com.exactpro.th2.common.schema.message.toBuilderWithMetadata import com.exactpro.th2.common.schema.message.toShortDebugString import com.google.protobuf.CodedInputStream +import com.google.protobuf.Message import com.rabbitmq.client.Delivery import io.prometheus.client.Counter import io.prometheus.client.Gauge @@ -39,11 +43,12 @@ import mu.KotlinLogging class RabbitMessageGroupBatchSubscriber( connectionManager: ConnectionManager, queue: String, - filterFunction: FilterFunction, + private val filterFunction: FilterFunction, th2Pin: String, private val filters: List, - private val messageRecursionLimit: Int -) : AbstractRabbitSubscriber(connectionManager, queue, filterFunction, th2Pin, MESSAGE_GROUP_TYPE) { + private val messageRecursionLimit: Int, + messageListener: ConfirmationListener +) : AbstractRabbitSubscriber(connectionManager, queue, th2Pin, MESSAGE_GROUP_TYPE, messageListener) { private val logger = KotlinLogging.logger {} override fun valueFromBytes(body: ByteArray): MessageGroupBatch = parseEncodedBatch(body) @@ -75,11 +80,11 @@ class RabbitMessageGroupBatchSubscriber( } .toList() - return if (groups.isEmpty()) null else MessageGroupBatch.newBuilder().addAllGroups(groups).build() + return if (groups.isEmpty()) null else batch.toBuilderWithMetadata().addAllGroups(groups).build() } override fun handle( - consumeTag: String, + deliveryMetadata: DeliveryMetadata, delivery: Delivery, value: MessageGroupBatch, confirmation: ManualAckDeliveryCallback.Confirmation @@ -91,9 +96,11 @@ class RabbitMessageGroupBatchSubscriber( MESSAGE_GROUP_SUBSCRIBE_TOTAL, MESSAGE_GROUP_SEQUENCE_SUBSCRIBE ) - super.handle(consumeTag, delivery, value, confirmation) + super.handle(deliveryMetadata, delivery, value, confirmation) } + private fun callFilterFunction(message: Message, filters: List): Boolean = filterFunction.apply(message, filters) + private fun parseEncodedBatch(body: ByteArray?): MessageGroupBatch { val ins = CodedInputStream.newInstance(body) ins.setRecursionLimit(messageRecursionLimit) diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/notification/NotificationEventBatchRouter.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/notification/NotificationEventBatchRouter.kt new file mode 100644 index 000000000..55f34115c --- /dev/null +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/notification/NotificationEventBatchRouter.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2021-2022 Exactpro (Exactpro Systems Limited) + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.exactpro.th2.common.schema.message.impl.rabbitmq.notification + +import com.exactpro.th2.common.grpc.EventBatch +import com.exactpro.th2.common.schema.exception.RouterException +import com.exactpro.th2.common.schema.message.ConfirmationListener +import com.exactpro.th2.common.schema.message.MessageListener +import com.exactpro.th2.common.schema.message.MessageRouterContext +import com.exactpro.th2.common.schema.message.NotificationRouter +import com.exactpro.th2.common.schema.message.SubscriberMonitor +import mu.KotlinLogging + +const val NOTIFICATION_QUEUE_PREFIX = "global-notification-queue" + +class NotificationEventBatchRouter : NotificationRouter { + private lateinit var queue: String + private lateinit var sender: NotificationEventBatchSender + private lateinit var subscriber: NotificationEventBatchSubscriber + + override fun init(context: MessageRouterContext) { + sender = NotificationEventBatchSender( + context.connectionManager, + context.configuration.globalNotification.exchange + ) + queue = context.connectionManager.queueExclusiveDeclareAndBind( + context.configuration.globalNotification.exchange + ) + subscriber = NotificationEventBatchSubscriber(context.connectionManager, queue) + } + + override fun send(message: EventBatch) { + try { + sender.send(message) + } catch (e: Exception) { + val errorMessage = "Notification cannot be send through the queue $queue" + LOGGER.error(e) { errorMessage } + throw RouterException(errorMessage) + } + } + + override fun subscribe(callback: MessageListener): SubscriberMonitor { + try { + subscriber.addListener(ConfirmationListener.wrap(callback)) + subscriber.start() + } catch (e: Exception) { + val errorMessage = "Listener can't be subscribed via the queue $queue" + LOGGER.error(e) { errorMessage } + throw RouterException(errorMessage) + } + return SubscriberMonitor { } + } + + override fun close() { + subscriber.close() + } + + companion object { + private val LOGGER = KotlinLogging.logger {} + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/notification/NotificationEventBatchSender.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/notification/NotificationEventBatchSender.kt new file mode 100644 index 000000000..0b4cd41eb --- /dev/null +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/notification/NotificationEventBatchSender.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2021-2021 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.exactpro.th2.common.schema.message.impl.rabbitmq.notification + +import com.exactpro.th2.common.grpc.EventBatch +import com.exactpro.th2.common.schema.message.MessageSender +import com.exactpro.th2.common.schema.message.impl.rabbitmq.connection.ConnectionManager +import com.exactpro.th2.common.schema.message.impl.rabbitmq.connection.ConnectionManager.EMPTY_ROUTING_KEY +import java.io.IOException + +class NotificationEventBatchSender( + private val connectionManager: ConnectionManager, + private val exchange: String +) : MessageSender { + @Deprecated( + "Method is deprecated, please use constructor", + ReplaceWith("NotificationEventBatchSender()") + ) + override fun init(connectionManager: ConnectionManager, exchangeName: String, routingKey: String) { + throw UnsupportedOperationException("Method is deprecated, please use constructor") + } + + override fun send(message: EventBatch) { + try { + connectionManager.basicPublish(exchange, EMPTY_ROUTING_KEY, null, message.toByteArray()) + } catch (e: Exception) { + throw IOException( + "Can not send notification message: EventBatch: parent_event_id = ${message.parentEventId.id}", + e + ) + } + } +} diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/notification/NotificationEventBatchSubscriber.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/notification/NotificationEventBatchSubscriber.kt new file mode 100644 index 000000000..f8b66e930 --- /dev/null +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/notification/NotificationEventBatchSubscriber.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2021-2023 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.exactpro.th2.common.schema.message.impl.rabbitmq.notification + +import com.exactpro.th2.common.grpc.EventBatch +import com.exactpro.th2.common.schema.message.ConfirmationListener +import com.exactpro.th2.common.schema.message.DeliveryMetadata +import com.exactpro.th2.common.schema.message.ManualAckDeliveryCallback +import com.exactpro.th2.common.schema.message.MessageSubscriber +import com.exactpro.th2.common.schema.message.SubscriberMonitor +import com.exactpro.th2.common.schema.message.impl.rabbitmq.connection.ConnectionManager +import com.rabbitmq.client.Delivery +import mu.KotlinLogging +import java.util.concurrent.CopyOnWriteArrayList + +// DRAFT of notification router +class NotificationEventBatchSubscriber( + private val connectionManager: ConnectionManager, + private val queue: String +) : MessageSubscriber { + private val listeners = CopyOnWriteArrayList>() + private lateinit var monitor: SubscriberMonitor + + fun start() { + monitor = connectionManager.basicConsume( + queue, + { deliveryMetadata: DeliveryMetadata, delivery: Delivery, confirmation: ManualAckDeliveryCallback.Confirmation -> + try { + for (listener in listeners) { + try { + listener.handle(deliveryMetadata, EventBatch.parseFrom(delivery.body), confirmation) + } catch (listenerExc: Exception) { + LOGGER.warn( + "Message listener from class '{}' threw exception", + listener.javaClass, + listenerExc + ) + } + } + } finally { + confirmation.confirm() + } + }, + { LOGGER.warn("Consuming cancelled for: '{}'", it) } + ) + } + + fun removeListener(messageListener: ConfirmationListener) { + listeners.remove(messageListener) + } + + fun addListener(messageListener: ConfirmationListener) { + listeners.add(messageListener) + } + + override fun close() { + monitor.unsubscribe() + listeners.forEach(ConfirmationListener::onClose) + listeners.clear() + } + + companion object { + private val LOGGER = KotlinLogging.logger {} + } +} diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/Cleanable.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/Cleanable.kt new file mode 100644 index 000000000..673c53681 --- /dev/null +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/Cleanable.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2023 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.exactpro.th2.common.schema.message.impl.rabbitmq.transport + +interface Cleanable { + /** + * Deep clean + */ + fun clean() +} \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/Codecs.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/Codecs.kt new file mode 100644 index 000000000..5fbd8d8a3 --- /dev/null +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/Codecs.kt @@ -0,0 +1,398 @@ +/* + * Copyright 2023 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.exactpro.th2.common.schema.message.impl.rabbitmq.transport + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import io.netty.buffer.ByteBuf +import io.netty.buffer.ByteBufInputStream +import io.netty.buffer.ByteBufOutputStream +import io.netty.buffer.ByteBufUtil +import java.io.OutputStream +import java.nio.charset.Charset +import java.time.Instant + +// TODO: maybe make length field a variable length int + +@Suppress("unused") +enum class ValueType(val codec: ValueCodec<*>) { + UNKNOWN(UnknownValueCodec), + LONG_TYPE(LongTypeCodec), + STRING_TYPE(StringTypeCodec), + MESSAGE_ID(MessageIdCodec), + BOOK(BookCodec), + SESSION_GROUP(SessionGroupCodec), + SESSION_ALIAS(SessionAliasCodec), + DIRECTION(DirectionCodec), + SEQUENCE(SequenceCodec), + SUBSEQUENCE(SubsequenceCodec), + TIMESTAMP(TimestampCodec), + METADATA(MetadataCodec), + PROTOCOL(ProtocolCodec), + MESSAGE_TYPE(MessageTypeCodec), + ID_CODEC(IdCodec), + SCOPE_CODEC(ScopeCodec), + EVENT_ID_CODEC(EventIdCodec), + RAW_MESSAGE(RawMessageCodec), + RAW_MESSAGE_BODY(RawMessageBodyCodec), + PARSED_MESSAGE(ParsedMessageCodec), + PARSED_MESSAGE_BODY(ParsedMessageRawBodyCodec), + MESSAGE_GROUP(MessageGroupCodec), + MESSAGE_LIST(MessageListCodec), + GROUP_BATCH(GroupBatchCodec), + GROUP_LIST(GroupListCodec); + + companion object { + fun forId(id: UByte): ValueType = MAPPING[id.toInt()] ?: UNKNOWN + + private val MAPPING: Array = arrayOfNulls(UByte.MAX_VALUE.toInt()).apply { + ValueType.values().forEach { + this[it.codec.type.toInt()]?.let { previous -> + error("$previous and $it elements of ValueType enum have the same type byte - ${it.codec.type}") + } + this[it.codec.type.toInt()] = it + } + } + } +} + +sealed interface ValueCodec { + val type: UByte + fun encode(source: T, target: ByteBuf) + fun decode(source: ByteBuf): T +} + +object UnknownValueCodec : ValueCodec { + override val type: UByte = 0u + override fun decode(source: ByteBuf): ByteBuf = source.readSlice(source.skipBytes(Byte.SIZE_BYTES).readIntLE()) + override fun encode(source: ByteBuf, target: ByteBuf): Nothing = throw UnsupportedOperationException() +} + +abstract class AbstractCodec(final override val type: UByte) : ValueCodec { + override fun encode(source: T, target: ByteBuf) { + val lengthIndex = target.writeByte(type.toInt()).writerIndex() + target.writeIntLE(0) + val valueIndex = target.writerIndex() + write(target, source) + target.setIntLE(lengthIndex, target.writerIndex() - valueIndex) + } + + protected abstract fun write(buffer: ByteBuf, value: T) + + override fun decode(source: ByteBuf): T { + val tag = source.readByte().toUByte() + check(tag == this.type) { "Unexpected type tag: $tag (expected: ${this.type})" } + val length = source.readIntLE() + return read(source.readSlice(length)) // FIXME: avoid slicing to avoid buffer allocation + } + + protected abstract fun read(buffer: ByteBuf): T +} + +abstract class StringCodec( + type: UByte, + private val charset: Charset = Charsets.UTF_8, +) : AbstractCodec(type) { + override fun read(buffer: ByteBuf): String = buffer.readCharSequence(buffer.readableBytes(), charset).toString() + + override fun write(buffer: ByteBuf, value: String) { + buffer.writeCharSequence(value, charset) + } +} + +abstract class LongCodec(type: UByte) : AbstractCodec(type) { + override fun read(buffer: ByteBuf): Long = buffer.readLongLE() + + override fun write(buffer: ByteBuf, value: Long) { + buffer.writeLongLE(value) + } +} + +abstract class IntCodec(type: UByte) : AbstractCodec(type) { + override fun read(buffer: ByteBuf): Int = buffer.readIntLE() + + override fun write(buffer: ByteBuf, value: Int) { + buffer.writeIntLE(value) + } +} + +abstract class ListCodec(type: UByte, private val elementCodec: ValueCodec) : AbstractCodec>(type) { + override fun read(buffer: ByteBuf): MutableList = mutableListOf().also { list -> + while (buffer.isReadable) { + list += elementCodec.decode(buffer) + } + } + + override fun write(buffer: ByteBuf, value: List) { + value.forEach { elementCodec.encode(it, buffer) } + } +} + +abstract class MapCodec( + type: UByte, + private val keyCodec: ValueCodec, + private val valueCodec: ValueCodec, +) : AbstractCodec>(type) { + override fun read(buffer: ByteBuf): MutableMap = hashMapOf().apply { + while (buffer.isReadable) { + this[keyCodec.decode(buffer)] = valueCodec.decode(buffer) + } + } + + override fun write(buffer: ByteBuf, value: Map): Unit = value.forEach { (key, value) -> + keyCodec.encode(key, buffer) + valueCodec.encode(value, buffer) + } +} + +abstract class ByteBufCodec(type: UByte) : AbstractCodec(type) { + override fun read(buffer: ByteBuf): ByteBuf = buffer.copy() + + override fun write(buffer: ByteBuf, value: ByteBuf) { + value.markReaderIndex().apply(buffer::writeBytes).resetReaderIndex() + } +} + +abstract class InstantCodec(type: UByte) : AbstractCodec(type) { + override fun read(buffer: ByteBuf): Instant = Instant.ofEpochSecond(buffer.readLongLE(), buffer.readIntLE().toLong()) + + override fun write(buffer: ByteBuf, value: Instant) { + buffer.writeLongLE(value.epochSecond).writeIntLE(value.nano) + } +} + +// FIXME: think about checking that type is unique +object LongTypeCodec : LongCodec(1u) + +object StringTypeCodec : StringCodec(2u) + +object IntTypeCodec : IntCodec(3u) + +object MessageIdCodec : AbstractCodec(10u) { + override fun read(buffer: ByteBuf): MessageId = MessageId.builder().apply { + buffer.forEachValue { codec -> + when (codec) { + is SessionAliasCodec -> setSessionAlias(codec.decode(buffer)) + is DirectionCodec -> setDirection(codec.decode(buffer)) + is SequenceCodec -> setSequence(codec.decode(buffer)) + is SubsequenceCodec -> setSubsequence(codec.decode(buffer)) + is TimestampCodec -> setTimestamp(codec.decode(buffer)) + else -> println("Skipping unexpected type ${codec.type} value: ${codec.decode(buffer)}") + } + } + }.build() + + override fun write(buffer: ByteBuf, value: MessageId) { + SessionAliasCodec.encode(value.sessionAlias, buffer) + DirectionCodec.encode(value.direction, buffer) + SequenceCodec.encode(value.sequence, buffer) + SubsequenceCodec.encode(value.subsequence, buffer) + TimestampCodec.encode(value.timestamp, buffer) + } +} + +object BookCodec : StringCodec(101u) + +object SessionGroupCodec : StringCodec(102u) + +object SessionAliasCodec : StringCodec(103u) + +object DirectionCodec : AbstractCodec(104u) { + override fun read(buffer: ByteBuf): Direction = Direction.forId(buffer.readByte().toInt()) + + override fun write(buffer: ByteBuf, value: Direction) { + buffer.writeByte(value.id) + } +} + +object SequenceCodec : LongCodec(105u) + +object SubsequenceCodec : ListCodec(106u, IntTypeCodec) + +object TimestampCodec : InstantCodec(107u) + +object MetadataCodec : MapCodec(11u, StringTypeCodec, StringTypeCodec) + +object ProtocolCodec : StringCodec(12u) + +object MessageTypeCodec : StringCodec(13u) + +object IdCodec : StringCodec(14u) + +object ScopeCodec : StringCodec(15u) + +object EventIdCodec : AbstractCodec(16u) { + override fun read(buffer: ByteBuf): EventId { + return EventId.builder().apply { + buffer.forEachValue { codec -> + when (codec) { + is IdCodec -> setId(codec.decode(buffer)) + is BookCodec -> setBook(codec.decode(buffer)) + is ScopeCodec -> setScope(codec.decode(buffer)) + is TimestampCodec -> setTimestamp(codec.decode(buffer)) + else -> println("Skipping unexpected type ${codec.type} value: ${codec.decode(buffer)}") + } + } + }.build() + } + + override fun write(buffer: ByteBuf, value: EventId) { + IdCodec.encode(value.id, buffer) + BookCodec.encode(value.book, buffer) + ScopeCodec.encode(value.scope, buffer) + TimestampCodec.encode(value.timestamp, buffer) + } +} + +object RawMessageCodec : AbstractCodec(20u) { + override fun read(buffer: ByteBuf): RawMessage = RawMessage.builder().apply { + buffer.forEachValue { codec -> + when (codec) { + is MessageIdCodec -> setId(codec.decode(buffer)) + is EventIdCodec -> setEventId(codec.decode(buffer)) + is MetadataCodec -> setMetadata(codec.decode(buffer)) + is ProtocolCodec -> setProtocol(codec.decode(buffer)) + is RawMessageBodyCodec -> setBody(codec.decode(buffer)) + else -> println("Skipping unexpected type ${codec.type} value: ${codec.decode(buffer)}") + } + } + }.build() + + override fun write(buffer: ByteBuf, value: RawMessage) { + MessageIdCodec.encode(value.id, buffer) + value.eventId?.run { EventIdCodec.encode(this, buffer) } + MetadataCodec.encode(value.metadata, buffer) + ProtocolCodec.encode(value.protocol, buffer) + RawMessageBodyCodec.encode(value.body, buffer) + } +} + +object RawMessageBodyCodec : ByteBufCodec(21u) + +object ParsedMessageCodec : AbstractCodec(30u) { + override fun read(buffer: ByteBuf): ParsedMessage = ParsedMessage.builder { buf -> + ByteBufInputStream(buf).use { MAPPER.readValue(it) } + }.apply { + buffer.forEachValue { codec -> + when (codec) { + is MessageIdCodec -> setId(codec.decode(buffer)) + is EventIdCodec -> setEventId(codec.decode(buffer)) + is MetadataCodec -> setMetadata(codec.decode(buffer)) + is ProtocolCodec -> setProtocol(codec.decode(buffer)) + is MessageTypeCodec -> setType(codec.decode(buffer)) + is ParsedMessageRawBodyCodec -> setRawBody(codec.decode(buffer)) + else -> println("Skipping unexpected type ${codec.type} value: ${codec.decode(buffer)}") + } + } + }.build() + + override fun write(buffer: ByteBuf, value: ParsedMessage) { + MessageIdCodec.encode(value.id, buffer) + value.eventId?.run { EventIdCodec.encode(this, buffer) } + MetadataCodec.encode(value.metadata, buffer) + ProtocolCodec.encode(value.protocol, buffer) + MessageTypeCodec.encode(value.type, buffer) + if (!value.isBodyInRaw) { + // Update raw body because the body was changed + ByteBufOutputStream(value.rawBody.clear()).use { + MAPPER.writeValue(it as OutputStream, value.body) + } + value.rawBody.resetReaderIndex() + } + ParsedMessageRawBodyCodec.encode(value.rawBody, buffer) + } + + @JvmField + val MAPPER: ObjectMapper = jacksonObjectMapper().registerModule(JavaTimeModule()) + // otherwise, type supported by JavaTimeModule will be serialized as array of date component + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + // this is required to serialize nulls and empty collections + .setSerializationInclusion(JsonInclude.Include.ALWAYS) +} + +object ParsedMessageRawBodyCodec : ByteBufCodec(31u) + +object MessageGroupCodec : AbstractCodec(40u) { + override fun read(buffer: ByteBuf): MessageGroup = MessageGroup.builder().apply { + buffer.forEachValue { codec -> + when (codec) { + is MessageListCodec -> setMessages(codec.decode(buffer)) + else -> println("Skipping unexpected type ${codec.type} value: ${codec.decode(buffer)}") + } + } + }.build() + + override fun write(buffer: ByteBuf, value: MessageGroup) { + MessageListCodec.encode(value.messages, buffer) + } +} + +object MessageListCodec : AbstractCodec>>(41u) { + override fun read(buffer: ByteBuf): MutableList> = mutableListOf>().apply { + buffer.forEachValue { codec -> + when (codec) { + is RawMessageCodec -> this += codec.decode(buffer) + is ParsedMessageCodec -> this += codec.decode(buffer) + else -> println("Skipping unexpected type ${codec.type} value: ${codec.decode(buffer)}") + } + } + } + + override fun write(buffer: ByteBuf, value: List>): Unit = value.forEach { message -> + when (message) { + is RawMessage -> RawMessageCodec.encode(message, buffer) + is ParsedMessage -> ParsedMessageCodec.encode(message, buffer) + else -> println("Skipping unsupported message type: $message") + } + } +} + +object GroupBatchCodec : AbstractCodec(50u) { + override fun read(buffer: ByteBuf): GroupBatch = GroupBatch.builder().apply { + buffer.forEachValue { codec -> + when (codec) { + is BookCodec -> setBook(codec.decode(buffer)) + is SessionGroupCodec -> setSessionGroup(codec.decode(buffer)) + is GroupListCodec -> setGroups(codec.decode(buffer)) + else -> println("Skipping unexpected type ${codec.type} value: ${codec.decode(buffer)}") + } + } + }.build() + + override fun write(buffer: ByteBuf, value: GroupBatch) { + BookCodec.encode(value.book, buffer) + SessionGroupCodec.encode(value.sessionGroup, buffer) + GroupListCodec.encode(value.groups, buffer) + } +} + +object GroupListCodec : ListCodec(51u, MessageGroupCodec) + +inline fun ByteBuf.forEachValue(action: (codec: ValueCodec<*>) -> Unit) { + while (isReadable) { + val type = getByte(readerIndex()).toUByte() + + when (val codec = ValueType.forId(type).codec) { + is UnknownValueCodec -> println("Skipping unknown type $type value: ${ByteBufUtil.hexDump(codec.decode(this))}") + else -> action(codec) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/Direction.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/Direction.kt new file mode 100644 index 000000000..fc68922e2 --- /dev/null +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/Direction.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2023 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.exactpro.th2.common.schema.message.impl.rabbitmq.transport + +enum class Direction(val id: Int) { + /** + * Related to messages are income to a client + */ + INCOMING(1), + /** + * Related to messages are out gone from a client + */ + OUTGOING(2); + + companion object { + fun forId(id: Int): Direction = when (id) { + 1 -> INCOMING + 2 -> OUTGOING + else -> error("Unknown direction id: $id") + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/EventId.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/EventId.kt new file mode 100644 index 000000000..98f140755 --- /dev/null +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/EventId.kt @@ -0,0 +1,155 @@ +/* + * Copyright 2023 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.exactpro.th2.common.schema.message.impl.rabbitmq.transport + +import java.time.Instant +import java.util.StringJoiner + +data class EventId( + val id: String, + val book: String, + val scope: String, + val timestamp: Instant, +) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as EventId + + if (id != other.id) return false + if (book != other.book) return false + if (scope != other.scope) return false + return timestamp == other.timestamp + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + book.hashCode() + result = 31 * result + scope.hashCode() + result = 31 * result + timestamp.hashCode() + return result + } + + override fun toString(): String { + return "EventId(id='$id', book='$book', scope='$scope', timestamp=$timestamp)" + } + + interface Builder { + val id: String + fun isIdSet(): Boolean + val book: String + fun isBookSet(): Boolean + val scope: String + fun isScopeSet(): Boolean + val timestamp: Instant + fun isTimestampSet(): Boolean + + fun setId(id: String): Builder + fun setBook(book: String): Builder + fun setScope(scope: String): Builder + fun setTimestamp(timestamp: Instant): Builder + fun build(): EventId + } + + fun toBuilder(): Builder = BuilderImpl(this) + + companion object { + @JvmStatic + fun builder(): Builder = BuilderImpl() + } + +} + +private class BuilderImpl : EventId.Builder { + private var _id: String? = null + private var _book: String? = null + private var _scope: String? = null + private var _timestamp: Instant? = null + + constructor() + constructor(source: EventId) { + _id = source.id + _book = source.book + _scope = source.scope + _timestamp = source.timestamp + } + + override fun setId(id: String): EventId.Builder = apply { + this._id = id + } + + override val id: String + get() = checkNotNull(_id) { "Property \"id\" has not been set" } + + override fun isIdSet(): Boolean = _id != null + + override fun setBook(book: String): EventId.Builder = apply { + this._book = book + } + + override val book: String + get() = checkNotNull(_book) { "Property \"book\" has not been set" } + + override fun isBookSet(): Boolean = _book != null + + override fun setScope(scope: String): EventId.Builder = apply { + this._scope = scope + } + + override val scope: String + get() = checkNotNull(_scope) { "Property \"scope\" has not been set" } + + override fun isScopeSet(): Boolean = _scope != null + + override fun setTimestamp(timestamp: Instant): EventId.Builder = apply { + this._timestamp = timestamp + } + + override val timestamp: Instant + get() { + return checkNotNull(_timestamp) { "Property \"timestamp\" has not been set" } + } + + override fun isTimestampSet(): Boolean = _timestamp != null + + override fun build(): EventId { + if (_id == null || _book == null || _scope == null || _timestamp == null) { + val missing = StringJoiner(",", "[", "]") + if (_id == null) { + missing.add("id") + } + if (_book == null) { + missing.add("book") + } + if (_scope == null) { + missing.add("scope") + } + if (_timestamp == null) { + missing.add("timestamp") + } + error("Missing required properties: $missing") + } + return EventId( + _id!!, + _book!!, + _scope!!, + _timestamp!!, + ) + } +} diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/GroupBatch.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/GroupBatch.kt new file mode 100644 index 000000000..933e78d14 --- /dev/null +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/GroupBatch.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2023 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.exactpro.th2.common.schema.message.impl.rabbitmq.transport + +import com.exactpro.th2.common.schema.message.impl.rabbitmq.transport.builders.CollectionBuilder +import com.google.auto.value.AutoBuilder + +data class GroupBatch( + val book: String, + val sessionGroup: String, + val groups: List = emptyList(), +) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as GroupBatch + + if (book != other.book) return false + if (sessionGroup != other.sessionGroup) return false + return groups == other.groups + } + + override fun hashCode(): Int { + var result = book.hashCode() + result = 31 * result + sessionGroup.hashCode() + result = 31 * result + groups.hashCode() + return result + } + + override fun toString(): String { + return "GroupBatch(book='$book', sessionGroup='$sessionGroup', groups=$groups)" + } + + @AutoBuilder + interface Builder { + val book: String + val sessionGroup: String + + fun setBook(book: String): Builder + fun setSessionGroup(sessionGroup: String): Builder + fun groupsBuilder(): CollectionBuilder + fun addGroup(group: MessageGroup): Builder = apply { + groupsBuilder().add(group) + } + + fun setGroups(groups: List): Builder + fun build(): GroupBatch + } + + fun toBuilder(): Builder = AutoBuilder_GroupBatch_Builder(this) + + companion object { + @JvmStatic + fun builder(): Builder = AutoBuilder_GroupBatch_Builder() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/Message.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/Message.kt new file mode 100644 index 000000000..a109c63f5 --- /dev/null +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/Message.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2023 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.exactpro.th2.common.schema.message.impl.rabbitmq.transport + +import com.exactpro.th2.common.schema.message.impl.rabbitmq.transport.builders.MapBuilder + +interface Message { + /** The id is not mutable by default */ + val id: MessageId + val eventId: EventId? + + /** The metadata is not mutable by default */ + val metadata: Map + val protocol: String + val body: T + + interface Builder> { + val protocol: String + fun isProtocolSet(): Boolean + val eventId: EventId? + + fun setId(id: MessageId): T + fun idBuilder(): MessageId.Builder + fun setEventId(eventId: EventId): T + fun setProtocol(protocol: String): T + fun setMetadata(metadata: Map): T + fun metadataBuilder(): MapBuilder + + fun addMetadataProperty(key: String, value: String): T + fun build(): Message<*> + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/MessageGroup.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/MessageGroup.kt new file mode 100644 index 000000000..00bbd1f74 --- /dev/null +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/MessageGroup.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2023 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.exactpro.th2.common.schema.message.impl.rabbitmq.transport + +import com.exactpro.th2.common.schema.message.impl.rabbitmq.transport.builders.GenericCollectionBuilder +import com.google.auto.value.AutoBuilder + + +data class MessageGroup( + val messages: List> = emptyList(), // FIXME: message can have incompatible book and group +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MessageGroup + + return messages == other.messages + } + + override fun hashCode(): Int { + return messages.hashCode() + } + + override fun toString(): String { + return "MessageGroup(messages=$messages)" + } + + // We cannot use AutoBuilder here because of the different method signatures in builder when a generic type is used + // + @AutoBuilder + interface Builder { + fun messagesBuilder(): GenericCollectionBuilder> + fun addMessage(message: Message<*>): Builder = apply { + messagesBuilder().add(message) + } + + fun setMessages(message: List>): Builder + fun build(): MessageGroup + } + + //TODO: add override annotation + fun toBuilder(): Builder = builder().setMessages(messages) + + class CollectionBuilder { + private val elements: MutableList> = mutableListOf() + + fun add(el: Message<*>): CollectionBuilder = apply { + elements += el + } + + fun addAll(vararg els: Message<*>): CollectionBuilder = apply { + for (el in els) { + add(el) + } + } + + fun addAll(elements: Collection>): CollectionBuilder = apply { + this.elements.addAll(elements) + } + + fun build(): List> = elements + } + + companion object { + @JvmStatic + fun builder(): Builder = AutoBuilder_MessageGroup_Builder() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/MessageId.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/MessageId.kt new file mode 100644 index 000000000..5bd43a5d6 --- /dev/null +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/MessageId.kt @@ -0,0 +1,198 @@ +/* + * Copyright 2023 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.exactpro.th2.common.schema.message.impl.rabbitmq.transport + +import com.exactpro.th2.common.schema.message.impl.rabbitmq.transport.builders.CollectionBuilder +import java.time.Instant +import java.util.StringJoiner + +data class MessageId( + val sessionAlias: String, + val direction: Direction, + val sequence: Long, + val timestamp: Instant, + /** The subsequence is not mutable by default */ + val subsequence: List = emptyList(), +) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MessageId + + if (sessionAlias != other.sessionAlias) return false + if (direction != other.direction) return false + if (sequence != other.sequence) return false + if (timestamp != other.timestamp) return false + return subsequence == other.subsequence + } + + override fun hashCode(): Int { + var result = sessionAlias.hashCode() + result = 31 * result + direction.hashCode() + result = 31 * result + sequence.hashCode() + result = 31 * result + timestamp.hashCode() + result = 31 * result + subsequence.hashCode() + return result + } + + override fun toString(): String { + return "MessageId(sessionAlias='$sessionAlias', direction=$direction, sequence=$sequence, timestamp=$timestamp, subsequence=$subsequence)" + } + + interface Builder { + val sessionAlias: String + fun isSessionAliasSet(): Boolean + val direction: Direction + fun isDirectionSet(): Boolean + val sequence: Long + fun isSequenceSet(): Boolean + val timestamp: Instant + fun isTimestampSet(): Boolean + + fun setSessionAlias(sessionAlias: String): Builder + fun setDirection(direction: Direction): Builder + fun setSequence(sequence: Long): Builder + fun setTimestamp(timestamp: Instant): Builder + fun subsequenceBuilder(): CollectionBuilder + fun addSubsequence(subsequence: Int): Builder = apply { + subsequenceBuilder().add(subsequence) + } + + fun setSubsequence(subsequence: List): Builder + fun build(): MessageId + } + + fun toBuilder(): Builder = MessageIdBuilderImpl(this) + + companion object { + @JvmStatic + val DEFAULT: MessageId = MessageId("", Direction.OUTGOING, 0, Instant.EPOCH) + + @JvmStatic + fun builder(): Builder = MessageIdBuilderImpl() + } +} + + +private const val SEQUENCE_NOT_SET = Long.MIN_VALUE + +private class MessageIdBuilderImpl : MessageId.Builder { + private var _sessionAlias: String? = null + private var _direction: Direction? = null + private var _sequence: Long = SEQUENCE_NOT_SET + private var _timestamp: Instant? = null + private var _subsequenceBuilder: CollectionBuilder? = null + private var _subsequence: List = emptyList() + + constructor() + constructor(source: MessageId) { + _sessionAlias = source.sessionAlias + _direction = source.direction + _sequence = source.sequence + _timestamp = source.timestamp + _subsequence = source.subsequence + } + + override fun setSessionAlias(sessionAlias: String): MessageId.Builder = apply { + this._sessionAlias = sessionAlias + } + + override val sessionAlias: String + get() = checkNotNull(_sessionAlias) { "Property \"sessionAlias\" has not been set" } + + override fun isSessionAliasSet(): Boolean = _sessionAlias != null + + override fun setDirection(direction: Direction): MessageId.Builder = apply { + this._direction = direction + } + + override val direction: Direction + get() = checkNotNull(_direction) { "Property \"direction\" has not been set" } + + override fun isDirectionSet(): Boolean = _direction != null + + override fun setSequence(sequence: Long): MessageId.Builder = apply { + require(sequence != SEQUENCE_NOT_SET) { "Value $sequence for property \"sequence\" is reserved" } + this._sequence = sequence + } + + override val sequence: Long + get() { + check(_sequence != SEQUENCE_NOT_SET) { "Property \"sequence\" has not been set" } + return _sequence + } + + override fun isSequenceSet(): Boolean = _sequence != SEQUENCE_NOT_SET + + override fun setTimestamp(timestamp: Instant): MessageId.Builder = apply { + this._timestamp = timestamp + } + + override val timestamp: Instant + get() = checkNotNull(_timestamp) { "Property \"timestamp\" has not been set" } + + override fun isTimestampSet(): Boolean = _timestamp != null + + override fun setSubsequence(subsequence: List): MessageId.Builder = apply { + check(_subsequenceBuilder == null) { "Cannot set subsequence after calling subsequenceBuilder()" } + this._subsequence = subsequence + } + + override fun subsequenceBuilder(): CollectionBuilder { + if (_subsequenceBuilder == null) { + if (_subsequence.isEmpty()) { + _subsequenceBuilder = CollectionBuilder() + } else { + _subsequenceBuilder = CollectionBuilder().apply { + addAll(_subsequence) + } + _subsequence = emptyList() + } + } + return checkNotNull(_subsequenceBuilder) { "subsequenceBuilder" } + } + + override fun build(): MessageId { + _subsequence = _subsequenceBuilder?.build() ?: _subsequence + if (_sessionAlias == null || _direction == null || _sequence == SEQUENCE_NOT_SET || _timestamp == null) { + val missing = StringJoiner(",", "[", "]") + if (_sessionAlias == null) { + missing.add("sessionAlias") + } + if (_direction == null) { + missing.add("direction") + } + if (_sequence == SEQUENCE_NOT_SET) { + missing.add("sequence") + } + if (_timestamp == null) { + missing.add("timestamp") + } + error("Missing required properties: $missing") + } + return MessageId( + _sessionAlias!!, + _direction!!, + _sequence, + _timestamp!!, + _subsequence, + ) + } +} + diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/ParsedMessage.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/ParsedMessage.kt new file mode 100644 index 000000000..cf5d9ccbb --- /dev/null +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/ParsedMessage.kt @@ -0,0 +1,317 @@ +/* + * Copyright 2023 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.exactpro.th2.common.schema.message.impl.rabbitmq.transport + +import com.exactpro.th2.common.schema.message.impl.rabbitmq.transport.builders.MapBuilder +import io.netty.buffer.ByteBuf +import io.netty.buffer.Unpooled +import java.util.Collections + +class ParsedMessage private constructor( + override val id: MessageId = MessageId.DEFAULT, + override val eventId: EventId? = null, + val type: String, + override val metadata: Map = emptyMap(), + override val protocol: String = "", + val rawBody: ByteBuf = Unpooled.buffer(), + private val bodySupplier: (ByteBuf) -> Map = DEFAULT_BODY_SUPPLIER, + body: Map = DEFAULT_BODY, +) : Message> { + constructor( + id: MessageId = MessageId.DEFAULT, + eventId: EventId? = null, + type: String, + metadata: Map = emptyMap(), + protocol: String = "", + rawBody: ByteBuf = Unpooled.buffer(), + bodySupplier: (ByteBuf) -> Map = DEFAULT_BODY_SUPPLIER, + ) : this( + id = id, + eventId = eventId, + type = type, + metadata = metadata, + protocol = protocol, + rawBody = rawBody, + bodySupplier = bodySupplier, + body = DEFAULT_BODY, + ) + + constructor( + id: MessageId = MessageId.DEFAULT, + eventId: EventId? = null, + type: String, + metadata: Map = emptyMap(), + protocol: String = "", + body: Map, + ) : this( + id = id, + eventId = eventId, + type = type, + metadata = metadata, + protocol = protocol, + rawBody = Unpooled.buffer(), + bodySupplier = DEFAULT_BODY_SUPPLIER, + body = body, + ) + + /** + * Is set to `true` if the [body] is deserialized from the [rawBody]. + * If the [body] is set directly returns `false` + */ + val isBodyInRaw: Boolean = body === DEFAULT_BODY + + override val body: Map by lazy { + if (body === DEFAULT_BODY) { + bodySupplier.invoke(rawBody).apply { + rawBody.resetReaderIndex() + } + } else { + body + } + } + + + interface Builder> : Message.Builder { + val type: String + fun isTypeSet(): Boolean + + fun setType(type: String): T + override fun build(): ParsedMessage + } + + //TODO: add override annotation + fun toBuilder(): FromMapBuilder { + return FromMapBuilderImpl() + .setBody(body) + .setId(id) + .setMetadata(metadata) + .setProtocol(protocol) + .setType(type) + .also { eventId?.let(it::setEventId) } + } + + interface FromRawBuilder : Builder { + fun setRawBody(rawBody: ByteBuf): FromRawBuilder + } + + interface FromMapBuilder : Builder { + fun setBody(body: Map): FromMapBuilder + fun bodyBuilder(): MapBuilder + fun addField(name: String, value: Any?): FromMapBuilder + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ParsedMessage + + if (id != other.id) return false + if (eventId != other.eventId) return false + if (type != other.type) return false + if (metadata != other.metadata) return false + if (protocol != other.protocol) return false + return rawBody == other.rawBody + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + (eventId?.hashCode() ?: 0) + result = 31 * result + type.hashCode() + result = 31 * result + metadata.hashCode() + result = 31 * result + protocol.hashCode() + result = 31 * result + rawBody.hashCode() + return result + } + + override fun toString(): String { + return "ParsedMessage(id=$id, " + + "eventId=$eventId, " + + "type='$type', " + + "metadata=$metadata, " + + "protocol='$protocol', " + + "rawBody=${rawBody.toString(Charsets.UTF_8)}, " + + "body=${ + if (isBodyInRaw) { + "!checkRawBody" + } else { + body.toString() + } + })" + } + + + companion object { + @JvmField + val EMPTY: ParsedMessage = ParsedMessage(type = "", body = emptyMap()) + + @Deprecated( + "Please use EMPTY instead. Added for binary compatibility", + level = DeprecationLevel.HIDDEN, + replaceWith = ReplaceWith(expression = "EMPTY") + ) + @JvmStatic + fun getEMPTY(): ParsedMessage = EMPTY + + /** + * We want to be able to identify the default body by reference. + * So, that is why we use unmodifiableMap with emptyMap + * Otherwise, we won't be able to identify it + */ + private val DEFAULT_BODY: Map = Collections.unmodifiableMap(emptyMap()) + + @JvmField + val DEFAULT_BODY_SUPPLIER: (ByteBuf) -> Map = { emptyMap() } + + @JvmStatic + fun builder(bodySupplier: (ByteBuf) -> Map): FromRawBuilder = FromRawBuilderImpl(bodySupplier) + + @JvmStatic + fun builder(): FromMapBuilder = FromMapBuilderImpl() + } +} + +@Suppress("PropertyName") +private sealed class BaseParsedBuilder> : ParsedMessage.Builder { + protected var idBuilder: MessageId.Builder? = null + protected var id: MessageId? = MessageId.DEFAULT + final override var eventId: EventId? = null + private set + protected var _protocol: String? = null + protected var _type: String? = null + protected var metadataBuilder: MapBuilder? = null + protected var metadata: Map? = emptyMap() + + override val protocol: String + get() = this._protocol ?: "" + + override fun isProtocolSet(): Boolean = _protocol != null + override val type: String + get() = requireNotNull(this._type) { + "Property \"type\" has not been set" + } + + override fun isTypeSet(): Boolean = _type != null + + override fun setId(id: MessageId): T = self { + require(idBuilder == null) { + "cannot set id after calling idBuilder()" + } + this.id = id + } + + override fun idBuilder(): MessageId.Builder { + if (idBuilder == null) { + idBuilder = id?.toBuilder()?.also { + id = null + } ?: MessageId.builder() + } + return requireNotNull(idBuilder) { "idBuilder is null" } + } + + override fun setEventId(eventId: EventId): T = self { + this.eventId = eventId + } + + override fun setProtocol(protocol: String): T = self { + this._protocol = protocol + } + + override fun setMetadata(metadata: Map): T = self { + require(metadataBuilder == null) { + "cannot set metadata after calling metadataBuilder()" + } + this.metadata = metadata + } + + override fun setType(type: String): T = self { + this._type = type + } + + override fun metadataBuilder(): MapBuilder { + if (metadataBuilder == null) { + metadataBuilder = metadata?.let { + metadata = null + MapBuilder().putAll(it) + } ?: MapBuilder() + } + return requireNotNull(metadataBuilder) { "metadataBuilder is null" } + } + + override fun addMetadataProperty(key: String, value: String): T = self { + metadataBuilder().put(key, value) + } + + @Suppress("UNCHECKED_CAST") + private inline fun self(block: BaseParsedBuilder.() -> Unit): T { + block() + return this as T + } +} + +private class FromRawBuilderImpl( + private val bodySupplier: (ByteBuf) -> Map, +) : BaseParsedBuilder(), ParsedMessage.FromRawBuilder { + private var rawBody: ByteBuf? = null + override fun setRawBody(rawBody: ByteBuf): ParsedMessage.FromRawBuilder = apply { + this.rawBody = rawBody + } + + override fun build(): ParsedMessage = ParsedMessage( + id = id ?: idBuilder?.build() ?: error("missing id"), + eventId = eventId, + type = _type ?: error("missing type"), + metadata = metadata ?: metadataBuilder?.build() ?: emptyMap(), + protocol = _protocol ?: "", + rawBody = rawBody ?: error("missing raw body"), + bodySupplier = bodySupplier, + ) +} + +private class FromMapBuilderImpl : BaseParsedBuilder(), ParsedMessage.FromMapBuilder { + private var body: Map? = null + private var bodyBuilder: MapBuilder? = null + override fun setBody(body: Map): ParsedMessage.FromMapBuilder = apply { + require(bodyBuilder == null) { + "cannot set body after calling bodyBuilder()" + } + this.body = body + } + + override fun bodyBuilder(): MapBuilder { + if (bodyBuilder == null) { + bodyBuilder = body?.let { + body = null + MapBuilder().putAll(it) + } ?: MapBuilder() + } + return requireNotNull(bodyBuilder) { "bodyBuilder is null" } + } + + override fun addField(name: String, value: Any?): ParsedMessage.FromMapBuilder = apply { + bodyBuilder().put(name, value) + } + + override fun build(): ParsedMessage = ParsedMessage( + id = id ?: idBuilder?.build() ?: error("missing id"), + eventId = eventId, + type = _type ?: error("missing type"), + metadata = metadata ?: metadataBuilder?.build() ?: emptyMap(), + protocol = _protocol ?: "", + body = body ?: bodyBuilder?.build() ?: error("missing body"), + ) +} \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/RawMessage.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/RawMessage.kt new file mode 100644 index 000000000..0defb4862 --- /dev/null +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/RawMessage.kt @@ -0,0 +1,190 @@ +/* + * Copyright 2023 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.exactpro.th2.common.schema.message.impl.rabbitmq.transport + +import com.exactpro.th2.common.schema.message.impl.rabbitmq.transport.MessageId.Companion.builder +import com.exactpro.th2.common.schema.message.impl.rabbitmq.transport.builders.MapBuilder +import io.netty.buffer.ByteBuf +import io.netty.buffer.ByteBufUtil.hexDump +import io.netty.buffer.Unpooled + +data class RawMessage( + override val id: MessageId = MessageId.DEFAULT, + override val eventId: EventId? = null, + override val metadata: Map = emptyMap(), + override val protocol: String = "", + /** The body is not mutable by default */ + override val body: ByteBuf = Unpooled.EMPTY_BUFFER, +) : Message { + + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as RawMessage + + if (id != other.id) return false + if (eventId != other.eventId) return false + if (metadata != other.metadata) return false + if (protocol != other.protocol) return false + return body == other.body + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + (eventId?.hashCode() ?: 0) + result = 31 * result + metadata.hashCode() + result = 31 * result + protocol.hashCode() + result = 31 * result + body.hashCode() + return result + } + + override fun toString(): String { + return "RawMessage(id=$id, eventId=$eventId, metadata=$metadata, protocol='$protocol', body=${hexDump(body)})" + } + + interface Builder : Message.Builder { + + val body: ByteBuf + fun isBodySet(): Boolean + fun setBody(body: ByteBuf): Builder + + fun setBody(data: ByteArray): Builder = setBody(Unpooled.wrappedBuffer(data)) + + override fun addMetadataProperty(key: String, value: String): Builder = this.apply { + metadataBuilder().put(key, value) + } + + override fun build(): RawMessage + } + + fun toBuilder(): Builder = RawMessageBuilderImpl(this) + + companion object { + + @JvmField + val EMPTY: RawMessage = RawMessage() + + @Deprecated( + "Please use EMPTY instead. Added for binary compatibility", + level = DeprecationLevel.HIDDEN, + replaceWith = ReplaceWith(expression = "EMPTY") + ) + @JvmStatic + fun getEMPTY(): RawMessage = EMPTY + + @JvmStatic + fun builder(): Builder = RawMessageBuilderImpl() + } +} + + +internal class RawMessageBuilderImpl : RawMessage.Builder { + private var _idBuilder: MessageId.Builder? = null + private var _id: MessageId? = null + private var _eventId: EventId? = null + private var _metadataBuilder: MapBuilder? = null + private var metadata: Map? = null + private var _protocol: String? = null + private var _body: ByteBuf? = null + + constructor() + constructor(source: RawMessage) { + _id = source.id + _eventId = source.eventId + metadata = source.metadata + _protocol = source.protocol + _body = source.body + } + + override fun setId(id: MessageId): RawMessage.Builder = apply { + check(_idBuilder == null) { "Cannot set id after calling idBuilder()" } + this._id = id + } + + override fun idBuilder(): MessageId.Builder { + if (_idBuilder == null) { + _idBuilder = _id?.toBuilder()?.also { + _id = null + } ?: builder() + } + return _idBuilder!! + } + + override fun setEventId(eventId: EventId): RawMessage.Builder = apply { + this._eventId = eventId + return this + } + + override val eventId: EventId? + get() = _eventId + + override fun setMetadata(metadata: Map): RawMessage.Builder = apply { + check(_metadataBuilder == null) { "Cannot set metadata after calling metadataBuilder()" } + this.metadata = metadata + } + + override fun metadataBuilder(): MapBuilder { + if (_metadataBuilder == null) { + if (metadata == null) { + _metadataBuilder = MapBuilder() + } else { + _metadataBuilder = MapBuilder().apply { + metadata?.let(this::putAll) + } + metadata = null + } + } + return _metadataBuilder!! + } + + override fun setProtocol(protocol: String): RawMessage.Builder = apply { + this._protocol = protocol + } + + override val protocol: String + get() { + return checkNotNull(_protocol) { "Property \"protocol\" has not been set" } + } + + override fun isProtocolSet(): Boolean = _protocol != null + + override fun setBody(body: ByteBuf): RawMessage.Builder = apply { + this._body = body + } + + override val body: ByteBuf + get() = checkNotNull(_body) { "Property \"body\" has not been set" } + + override fun isBodySet(): Boolean = _body != null + + override fun build(): RawMessage { + _id = _idBuilder?.build() + ?: _id + metadata = _metadataBuilder?.build() + ?: metadata + return RawMessage( + _id ?: MessageId.DEFAULT, + _eventId, + metadata ?: emptyMap(), + _protocol ?: "", + _body ?: Unpooled.EMPTY_BUFFER, + ) + } +} + diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/TransportGroupBatchRouter.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/TransportGroupBatchRouter.kt new file mode 100644 index 000000000..a887fc063 --- /dev/null +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/TransportGroupBatchRouter.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2023 Exactpro (Exactpro Systems Limited) + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.exactpro.th2.common.schema.message.impl.rabbitmq.transport + +import com.exactpro.th2.common.schema.message.ConfirmationListener +import com.exactpro.th2.common.schema.message.MessageSender +import com.exactpro.th2.common.schema.message.MessageSubscriber +import com.exactpro.th2.common.schema.message.QueueAttribute +import com.exactpro.th2.common.schema.message.configuration.QueueConfiguration +import com.exactpro.th2.common.schema.message.impl.rabbitmq.AbstractRabbitRouter +import com.exactpro.th2.common.schema.message.impl.rabbitmq.BookName +import com.exactpro.th2.common.schema.message.impl.rabbitmq.PinConfiguration +import com.exactpro.th2.common.schema.message.impl.rabbitmq.PinName +import org.jetbrains.annotations.NotNull + +class TransportGroupBatchRouter : AbstractRabbitRouter() { + override fun splitAndFilter( + message: GroupBatch, + pinConfiguration: @NotNull QueueConfiguration, + pinName: PinName, + ): GroupBatch? = pinConfiguration.filters.filter(message) + + override fun createSender( + pinConfig: QueueConfiguration, + pinName: PinName, + bookName: BookName, + ): MessageSender { + return TransportGroupBatchSender( + connectionManager, + pinConfig.exchange, + pinConfig.routingKey, + pinName, + bookName + ) + } + + override fun createSubscriber( + pinConfig: PinConfiguration, + pinName: PinName, + listener: ConfirmationListener + ): MessageSubscriber { + return TransportGroupBatchSubscriber( + connectionManager, + pinConfig.queue, + pinName, + pinConfig.filters, + listener + ) + } + + override fun GroupBatch.toErrorString(): String = toString() + + override fun getRequiredSendAttributes(): Set = REQUIRED_SEND_ATTRIBUTES + override fun getRequiredSubscribeAttributes(): Set = REQUIRED_SUBSCRIBE_ATTRIBUTES + + companion object { + const val TRANSPORT_GROUP_TYPE = "TRANSPORT_GROUP" + const val TRANSPORT_GROUP_ATTRIBUTE = "transport-group" + private val REQUIRED_SUBSCRIBE_ATTRIBUTES = setOf(QueueAttribute.SUBSCRIBE.value, TRANSPORT_GROUP_ATTRIBUTE) + private val REQUIRED_SEND_ATTRIBUTES = setOf(QueueAttribute.PUBLISH.value, TRANSPORT_GROUP_ATTRIBUTE) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/TransportGroupBatchSender.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/TransportGroupBatchSender.kt new file mode 100644 index 000000000..40e47fbef --- /dev/null +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/TransportGroupBatchSender.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2023 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.exactpro.th2.common.schema.message.impl.rabbitmq.transport + +import com.exactpro.th2.common.metrics.BOOK_NAME_LABEL +import com.exactpro.th2.common.metrics.SESSION_GROUP_LABEL +import com.exactpro.th2.common.metrics.TH2_PIN_LABEL +import com.exactpro.th2.common.schema.message.impl.rabbitmq.AbstractRabbitSender +import com.exactpro.th2.common.schema.message.impl.rabbitmq.BookName +import com.exactpro.th2.common.schema.message.impl.rabbitmq.connection.ConnectionManager +import com.exactpro.th2.common.schema.message.impl.rabbitmq.transport.TransportGroupBatchRouter.Companion.TRANSPORT_GROUP_TYPE +import io.prometheus.client.Counter + +class TransportGroupBatchSender( + connectionManager: ConnectionManager, + exchangeName: String, + routingKey: String, + th2Pin: String, + bookName: BookName, +) : AbstractRabbitSender( + connectionManager, + exchangeName, + routingKey, + th2Pin, + TRANSPORT_GROUP_TYPE, + bookName +) { + override fun send(value: GroupBatch) { + TRANSPORT_GROUP_PUBLISH_TOTAL + .labels(th2Pin, value.book, value.sessionGroup) + .inc(value.groups.size.toDouble()) + + super.send(value) + } + + override fun valueToBytes(value: GroupBatch): ByteArray = value.toByteArray() + + override fun toShortTraceString(value: GroupBatch): String = value.toString() + + override fun toShortDebugString(value: GroupBatch): String = value.toString() + + companion object { + private val TRANSPORT_GROUP_PUBLISH_TOTAL = Counter.build() + .name("th2_transport_group_publish_total") + .labelNames(TH2_PIN_LABEL, BOOK_NAME_LABEL, SESSION_GROUP_LABEL) + .help("Quantity of published transport groups") + .register() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/TransportGroupBatchSubscriber.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/TransportGroupBatchSubscriber.kt new file mode 100644 index 000000000..4cc75b9eb --- /dev/null +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/TransportGroupBatchSubscriber.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2020-2023 Exactpro (Exactpro Systems Limited) + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.exactpro.th2.common.schema.message.impl.rabbitmq.transport + +import com.exactpro.th2.common.metrics.BOOK_NAME_LABEL +import com.exactpro.th2.common.metrics.SESSION_GROUP_LABEL +import com.exactpro.th2.common.metrics.TH2_PIN_LABEL +import com.exactpro.th2.common.schema.message.ConfirmationListener +import com.exactpro.th2.common.schema.message.DeliveryMetadata +import com.exactpro.th2.common.schema.message.ManualAckDeliveryCallback +import com.exactpro.th2.common.schema.message.configuration.RouterFilter +import com.exactpro.th2.common.schema.message.impl.rabbitmq.AbstractRabbitSubscriber +import com.exactpro.th2.common.schema.message.impl.rabbitmq.connection.ConnectionManager +import com.exactpro.th2.common.schema.message.impl.rabbitmq.transport.TransportGroupBatchRouter.Companion.TRANSPORT_GROUP_TYPE +import com.rabbitmq.client.Delivery +import io.netty.buffer.Unpooled +import io.prometheus.client.Counter + +class TransportGroupBatchSubscriber( + connectionManager: ConnectionManager, + queue: String, + th2Pin: String, + private val filters: List, + messageListener: ConfirmationListener +) : AbstractRabbitSubscriber(connectionManager, queue, th2Pin, TRANSPORT_GROUP_TYPE, messageListener) { + + override fun valueFromBytes(body: ByteArray): GroupBatch = Unpooled.wrappedBuffer(body).run(GroupBatchCodec::decode) + + override fun toShortTraceString(value: GroupBatch): String = value.toString() + + override fun toShortDebugString(value: GroupBatch): String = value.toString() + + override fun filter(batch: GroupBatch): GroupBatch? = filters.filter(batch) + + override fun handle( + deliveryMetadata: DeliveryMetadata, + delivery: Delivery, + value: GroupBatch, + confirmation: ManualAckDeliveryCallback.Confirmation, + ) { + TRANSPORT_GROUP_SUBSCRIBE_TOTAL + .labels(th2Pin, value.book, value.sessionGroup) + .inc(value.groups.size.toDouble()) + super.handle(deliveryMetadata, delivery, value, confirmation) + } + + companion object { + private val TRANSPORT_GROUP_SUBSCRIBE_TOTAL = Counter.build() + .name("th2_transport_group_subscribe_total") + .labelNames(TH2_PIN_LABEL, BOOK_NAME_LABEL, SESSION_GROUP_LABEL) + .help("Quantity of received transport groups") + .register() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/TransportUtils.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/TransportUtils.kt new file mode 100644 index 000000000..9dec92e44 --- /dev/null +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/TransportUtils.kt @@ -0,0 +1,114 @@ +/* + * Copyright 2023 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.exactpro.th2.common.schema.message.impl.rabbitmq.transport + +import com.exactpro.th2.common.grpc.Direction.FIRST +import com.exactpro.th2.common.grpc.Direction.SECOND +import com.exactpro.th2.common.schema.filter.strategy.impl.AbstractTh2MsgFilterStrategy.BOOK_KEY +import com.exactpro.th2.common.schema.filter.strategy.impl.AbstractTh2MsgFilterStrategy.DIRECTION_KEY +import com.exactpro.th2.common.schema.filter.strategy.impl.AbstractTh2MsgFilterStrategy.MESSAGE_TYPE_KEY +import com.exactpro.th2.common.schema.filter.strategy.impl.AbstractTh2MsgFilterStrategy.PROTOCOL_KEY +import com.exactpro.th2.common.schema.filter.strategy.impl.AbstractTh2MsgFilterStrategy.SESSION_ALIAS_KEY +import com.exactpro.th2.common.schema.filter.strategy.impl.AbstractTh2MsgFilterStrategy.SESSION_GROUP_KEY +import com.exactpro.th2.common.schema.filter.strategy.impl.checkFieldValue +import com.exactpro.th2.common.schema.message.configuration.FieldFilterConfiguration +import com.exactpro.th2.common.schema.message.configuration.RouterFilter +import com.exactpro.th2.common.schema.message.impl.rabbitmq.transport.Direction.INCOMING +import com.exactpro.th2.common.schema.message.impl.rabbitmq.transport.Direction.OUTGOING +import io.netty.buffer.ByteBuf +import io.netty.buffer.Unpooled +import com.exactpro.th2.common.grpc.Direction as ProtoDirection + +fun Collection.filter(batch: GroupBatch): GroupBatch? { + if (isEmpty()) { + return batch + } + + forEach { filterSet -> + if (!filterSet.metadata[BOOK_KEY].verify(batch.book)) { + return@forEach + } + if (!filterSet.metadata[SESSION_GROUP_KEY].verify(batch.sessionGroup)) { + return@forEach + } + + if (!filterSet.metadata[SESSION_ALIAS_KEY].verify(batch.groups) { id.sessionAlias }) { + return@forEach + } + if (!filterSet.metadata[MESSAGE_TYPE_KEY].verify(batch.groups) { if (this is ParsedMessage) type else "" }) { + return@forEach + } + if (!filterSet.metadata[DIRECTION_KEY].verify(batch.groups) { id.direction.proto.name }) { + return@forEach + } + if (!filterSet.metadata[PROTOCOL_KEY].verify(batch.groups) { protocol }) { + return@forEach + } + + return batch + } + + return null +} + +private fun Collection?.verify(value: String): Boolean { + if (isNullOrEmpty()) { + return true + } + return all { it.checkFieldValue(value) } +} + +private inline fun Collection?.verify( + messageGroups: Collection, + value: Message<*>.() -> String +): Boolean { + if (isNullOrEmpty()) { + return true + } + + // Illegal cases when groups or messages are empty + if (messageGroups.isEmpty()) { + return false + } + val firstGroup = messageGroups.first() + if (firstGroup.messages.isEmpty()) { + return false + } + + return all { filter -> filter.checkFieldValue(firstGroup.messages.first().value()) } +} + +fun GroupBatch.toByteArray(): ByteArray = Unpooled.buffer().run { + GroupBatchCodec.encode(this@toByteArray, this@run) + ByteArray(readableBytes()).apply(::readBytes) +} + +fun ByteBuf.toByteArray(): ByteArray = ByteArray(readableBytes()) + .apply(::readBytes).also { resetReaderIndex() } + +val Direction.proto: ProtoDirection + get() = when (this) { + INCOMING -> FIRST + OUTGOING -> SECOND + } + +val ProtoDirection.transport: Direction + get() = when (this) { + FIRST -> INCOMING + SECOND -> OUTGOING + else -> error("Unsupported $this direction in the th2 transport protocol") + } diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/builders/CollectionBuilder.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/builders/CollectionBuilder.kt new file mode 100644 index 000000000..965feebcc --- /dev/null +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/builders/CollectionBuilder.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2023 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.exactpro.th2.common.schema.message.impl.rabbitmq.transport.builders + +class CollectionBuilder { + private val elements: MutableList = mutableListOf() + + val size: Int + get() = elements.size + + fun isEmpty(): Boolean = elements.isEmpty() + + operator fun get(index: Int): T = elements[index] + + fun add(el: T): CollectionBuilder = apply { + elements += el + } + + fun addAll(vararg els: T): CollectionBuilder = apply { + for (el in els) { + add(el) + } + } + + fun addAll(elements: Collection): CollectionBuilder = apply { + this.elements.addAll(elements) + } + + fun build(): List = elements +} \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/builders/MapBuilder.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/builders/MapBuilder.kt new file mode 100644 index 000000000..e30e5c328 --- /dev/null +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/builders/MapBuilder.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2023 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.exactpro.th2.common.schema.message.impl.rabbitmq.transport.builders + +class MapBuilder( + private val innerMap: MutableMap = hashMapOf(), +) { + val size: Int + get() = innerMap.size + + fun contains(key: K): Boolean = innerMap.contains(key) + operator fun get(key: K): V? = innerMap[key] + fun put(key: K, value: V): MapBuilder = apply { + innerMap[key] = value + } + + fun putAll(from: Map): MapBuilder = apply { + innerMap.putAll(from) + } + + fun build(): Map { + return innerMap + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/common/value/ValueUtils.kt b/src/main/kotlin/com/exactpro/th2/common/value/ValueUtils.kt index ebc87831d..0bddbfef1 100644 --- a/src/main/kotlin/com/exactpro/th2/common/value/ValueUtils.kt +++ b/src/main/kotlin/com/exactpro/th2/common/value/ValueUtils.kt @@ -15,6 +15,7 @@ */ @file:JvmName("ValueUtils") +@file:Suppress("unused") package com.exactpro.th2.common.value @@ -26,11 +27,12 @@ import com.exactpro.th2.common.grpc.NullValue.NULL_VALUE import com.exactpro.th2.common.grpc.Value import com.exactpro.th2.common.grpc.Value.KindCase.SIMPLE_VALUE import com.exactpro.th2.common.grpc.ValueOrBuilder +import com.exactpro.th2.common.message.addField import java.math.BigDecimal import java.math.BigInteger fun nullValue(): Value = Value.newBuilder().setNullValue(NULL_VALUE).build() -fun listValue() : ListValue.Builder = ListValue.newBuilder() +fun listValue(): ListValue.Builder = ListValue.newBuilder() fun Value.getString(): String? = takeIf { kindCase == SIMPLE_VALUE }?.simpleValue fun Value.getInt(): Int? = this.getString()?.toIntOrNull() @@ -39,34 +41,53 @@ fun Value.getDouble(): Double? = this.getString()?.toDoubleOrNull() fun Value.getBigInteger(): BigInteger? = this.getString()?.toBigIntegerOrNull() fun Value.getBigDecimal(): BigDecimal? = this.getString()?.toBigDecimalOrNull() fun Value.getMessage(): Message? = takeIf(Value::hasMessageValue)?.messageValue -fun Value.getList() : List? = takeIf(Value::hasListValue)?.listValue?.valuesList +fun Value.getList(): List? = takeIf(Value::hasListValue)?.listValue?.valuesList -fun Value.Builder.updateList(updateFunc: ListValue.Builder.() -> ListValueOrBuilder) : Value.Builder = apply { check(hasListValue()) { "Can not find list value" }; updateOrAddList(updateFunc) } -fun Value.Builder.updateString(updateFunc: String.() -> String) : ValueOrBuilder = apply { simpleValue = updateFunc(simpleValue ?: throw NullPointerException("Can not find simple value")) } -fun Value.Builder.updateMessage(updateFunc: Message.Builder.() -> MessageOrBuilder) : Value.Builder = apply { check(hasMessageValue()) { "Can not find message value" }; updateOrAddMessage(updateFunc) } +fun Value.Builder.updateList(updateFunc: ListValue.Builder.() -> ListValueOrBuilder): Value.Builder = + apply { check(hasListValue()) { "Can not find list value" }; updateOrAddList(updateFunc) } -fun Value.Builder.updateOrAddList(updateFunc: ListValue.Builder.() -> ListValueOrBuilder) : Value.Builder = apply { updateFunc(listValueBuilder).also { - when (it) { - is ListValue -> listValue = it - is ListValue.Builder -> setListValue(it) - else -> error("Can not set list value. Wrong type = ${it::class.java.canonicalName}") +fun Value.Builder.updateString(updateFunc: String.() -> String): ValueOrBuilder = + apply { simpleValue = updateFunc(simpleValue ?: throw NullPointerException("Can not find simple value")) } + +fun Value.Builder.updateMessage(updateFunc: Message.Builder.() -> MessageOrBuilder): Value.Builder = + apply { check(hasMessageValue()) { "Can not find message value" }; updateOrAddMessage(updateFunc) } + +fun Value.Builder.updateOrAddList(updateFunc: ListValue.Builder.() -> ListValueOrBuilder): Value.Builder = apply { + updateFunc(listValueBuilder).also { + when (it) { + is ListValue -> listValue = it + is ListValue.Builder -> setListValue(it) + else -> error("Can not set list value. Wrong type = ${it::class.java.canonicalName}") + } } -} } -fun Value.Builder.updateOrAddMessage(updateFunc: Message.Builder.() -> MessageOrBuilder) : Value.Builder = apply { updateFunc(messageValueBuilder).also { - when(it) { - is Message -> messageValue = it - is Message.Builder -> setMessageValue(it) - else -> error("Can not set message value. Wrong type = ${it::class.java.canonicalName}") +} + +fun Value.Builder.updateOrAddMessage(updateFunc: Message.Builder.() -> MessageOrBuilder): Value.Builder = apply { + updateFunc(messageValueBuilder).also { + when (it) { + is Message -> messageValue = it + is Message.Builder -> setMessageValue(it) + else -> error("Can not set message value. Wrong type = ${it::class.java.canonicalName}") + } } -} } -fun Value.Builder.updateOrAddString(updateFunc: String.() -> String) : Value.Builder = apply { updateFunc(simpleValue).also { simpleValue = it } } +} + +fun Value.Builder.updateOrAddString(updateFunc: String.() -> String): Value.Builder = + apply { updateFunc(simpleValue).also { simpleValue = it } } -fun ListValue.Builder.update(i: Int, updateFunc: Value.Builder.() -> ValueOrBuilder?): ListValue.Builder = apply { updateFunc(getValuesBuilder(i))?.let { setValues(i, it.toValue()) } } -fun ListValue.Builder.updateList(i: Int, updateFunc: ListValue.Builder.() -> ListValueOrBuilder) : ListValue.Builder = apply { getValuesBuilder(i).updateList(updateFunc)} -fun ListValue.Builder.updateString(i: Int, updateFunc: String.() -> String) : ListValue.Builder = apply { getValuesBuilder(i).updateString(updateFunc) } -fun ListValue.Builder.updateMessage(i: Int, updateFunc: Message.Builder.() -> MessageOrBuilder) : ListValue.Builder = apply { getValuesBuilder(i).updateMessage(updateFunc) } +fun ListValue.Builder.update(i: Int, updateFunc: Value.Builder.() -> ValueOrBuilder?): ListValue.Builder = + apply { updateFunc(getValuesBuilder(i))?.let { setValues(i, it.toValue()) } } -fun ListValue.Builder.updateOrAdd(i: Int, updateFunc: (Value.Builder?) -> ValueOrBuilder?) : ListValue.Builder = apply { +fun ListValue.Builder.updateList(i: Int, updateFunc: ListValue.Builder.() -> ListValueOrBuilder): ListValue.Builder = + apply { getValuesBuilder(i).updateList(updateFunc) } + +fun ListValue.Builder.updateString(i: Int, updateFunc: String.() -> String): ListValue.Builder = + apply { getValuesBuilder(i).updateString(updateFunc) } + +fun ListValue.Builder.updateMessage(i: Int, updateFunc: Message.Builder.() -> MessageOrBuilder): ListValue.Builder = + apply { getValuesBuilder(i).updateMessage(updateFunc) } + +fun ListValue.Builder.updateOrAdd(i: Int, updateFunc: (Value.Builder?) -> ValueOrBuilder?): ListValue.Builder = apply { updateFunc(if (i < valuesCount) getValuesBuilder(i) else null).also { while (i < valuesCount) { addValues(nullValue()) @@ -74,23 +95,36 @@ fun ListValue.Builder.updateOrAdd(i: Int, updateFunc: (Value.Builder?) -> ValueO add(i, it) } } -fun ListValue.Builder.updateOrAddList(i: Int, updateFunc: (ListValue.Builder?) -> ListValueOrBuilder) : ListValue.Builder = apply { updateOrAdd(i) { it?.updateOrAddList(updateFunc) ?: updateFunc(null)?.toValue() }} -fun ListValue.Builder.updateOrAddString(i: Int, updateFunc: (String?) -> String) : ListValue.Builder = apply { updateOrAdd(i) { it?.updateOrAddString(updateFunc) ?: updateFunc(null)?.toValue() } } -fun ListValue.Builder.updateOrAddMessage(i: Int, updateFunc: (Message.Builder?) -> MessageOrBuilder) : ListValue.Builder = apply { updateOrAdd(i) { it?.updateOrAddMessage(updateFunc) ?: updateFunc(null)?.toValue() }} -operator fun ListValueOrBuilder.get(i: Int) : Value = getValues(i) -operator fun ListValue.Builder.set(i: Int, value: Any?): ListValue.Builder = setValues(i, value?.toValue() ?: nullValue()) +fun ListValue.Builder.updateOrAddList( + i: Int, + updateFunc: (ListValue.Builder?) -> ListValueOrBuilder +): ListValue.Builder = apply { updateOrAdd(i) { it?.updateOrAddList(updateFunc) ?: updateFunc(null).toValue() } } + +fun ListValue.Builder.updateOrAddString(i: Int, updateFunc: (String?) -> String): ListValue.Builder = + apply { updateOrAdd(i) { it?.updateOrAddString(updateFunc) ?: updateFunc(null).toValue() } } -fun ListValue.Builder.add(value: Any?) : ListValue.Builder = apply { addValues(value?.toValue() ?: nullValue()) } -fun ListValue.Builder.add(i: Int, value: Any?) : ListValue.Builder = apply { addValues(i, value?.toValue() ?: nullValue()) } +fun ListValue.Builder.updateOrAddMessage( + i: Int, + updateFunc: (Message.Builder?) -> MessageOrBuilder +): ListValue.Builder = apply { updateOrAdd(i) { it?.updateOrAddMessage(updateFunc) ?: updateFunc(null).toValue() } } -fun Any.toValue(): Value = when (this) { +operator fun ListValueOrBuilder.get(i: Int): Value = getValues(i) +operator fun ListValue.Builder.set(i: Int, value: Any?): ListValue.Builder = + setValues(i, value?.toValue() ?: nullValue()) + +fun ListValue.Builder.add(value: Any?): ListValue.Builder = apply { addValues(value?.toValue() ?: nullValue()) } +fun ListValue.Builder.add(i: Int, value: Any?): ListValue.Builder = + apply { addValues(i, value?.toValue() ?: nullValue()) } + +fun Any?.toValue(): Value = when (this) { is Message -> toValue() is Message.Builder -> toValue() is ListValue -> toValue() is ListValue.Builder -> toValue() is Iterator<*> -> toValue() is Iterable<*> -> toValue() + is Map<*, *> -> toValue() is Array<*> -> toValue() is BooleanArray -> toValue() is ByteArray -> toValue() @@ -102,6 +136,7 @@ fun Any.toValue(): Value = when (this) { is DoubleArray -> toValue() is Value -> this is Value.Builder -> toValue() + null -> nullValue() else -> toString().toValue() } @@ -110,13 +145,19 @@ fun String.toValue(): Value = Value.newBuilder().setSimpleValue(this).build() fun Message.toValue(): Value = Value.newBuilder().setMessageValue(this).build() fun Message.Builder.toValue(): Value = Value.newBuilder().setMessageValue(this).build() -fun ListValue.toValue() : Value = Value.newBuilder().setListValue(this).build() -fun ListValue.Builder.toValue() : Value = Value.newBuilder().setListValue(this).build() +fun ListValue.toValue(): Value = Value.newBuilder().setListValue(this).build() +fun ListValue.Builder.toValue(): Value = Value.newBuilder().setListValue(this).build() -fun Value.Builder.toValue() : Value = this.build() +fun Value.Builder.toValue(): Value = this.build() fun Iterator<*>.toValue(): Value = toListValue().toValue() fun Iterable<*>.toValue(): Value = iterator().toValue() +fun Map<*, *>.toValue(): Value = Message.newBuilder().apply { + forEach { (key, value) -> + addField(key.toString(), value.toValue()) + } +}.toValue() + fun Array<*>.toValue(): Value = iterator().toValue() fun BooleanArray.toValue(): Value = toTypedArray().toValue() @@ -129,14 +170,14 @@ fun FloatArray.toValue(): Value = toTypedArray().toValue() fun DoubleArray.toValue(): Value = toTypedArray().toValue() -fun Iterator<*>.toListValue() : ListValue = listValue().also { list -> +fun Iterator<*>.toListValue(): ListValue = listValue().also { list -> forEach { - it?.toValue().run(list::addValues) + it.toValue().run(list::addValues) } }.build() -fun Iterable<*>.toListValue() : ListValue = iterator().toListValue() -fun Array<*>.toListValue() : ListValue = iterator().toListValue() +fun Iterable<*>.toListValue(): ListValue = iterator().toListValue() +fun Array<*>.toListValue(): ListValue = iterator().toListValue() fun BooleanArray.toListValue(): ListValue = toTypedArray().toListValue() fun ByteArray.toListValue(): ListValue = toTypedArray().toListValue() diff --git a/src/test/java/com/exactpro/th2/common/builder/MessageEventIdBuildersTest.java b/src/test/java/com/exactpro/th2/common/builder/MessageEventIdBuildersTest.java new file mode 100644 index 000000000..39a281674 --- /dev/null +++ b/src/test/java/com/exactpro/th2/common/builder/MessageEventIdBuildersTest.java @@ -0,0 +1,104 @@ +/* + * Copyright 2021-2021 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.exactpro.th2.common.builder; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import com.exactpro.th2.common.grpc.ConnectionID; +import com.exactpro.th2.common.grpc.Direction; +import com.exactpro.th2.common.grpc.EventID; +import com.exactpro.th2.common.grpc.MessageID; +import com.exactpro.th2.common.schema.factory.CommonFactory; + +import static com.exactpro.th2.common.message.MessageUtils.toJson; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class MessageEventIdBuildersTest { + private static final String CONFIG_DIRECTORY = "src/test/resources/test_message_event_id_builders"; + private static final String CUSTOM_BOOK = "custom_book"; + private static final String DEFAULT_ALIAS = "alias"; + private static final String DEFAULT_GROUP = "group"; + private static final Direction DEFAULT_DIRECTION = Direction.FIRST; + private static final int DEFAULT_SEQUENCE = 1; + private static final int DEFAULT_SUBSEQUENCE = 2; + private static final String DEFAULT_ID = "id"; + private static final String DEFAULT_SCOPE = "scope"; + + private CommonFactory commonFactory; + + @AfterEach + void tearDown() { + commonFactory.close(); + } + + @Test + public void testWithDefaultBookName() { + commonFactory = CommonFactory.createFromArguments("-c", ""); + assertIds(commonFactory.getBoxConfiguration().getBookName(), defaultMessageIdBuilder(), defaultEventIdBuilder()); + } + + @Test + public void testWithConfigBookName() { + commonFactory = CommonFactory.createFromArguments("-c", CONFIG_DIRECTORY); + assertIds("config_book", defaultMessageIdBuilder(), defaultEventIdBuilder()); + } + + @Test + public void testWithBookName() { + commonFactory = CommonFactory.createFromArguments("-c", CONFIG_DIRECTORY); + assertIds(CUSTOM_BOOK, defaultMessageIdBuilder().setBookName(CUSTOM_BOOK), defaultEventIdBuilder().setBookName(CUSTOM_BOOK)); + } + + private void assertIds(String bookName, MessageID.Builder messageIdBuilder, EventID.Builder eventIdBuilder) { + assertEquals( + "{\n" + + " \"connectionId\": {\n" + + " \"sessionAlias\": \"" + DEFAULT_ALIAS + "\",\n" + + " \"sessionGroup\": \"" + DEFAULT_GROUP + "\"\n" + + " },\n" + + " \"direction\": \"" + DEFAULT_DIRECTION.name() + "\",\n" + + " \"sequence\": \"" + DEFAULT_SEQUENCE + "\",\n" + + " \"subsequence\": [" + DEFAULT_SUBSEQUENCE + "],\n" + + " \"bookName\": \"" + bookName + "\"\n" + + "}", + toJson(messageIdBuilder.build(), false) + ); + assertEquals( + "{\n" + + " \"id\": \"" + DEFAULT_ID + "\",\n" + + " \"bookName\": \"" + bookName + "\",\n" + + " \"scope\": \"" + DEFAULT_SCOPE + "\"\n" + + "}", + toJson(eventIdBuilder.build(), false) + ); + } + + private MessageID.Builder defaultMessageIdBuilder() { + return commonFactory.newMessageIDBuilder() + .setConnectionId(ConnectionID.newBuilder().setSessionAlias(DEFAULT_ALIAS).setSessionGroup(DEFAULT_GROUP)) + .setDirection(DEFAULT_DIRECTION) + .setSequence(DEFAULT_SEQUENCE) + .addSubsequence(DEFAULT_SUBSEQUENCE); + } + + private EventID.Builder defaultEventIdBuilder() { + return commonFactory.newEventIDBuilder() + .setId(DEFAULT_ID) + .setScope(DEFAULT_SCOPE); + } +} diff --git a/src/test/java/com/exactpro/th2/common/event/bean/BaseTest.java b/src/test/java/com/exactpro/th2/common/event/bean/BaseTest.java index 19fa758a0..067d50333 100644 --- a/src/test/java/com/exactpro/th2/common/event/bean/BaseTest.java +++ b/src/test/java/com/exactpro/th2/common/event/bean/BaseTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2020 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2023 Exactpro (Exactpro Systems Limited) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,15 +15,25 @@ */ package com.exactpro.th2.common.event.bean; +import com.exactpro.th2.common.grpc.EventID; +import com.exactpro.th2.common.schema.box.configuration.BoxConfiguration; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; + import org.junit.jupiter.api.Assertions; import java.io.IOException; +import java.time.Instant; +import static com.exactpro.th2.common.event.EventUtils.toEventID; import static com.fasterxml.jackson.module.kotlin.ExtensionsKt.jacksonObjectMapper; public class BaseTest { + public static final BoxConfiguration BOX_CONFIGURATION = new BoxConfiguration(); + public static final String BOOK_NAME = BOX_CONFIGURATION.getBookName(); + public static final String SESSION_GROUP = "test-group"; + public static final String SESSION_ALIAS = "test-alias"; + public static final EventID PARENT_EVENT_ID = toEventID(Instant.now(), BOOK_NAME, "id"); private static final ObjectMapper jacksonMapper = jacksonObjectMapper(); diff --git a/src/test/java/com/exactpro/th2/common/event/bean/MessageTest.java b/src/test/java/com/exactpro/th2/common/event/bean/MessageTest.java index e01cd7cf3..15992c8f5 100644 --- a/src/test/java/com/exactpro/th2/common/event/bean/MessageTest.java +++ b/src/test/java/com/exactpro/th2/common/event/bean/MessageTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2020 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2021 Exactpro (Exactpro Systems Limited) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ import com.exactpro.th2.common.event.Event; import com.exactpro.th2.common.event.bean.builder.MessageBuilder; + import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -29,8 +30,10 @@ public void testSerializationMessage() throws IOException { Message message = new MessageBuilder().text("My message for report") .build(); - com.exactpro.th2.common.grpc.Event event = - Event.start().bodyData(message).toProtoEvent("id"); + com.exactpro.th2.common.grpc.Event event = Event + .start() + .bodyData(message) + .toProto(PARENT_EVENT_ID); String expectedJson = "[\n" + " {\n" + @@ -48,5 +51,4 @@ public void testSerializationMessageNullBody() { new MessageBuilder().text(null).build(); }); } - } diff --git a/src/test/java/com/exactpro/th2/common/event/bean/TableTest.java b/src/test/java/com/exactpro/th2/common/event/bean/TableTest.java index 1d6d33d98..87be2e47d 100644 --- a/src/test/java/com/exactpro/th2/common/event/bean/TableTest.java +++ b/src/test/java/com/exactpro/th2/common/event/bean/TableTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2020 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2021 Exactpro (Exactpro Systems Limited) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ import com.exactpro.th2.common.event.Event; import com.exactpro.th2.common.event.bean.builder.TableBuilder; + import org.junit.jupiter.api.Test; import java.io.IOException; @@ -39,8 +40,10 @@ public void testSerializationTable() throws IOException { TableBuilder tableBuilder = new TableBuilder<>(); Table table = tableBuilder.row(row1) .row(row2).build(); - com.exactpro.th2.common.grpc.Event event = - Event.start().bodyData(table).toProtoEvent("id"); + com.exactpro.th2.common.grpc.Event event = Event + .start() + .bodyData(table) + .toProto(PARENT_EVENT_ID); String expectedJson = "[\n" + " {\n" + @@ -60,5 +63,4 @@ public void testSerializationTable() throws IOException { assertCompareBytesAndJson(event.getBody().toByteArray(), expectedJson); } - } diff --git a/src/test/java/com/exactpro/th2/common/event/bean/TestVerification.java b/src/test/java/com/exactpro/th2/common/event/bean/TestVerification.java index fff144af9..1d47494e9 100644 --- a/src/test/java/com/exactpro/th2/common/event/bean/TestVerification.java +++ b/src/test/java/com/exactpro/th2/common/event/bean/TestVerification.java @@ -16,6 +16,7 @@ package com.exactpro.th2.common.event.bean; import com.exactpro.th2.common.event.Event; + import org.junit.jupiter.api.Test; import java.io.IOException; @@ -40,8 +41,10 @@ public void testSerializationSimpleVerification() throws IOException { put("Field A", verificationEntry); }}); - com.exactpro.th2.common.grpc.Event event = - Event.start().bodyData(verification).toProtoEvent("id"); + com.exactpro.th2.common.grpc.Event event = Event + .start() + .bodyData(verification) + .toProto(PARENT_EVENT_ID); String expectedJson = "[\n" + " {\n" + @@ -92,8 +95,10 @@ public void testSerializationRecursiveVerification() throws IOException { put("Sub message A", verificationEntry); }}); - com.exactpro.th2.common.grpc.Event event = - Event.start().bodyData(verification).toProtoEvent("id"); + com.exactpro.th2.common.grpc.Event event = Event + .start() + .bodyData(verification) + .toProto(PARENT_EVENT_ID); String expectedJson = "[\n" + " {\n" + diff --git a/src/test/java/com/exactpro/th2/common/event/bean/TreeTableTest.java b/src/test/java/com/exactpro/th2/common/event/bean/TreeTableTest.java index 7a9e998a1..5a45e6e2c 100644 --- a/src/test/java/com/exactpro/th2/common/event/bean/TreeTableTest.java +++ b/src/test/java/com/exactpro/th2/common/event/bean/TreeTableTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2020 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2021 Exactpro (Exactpro Systems Limited) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,12 +19,12 @@ import com.exactpro.th2.common.event.bean.builder.CollectionBuilder; import com.exactpro.th2.common.event.bean.builder.RowBuilder; import com.exactpro.th2.common.event.bean.builder.TreeTableBuilder; + import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import java.io.IOException; - public class TreeTableTest extends BaseTest { @Test @@ -38,8 +38,10 @@ public void testSerializationRow() throws IOException { TreeTableBuilder treeTableBuilder = new TreeTableBuilder(); TreeTable treeTable = treeTableBuilder.row("FirstRow", row).build(); - com.exactpro.th2.common.grpc.Event event = - Event.start().bodyData(treeTable).toProtoEvent("id"); + com.exactpro.th2.common.grpc.Event event = Event + .start() + .bodyData(treeTable) + .toProto(PARENT_EVENT_ID); String expectedJson = "[{\n" + " \"type\": \"treeTable\",\n" + @@ -80,8 +82,10 @@ public void testSerializationCollection() throws IOException { TreeTableBuilder treeTableBuilder = new TreeTableBuilder(); TreeTable treeTable = treeTableBuilder.row("Row B with some other name", collection).build(); - com.exactpro.th2.common.grpc.Event event = - Event.start().bodyData(treeTable).toProtoEvent("id"); + com.exactpro.th2.common.grpc.Event event = Event + .start() + .bodyData(treeTable) + .toProto(PARENT_EVENT_ID); String expectedJson = "[ {\"type\": \"treeTable\",\n" + " \"rows\": {" + @@ -141,8 +145,10 @@ public void testSerializationHybrid() throws IOException { .row("FirstRow", row) .build(); - com.exactpro.th2.common.grpc.Event event = - Event.start().bodyData(treeTable).toProtoEvent("id"); + com.exactpro.th2.common.grpc.Event event = Event + .start() + .bodyData(treeTable) + .toProto(PARENT_EVENT_ID); String expectedJson = "[ {\"type\": \"treeTable\",\n" + " \"rows\": {" + @@ -192,8 +198,10 @@ public void testSerializationRecursive() throws IOException { TreeTableBuilder treeTableBuilder = new TreeTableBuilder(); TreeTable treeTable = treeTableBuilder.row("Row B with some other name", collection).build(); - com.exactpro.th2.common.grpc.Event event = - Event.start().bodyData(treeTable).toProtoEvent("id"); + com.exactpro.th2.common.grpc.Event event = Event + .start() + .bodyData(treeTable) + .toProto(PARENT_EVENT_ID); String expectedJson = "[ {\"type\": \"treeTable\",\n" + " \"rows\": {" + diff --git a/src/test/kotlin/com/exactpro/th2/common/event/TestEvent.kt b/src/test/kotlin/com/exactpro/th2/common/event/TestEvent.kt index 8e25c8fed..3bcbacc87 100644 --- a/src/test/kotlin/com/exactpro/th2/common/event/TestEvent.kt +++ b/src/test/kotlin/com/exactpro/th2/common/event/TestEvent.kt @@ -18,6 +18,7 @@ package com.exactpro.th2.common.event import com.exactpro.th2.common.event.Event.UNKNOWN_EVENT_NAME import com.exactpro.th2.common.event.Event.UNKNOWN_EVENT_TYPE import com.exactpro.th2.common.event.EventUtils.toEventID +import com.exactpro.th2.common.event.bean.BaseTest.BOOK_NAME import com.exactpro.th2.common.grpc.EventBatch import com.exactpro.th2.common.grpc.EventID import com.exactpro.th2.common.grpc.EventStatus.FAILED @@ -30,19 +31,23 @@ import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertAll +import java.time.Instant +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime +import java.time.ZoneOffset typealias ProtoEvent = com.exactpro.th2.common.grpc.Event class TestEvent { - - private val parentEventId: EventID = toEventID("parentEventId")!! + private val parentEventId: EventID = toEventID(Instant.now(), BOOK_NAME, "parentEventId") private val data = EventUtils.createMessageBean("0123456789".repeat(20)) private val dataSize = MAPPER.writeValueAsBytes(listOf(data)).size private val bigData = EventUtils.createMessageBean("0123456789".repeat(30)) @Test fun `call the toProto method on a simple event`() { - Event.start().toProto(null).run { + Event.start().toProto(BOOK_NAME).run { checkDefaultEventFields() assertFalse(hasParentId()) } @@ -56,9 +61,8 @@ class TestEvent { @Test fun `set parent to the toListProto method`() { val event = Event.start() - val toListProtoWithParent = event.toListProto(parentEventId) - val toListProtoWithoutParent = event.toListProto(null) + val toListProtoWithoutParent = event.toListProto(BOOK_NAME) assertAll( { assertEquals(1, toListProtoWithParent.size) }, { assertEquals(1, toListProtoWithoutParent.size) }, @@ -193,17 +197,17 @@ class TestEvent { fun `root event to list batch proto with size limit`() { val rootName = "root" val childName = "child" - val rootEvent = Event.start().apply { - name = rootName - bodyData(data).apply { - addSubEventWithSamePeriod().apply { - name = childName - bodyData(data) + val rootEvent = Event.start().also { + it.name = rootName + it.bodyData(data).apply { + addSubEventWithSamePeriod().also { subEvent -> + subEvent.name = childName + subEvent.bodyData(data) } } } - val batches = rootEvent.toBatchesProtoWithLimit(dataSize, null) + val batches = rootEvent.toBatchesProtoWithLimit(dataSize, BOOK_NAME) assertEquals(2, batches.size) checkEventStatus(batches, 2, 0) @@ -215,17 +219,17 @@ class TestEvent { fun `root event to list batch proto without size limit`() { val rootName = "root" val childName = "child" - val rootEvent = Event.start().apply { - name = rootName - bodyData(data).apply { - addSubEventWithSamePeriod().apply { - name = childName - bodyData(data) + val rootEvent = Event.start().also { + it.name = rootName + it.bodyData(data).apply { + addSubEventWithSamePeriod().also { subEvent -> + subEvent.name = childName + subEvent.bodyData(data) } } } - val batch = rootEvent.toBatchProto(null) + val batch = rootEvent.toBatchProto(BOOK_NAME) checkEventStatus(listOf(batch), 2, 0) batch.checkEventBatch(false, listOf(rootName, childName)) } @@ -268,19 +272,44 @@ class TestEvent { @Test fun `pack single event single batch`() { - val rootEvent = Event.start() - - val batch = rootEvent.toBatchProto(parentEventId) + val batch = Event.start().toBatchProto(parentEventId) assertFalse(batch.hasParentEventId()) checkEventStatus(listOf(batch), 1, 0) } + @Test + fun `serializes date time fields`() { + class TestBody( + val instant: Instant, + val dateTime: LocalDateTime, + val date: LocalDate, + val time: LocalTime, + ) : IBodyData + + // Friday, 13 October 2023 y., 12:35:05 + val instant = Instant.ofEpochSecond(1697200505) + val protoEvent = Event.start().endTimestamp() + .bodyData( + TestBody( + instant = instant, + dateTime = LocalDateTime.ofInstant(instant, ZoneOffset.UTC), + date = LocalDate.ofInstant(instant, ZoneOffset.UTC), + time = LocalTime.ofInstant(instant, ZoneOffset.UTC), + ) + ).toProto(parentEventId) + val jsonBody = protoEvent.body.toStringUtf8() + assertEquals( + """[{"instant":"2023-10-13T12:35:05Z","dateTime":"2023-10-13T12:35:05","date":"2023-10-13","time":"12:35:05"}]""", + jsonBody, + "unexpected JSON body", + ) + } + private fun com.exactpro.th2.common.grpc.Event.checkDefaultEventFields() { assertAll( { assertTrue(hasId()) }, { assertEquals(UNKNOWN_EVENT_NAME, name) }, { assertEquals(UNKNOWN_EVENT_TYPE, type) }, - { assertTrue(hasStartTimestamp()) }, { assertTrue(hasEndTimestamp()) }, { assertEquals(SUCCESS, status) }, { assertEquals(ByteString.copyFrom("[]".toByteArray()), body) }, diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/TestJsonConfiguration.kt b/src/test/kotlin/com/exactpro/th2/common/schema/TestJsonConfiguration.kt index 16a5857b4..cfa9efb5d 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/TestJsonConfiguration.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/TestJsonConfiguration.kt @@ -15,28 +15,20 @@ package com.exactpro.th2.common.schema +import com.exactpro.cradle.cassandra.CassandraStorageSettings import com.exactpro.th2.common.metrics.PrometheusConfiguration import com.exactpro.th2.common.schema.cradle.CradleConfidentialConfiguration import com.exactpro.th2.common.schema.cradle.CradleNonConfidentialConfiguration -import com.exactpro.th2.common.schema.grpc.configuration.GrpcConfiguration -import com.exactpro.th2.common.schema.grpc.configuration.GrpcEndpointConfiguration -import com.exactpro.th2.common.schema.grpc.configuration.GrpcRawRobinStrategy -import com.exactpro.th2.common.schema.grpc.configuration.GrpcServerConfiguration -import com.exactpro.th2.common.schema.grpc.configuration.GrpcServiceConfiguration -import com.exactpro.th2.common.schema.message.configuration.FieldFilterConfiguration -import com.exactpro.th2.common.schema.message.configuration.FieldFilterOperation -import com.exactpro.th2.common.schema.message.configuration.MessageRouterConfiguration -import com.exactpro.th2.common.schema.message.configuration.MqRouterFilterConfiguration -import com.exactpro.th2.common.schema.message.configuration.QueueConfiguration +import com.exactpro.th2.common.schema.factory.AbstractCommonFactory.MAPPER +import com.exactpro.th2.common.schema.grpc.configuration.* +import com.exactpro.th2.common.schema.message.configuration.* import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.ConnectionManagerConfiguration import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.RabbitMQConfiguration import com.exactpro.th2.common.schema.strategy.route.impl.RobinRoutingStrategy -import com.exactpro.th2.common.schema.strategy.route.json.RoutingStrategyModule -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule -import com.fasterxml.jackson.module.kotlin.KotlinModule import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test +import org.testcontainers.shaded.org.apache.commons.lang3.builder.EqualsBuilder import java.nio.file.Path class TestJsonConfiguration { @@ -96,11 +88,22 @@ class TestJsonConfiguration { testDeserialize(CRADLE_NON_CONFIDENTIAL_CONF_JSON, CRADLE_NON_CONFIDENTIAL_CONF) } + @Test + fun `test cassandra storage settings json configuration deserialize`() { + assertTrue(EqualsBuilder.reflectionEquals(MAPPER.readValue(CASSANDRA_STORAGE_SETTINGS_JSON, CASSANDRA_STORAGE_SETTINGS::class.java), CASSANDRA_STORAGE_SETTINGS)) + } + @Test fun `test cradle non confidential json configuration serialize and deserialize`() { testSerializeAndDeserialize(CRADLE_NON_CONFIDENTIAL_CONF) } + @Test + fun `test json configuration deserialize combo`() { + assertTrue(EqualsBuilder.reflectionEquals(CASSANDRA_STORAGE_SETTINGS, MAPPER.readValue(CRADLE_NON_CONFIDENTIAL_COMBO_CONF_JSON, CASSANDRA_STORAGE_SETTINGS::class.java))) + assertEquals(CRADLE_NON_CONFIDENTIAL_CONF, MAPPER.readValue(CRADLE_NON_CONFIDENTIAL_COMBO_CONF_JSON, CRADLE_NON_CONFIDENTIAL_CONF::class.java)) + } + @Test fun `test prometheus confidential json configuration deserialize`() { testDeserialize(PROMETHEUS_CONF_JSON, PROMETHEUS_CONF) @@ -112,20 +115,16 @@ class TestJsonConfiguration { } private fun testSerializeAndDeserialize(configuration: Any) { - OBJECT_MAPPER.writeValueAsString(configuration).also { jsonString -> + MAPPER.writeValueAsString(configuration).also { jsonString -> testDeserialize(jsonString, configuration) } } private fun testDeserialize(json: String, obj: Any) { - assertEquals(obj, OBJECT_MAPPER.readValue(json, obj::class.java)) + assertEquals(obj, MAPPER.readValue(json, obj::class.java)) } companion object { - @JvmStatic - private val OBJECT_MAPPER: ObjectMapper = ObjectMapper() - .registerModule(JavaTimeModule()) - @JvmStatic private val CONF_DIR = Path.of("test_json_configurations") @@ -137,11 +136,11 @@ class TestJsonConfiguration { init(GrpcRawRobinStrategy(listOf("endpoint"))) }, GrpcConfiguration::class.java, - mapOf("endpoint" to GrpcEndpointConfiguration("host", 12345, listOf("test_attr"))), + mapOf("endpoint" to GrpcEndpointConfiguration("host", 12345, attributes = listOf("test_attr"))), emptyList() ) ), - GrpcServerConfiguration("host123", 1234, 58) + GrpcServerConfiguration("host123", 1234, 58), ) private val RABBITMQ_CONF_JSON = loadConfJson("rabbitMq") @@ -199,7 +198,8 @@ class TestJsonConfiguration { ) ) ) - }) + }), + GlobalNotificationConfiguration() ) private val CRADLE_CONFIDENTIAL_CONF_JSON = loadConfJson("cradle_confidential") @@ -209,28 +209,51 @@ class TestJsonConfiguration { "keyspace", 1234, "user", - "pass", - "instance" + "pass" ) + private val CRADLE_NON_CONFIDENTIAL_COMBO_CONF_JSON = loadConfJson("cradle_non_confidential_combo") private val CRADLE_NON_CONFIDENTIAL_CONF_JSON = loadConfJson("cradle_non_confidential") private val CRADLE_NON_CONFIDENTIAL_CONF = CradleNonConfidentialConfiguration( - 888, + false, 111, 123, 321, - false + 5000, + 1280002, ) + private val CASSANDRA_STORAGE_SETTINGS_JSON = loadConfJson("cassandra_storage_settings") + private val CASSANDRA_STORAGE_SETTINGS = CassandraStorageSettings().apply { +// networkTopologyStrategy = NetworkTopologyStrategyBuilder() +// .add("A", 3) +// .add("B", 3) +// .build() + timeout = 4999 +// writeConsistencyLevel = ConsistencyLevel.THREE +// readConsistencyLevel = ConsistencyLevel.QUORUM + keyspace = "test-keyspace" + keyspaceReplicationFactor = 1 + maxParallelQueries = 1 + resultPageSize = 2 + maxMessageBatchSize = 3 + maxUncompressedMessageBatchSize = 5 + maxTestEventBatchSize = 8 + maxUncompressedTestEventSize = 13 + sessionsCacheSize = 21 + scopesCacheSize = 34 + pageSessionsCacheSize = 55 + pageScopesCacheSize = 89 + sessionStatisticsCacheSize = 144 + pageGroupsCacheSize = 233 + groupsCacheSize = 377 + eventBatchDurationCacheSize = 610 + counterPersistenceInterval = 987 + composingServiceThreads = 1597 + } private val PROMETHEUS_CONF_JSON = loadConfJson("prometheus") private val PROMETHEUS_CONF = PrometheusConfiguration("123.3.3.3", 1234, false) - init { - OBJECT_MAPPER.registerModule(KotlinModule()) - - OBJECT_MAPPER.registerModule(RoutingStrategyModule(OBJECT_MAPPER)) - } - private fun loadConfJson(fileName: String): String { val path = CONF_DIR.resolve(fileName) diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/factory/CommonFactoryTest.kt b/src/test/kotlin/com/exactpro/th2/common/schema/factory/CommonFactoryTest.kt new file mode 100644 index 000000000..c918f8879 --- /dev/null +++ b/src/test/kotlin/com/exactpro/th2/common/schema/factory/CommonFactoryTest.kt @@ -0,0 +1,123 @@ +/* + * Copyright 2023 Exactpro (Exactpro Systems Limited) + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.exactpro.th2.common.schema.factory + +import com.exactpro.cradle.cassandra.CassandraStorageSettings +import com.exactpro.th2.common.metrics.PrometheusConfiguration +import com.exactpro.th2.common.schema.box.configuration.BoxConfiguration +import com.exactpro.th2.common.schema.cradle.CradleConfidentialConfiguration +import com.exactpro.th2.common.schema.cradle.CradleNonConfidentialConfiguration +import com.exactpro.th2.common.schema.factory.CommonFactory.CONFIG_DEFAULT_PATH +import com.exactpro.th2.common.schema.factory.CommonFactory.CUSTOM_FILE_NAME +import com.exactpro.th2.common.schema.factory.CommonFactory.DICTIONARY_ALIAS_DIR_NAME +import com.exactpro.th2.common.schema.factory.CommonFactory.DICTIONARY_TYPE_DIR_NAME +import com.exactpro.th2.common.schema.factory.CommonFactory.TH2_COMMON_CONFIGURATION_DIRECTORY_SYSTEM_PROPERTY +import com.exactpro.th2.common.schema.grpc.configuration.GrpcConfiguration +import com.exactpro.th2.common.schema.grpc.configuration.GrpcRouterConfiguration +import com.exactpro.th2.common.schema.message.configuration.MessageRouterConfiguration +import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.ConnectionManagerConfiguration +import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.RabbitMQConfiguration +import org.junit.jupiter.api.Test +import org.junitpioneer.jupiter.SetSystemProperty +import java.nio.file.Path +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + + +class CommonFactoryTest { + + @Test + fun `test load config by default path (default constructor)`() { + CommonFactory().use { commonFactory -> + assertConfigs(commonFactory, CONFIG_DEFAULT_PATH) + } + } + + @Test + fun `test load config by default path (createFromArguments(empty))`() { + CommonFactory.createFromArguments().use { commonFactory -> + assertConfigs(commonFactory, CONFIG_DEFAULT_PATH) + } + } + + @Test + fun `test load config by custom path (createFromArguments(not empty))`() { + CommonFactory.createFromArguments("-c", CONFIG_DIR_IN_RESOURCE).use { commonFactory -> + assertConfigs(commonFactory, Path.of(CONFIG_DIR_IN_RESOURCE)) + } + } + + @Test + @SetSystemProperty(key = TH2_COMMON_CONFIGURATION_DIRECTORY_SYSTEM_PROPERTY, value = CONFIG_DIR_IN_RESOURCE) + fun `test load config by environment variable path (default constructor)`() { + CommonFactory().use { commonFactory -> + assertConfigs(commonFactory, Path.of(CONFIG_DIR_IN_RESOURCE)) + } + } + + @Test + @SetSystemProperty(key = TH2_COMMON_CONFIGURATION_DIRECTORY_SYSTEM_PROPERTY, value = CONFIG_DIR_IN_RESOURCE) + fun `test load config by environment variable path (createFromArguments(empty))`() { + CommonFactory.createFromArguments().use { commonFactory -> + assertConfigs(commonFactory, Path.of(CONFIG_DIR_IN_RESOURCE)) + } + } + + @Test + @SetSystemProperty(key = TH2_COMMON_CONFIGURATION_DIRECTORY_SYSTEM_PROPERTY, value = CONFIG_DIR_IN_RESOURCE) + fun `test load config by custom path (createFromArguments(not empty) + environment variable)`() { + CommonFactory.createFromArguments("-c", CONFIG_DIR_IN_RESOURCE).use { commonFactory -> + assertConfigs(commonFactory, Path.of(CONFIG_DIR_IN_RESOURCE)) + } + } + + + private fun assertConfigs(commonFactory: CommonFactory, configPath: Path) { + CONFIG_NAME_TO_COMMON_FACTORY_SUPPLIER.forEach { (configName, actualPathSupplier) -> + assertEquals(configPath.resolve(configName), commonFactory.actualPathSupplier(), "Configured config path: $configPath, config name: $configName") + } + assertConfigurationManager(commonFactory, configPath) + } + + private fun assertConfigurationManager(commonFactory: CommonFactory, configPath: Path) { + CONFIG_CLASSES.forEach { clazz -> + assertNotNull(commonFactory.configurationManager[clazz]) + assertEquals(configPath, commonFactory.configurationManager[clazz]?.parent , "Configured config path: $configPath, config class: $clazz") + } + } + + companion object { + private const val CONFIG_DIR_IN_RESOURCE = "src/test/resources/test_common_factory_load_configs" + + private val CONFIG_NAME_TO_COMMON_FACTORY_SUPPLIER: Map Path> = mapOf( + CUSTOM_FILE_NAME to { pathToCustomConfiguration }, + DICTIONARY_ALIAS_DIR_NAME to { pathToDictionaryAliasesDir }, + DICTIONARY_TYPE_DIR_NAME to { pathToDictionaryTypesDir }, + ) + + private val CONFIG_CLASSES: Set> = setOf( + RabbitMQConfiguration::class.java, + MessageRouterConfiguration::class.java, + ConnectionManagerConfiguration::class.java, + GrpcConfiguration::class.java, + GrpcRouterConfiguration::class.java, + CradleConfidentialConfiguration::class.java, + CradleNonConfidentialConfiguration::class.java, + CassandraStorageSettings::class.java, + PrometheusConfiguration::class.java, + BoxConfiguration::class.java, + ) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/filter/strategy/impl/TestAnyMessageFilterStrategy.kt b/src/test/kotlin/com/exactpro/th2/common/schema/filter/strategy/impl/TestAnyMessageFilterStrategy.kt index be2cb2a9a..ce36fb7be 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/filter/strategy/impl/TestAnyMessageFilterStrategy.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/filter/strategy/impl/TestAnyMessageFilterStrategy.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Exactpro (Exactpro Systems Limited) + * Copyright 2021-2023 Exactpro (Exactpro Systems Limited) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,31 +16,181 @@ package com.exactpro.th2.common.schema.filter.strategy.impl +import com.exactpro.th2.common.event.bean.BaseTest.BOOK_NAME import com.exactpro.th2.common.grpc.AnyMessage import com.exactpro.th2.common.grpc.Direction import com.exactpro.th2.common.grpc.RawMessage +import com.exactpro.th2.common.message.addField import com.exactpro.th2.common.message.message import com.exactpro.th2.common.message.toJson +import com.exactpro.th2.common.schema.filter.strategy.impl.AnyMessageFilterStrategy.verify import com.exactpro.th2.common.schema.message.configuration.FieldFilterConfiguration import com.exactpro.th2.common.schema.message.configuration.FieldFilterOperation import com.exactpro.th2.common.schema.message.configuration.MqRouterFilterConfiguration import org.apache.commons.collections4.MultiMapUtils -import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.Arguments.arguments import org.junit.jupiter.params.provider.MethodSource -import org.junit.jupiter.params.provider.ValueSource class TestAnyMessageFilterStrategy { - private val strategy = AnyMessageFilterStrategy() + + @ParameterizedTest + @MethodSource("multipleFiltersMatch") + fun `matches any filter`(anyMessage: AnyMessage, expectMatch: Boolean) { + val match = verify( + anyMessage, + listOf( + MqRouterFilterConfiguration( + metadata = MultiMapUtils.newListValuedHashMap().apply { + put("message_type", FieldFilterConfiguration( + fieldName = "message_type", + operation = FieldFilterOperation.EQUAL, + expectedValue = "test" + )) + } + ), + MqRouterFilterConfiguration( + metadata = MultiMapUtils.newListValuedHashMap().apply { + put("direction", FieldFilterConfiguration( + fieldName = "direction", + operation = FieldFilterOperation.EQUAL, + expectedValue = "FIRST" + )) + } + ) + ) + ) + assertEquals(expectMatch, match) { "The message ${anyMessage.toJson()} was${if (expectMatch) "" else " not"} matched" } + } + + @ParameterizedTest + @MethodSource("rawMessagesBothFilters") + fun `matches with multiple metadata filters`(anyMessage: AnyMessage, expectMatch: Boolean) { + val match = verify( + anyMessage, + MqRouterFilterConfiguration( + metadata = MultiMapUtils.newListValuedHashMap().apply { + put("session_alias", FieldFilterConfiguration( + fieldName = "session_alias", + operation = FieldFilterOperation.EQUAL, + expectedValue = "test-alias" + )) + put("direction", FieldFilterConfiguration( + fieldName = "direction", + operation = FieldFilterOperation.EQUAL, + expectedValue = "FIRST" + )) + } + ) + ) + assertEquals(expectMatch, match) { "The message ${anyMessage.toJson()} was${if (expectMatch) "" else " not"} matched" } + } + + @ParameterizedTest + @MethodSource("parsedMessagesBothFilters") + fun `matches with multiple message filters`(anyMessage: AnyMessage, expectMatch: Boolean) { + val match = verify( + anyMessage, + MqRouterFilterConfiguration( + message = MultiMapUtils.newListValuedHashMap().apply { + put("test-field1", FieldFilterConfiguration( + fieldName = "test-field1", + operation = FieldFilterOperation.EQUAL, + expectedValue = "test-value1" + )) + put("test-field2", FieldFilterConfiguration( + fieldName = "test-field2", + operation = FieldFilterOperation.EQUAL, + expectedValue = "test-value2" + )) + } + ) + ) + assertEquals(expectMatch, match) { "The message ${anyMessage.toJson()} was${if (expectMatch) "" else " not"} matched" } + } + + @ParameterizedTest + @MethodSource("messagesWithProperties") + fun `matches with multiple properties filters`(anyMessage: AnyMessage, expectMatch: Boolean) { + val match = verify( + anyMessage, + MqRouterFilterConfiguration( + metadata = MultiMapUtils.newListValuedHashMap().apply { + put("prop-field1", FieldFilterConfiguration( + fieldName = "prop-field1", + operation = FieldFilterOperation.EQUAL, + expectedValue = "prop-value1" + )) + put("prop-field2", FieldFilterConfiguration( + fieldName = "prop-field2", + operation = FieldFilterOperation.EQUAL, + expectedValue = "prop-value2" + )) + } + ) + ) + assertEquals(expectMatch, match) { "The message ${anyMessage.toJson()} was${if (expectMatch) "" else " not"} matched" } + } + + @ParameterizedTest + @MethodSource("messagesWithMessageAndMetadataFilters") + fun `matches with multiple message and metadata filters`(anyMessage: AnyMessage, expectMatch: Boolean) { + val match = verify( + anyMessage, + MqRouterFilterConfiguration( + message = MultiMapUtils.newListValuedHashMap().apply { + put("test-field1", FieldFilterConfiguration( + fieldName = "test-field1", + operation = FieldFilterOperation.EQUAL, + expectedValue = "test-value1" + )) + put("test-field2", FieldFilterConfiguration( + fieldName = "test-field2", + operation = FieldFilterOperation.EQUAL, + expectedValue = "test-value2" + )) + }, + metadata = MultiMapUtils.newListValuedHashMap().apply { + put("message_type", FieldFilterConfiguration( + fieldName = "message_type", + operation = FieldFilterOperation.EQUAL, + expectedValue = "test" + )) + put("direction", FieldFilterConfiguration( + fieldName = "direction", + operation = FieldFilterOperation.EQUAL, + expectedValue = "FIRST" + )) + } + ) + ) + assertEquals(expectMatch, match) { "The message ${anyMessage.toJson()} was${if (expectMatch) "" else " not"} matched" } + } + + @ParameterizedTest + @MethodSource("messageWithProtocol") + fun `matches protocol metadata filter`(anyMessage: AnyMessage, expectMatch: Boolean) { + val match = verify( + anyMessage, + MqRouterFilterConfiguration( + metadata = MultiMapUtils.newListValuedHashMap().apply { + put("protocol", FieldFilterConfiguration( + fieldName = "protocol", + operation = FieldFilterOperation.EQUAL, + expectedValue = "HTTP" + )) + } + ) + ) + assertEquals(expectMatch, match) { "The message ${anyMessage.toJson()} was${if (expectMatch) "" else " not"} matched" } + } @ParameterizedTest @MethodSource("parsedMessages") fun `matches the parsed message by message type with single filter`(anyMessage: AnyMessage, expectMatch: Boolean) { - val match = strategy.verify( + val match = verify( anyMessage, MqRouterFilterConfiguration( metadata = MultiMapUtils.newListValuedHashMap().apply { @@ -58,7 +208,7 @@ class TestAnyMessageFilterStrategy { @ParameterizedTest @MethodSource("messages") fun `matches the parsed message by direction with single filter`(message: AnyMessage, expectMatch: Boolean) { - val match = strategy.verify( + val match = verify( message, MqRouterFilterConfiguration( metadata = MultiMapUtils.newListValuedHashMap().apply { @@ -76,7 +226,7 @@ class TestAnyMessageFilterStrategy { @ParameterizedTest @MethodSource("messages") fun `matches the parsed message by alias with single filter`(message: AnyMessage, expectMatch: Boolean) { - val match = strategy.verify( + val match = verify( message, MqRouterFilterConfiguration( metadata = MultiMapUtils.newListValuedHashMap().apply { @@ -86,48 +236,320 @@ class TestAnyMessageFilterStrategy { expectedValue = "test-alias" )) } - )) + ) + ) + + assertEquals(expectMatch, match) { "The message ${message.toJson()} was${if (expectMatch) "" else " not"} matched" } + } + + @ParameterizedTest + @MethodSource("messagesWithSameFilterFields") + fun `miss matches with the same filter fields`(message: AnyMessage, expectMatch: Boolean) { + val match = verify( + message, + MqRouterFilterConfiguration( + message = MultiMapUtils.newListValuedHashMap().apply { + put( + "test-field", FieldFilterConfiguration( + fieldName = "test-field", + operation = FieldFilterOperation.EQUAL, + expectedValue = "test-value1" + ) + ) + put( + "test-field", FieldFilterConfiguration( + fieldName = "test-field", + operation = FieldFilterOperation.EQUAL, + expectedValue = "test-value2" + ) + ) + } + ) + ) + + assertEquals(expectMatch, match) { "The message ${message.toJson()} was${if (expectMatch) "" else " not"} matched" } + } + + @ParameterizedTest + @MethodSource("messagesWithMultipleFiltersWithSameFilterField") + fun `matches with multiple filters with the same filter fields`(message: AnyMessage, expectMatch: Boolean) { + val match = verify( + message, + listOf( + MqRouterFilterConfiguration( + message = MultiMapUtils.newListValuedHashMap().apply { + put( + "test-field", FieldFilterConfiguration( + fieldName = "test-field", + operation = FieldFilterOperation.EQUAL, + expectedValue = "test-value1" + ) + ) + } + ), + MqRouterFilterConfiguration( + message = MultiMapUtils.newListValuedHashMap().apply { + put( + "test-field", FieldFilterConfiguration( + fieldName = "test-field", + operation = FieldFilterOperation.EQUAL, + expectedValue = "test-value2" + ) + ) + } + ) + ) + ) + + assertEquals(expectMatch, match) { "The message ${message.toJson()} was${if (expectMatch) "" else " not"} matched" } + } + + @ParameterizedTest + @MethodSource("messagesWithMultipleSameFields") + fun `matches message with multiple fields with same name`(message: AnyMessage, expectMatch: Boolean) { + val match = verify( + message, + MqRouterFilterConfiguration( + message = MultiMapUtils.newListValuedHashMap().apply { + put( + "test-field", FieldFilterConfiguration( + fieldName = "test-field", + operation = FieldFilterOperation.NOT_EQUAL, + expectedValue = "test-value1" + ) + ) + put( + "test-field", FieldFilterConfiguration( + fieldName = "test-field", + operation = FieldFilterOperation.EQUAL, + expectedValue = "test-value2" + ) + ) + } + ) + ) + + assertEquals(expectMatch, match) { "The message ${message.toJson()} was${if (expectMatch) "" else " not"} matched" } + } + + @ParameterizedTest + @MethodSource("messagesWithOneProperty") + fun `matches message with properties`(message: AnyMessage, expectMatch: Boolean) { + val match = verify( + message, + MqRouterFilterConfiguration( + metadata = MultiMapUtils.newListValuedHashMap().apply { + put( + "test-property", FieldFilterConfiguration( + fieldName = "test-property", + operation = FieldFilterOperation.EQUAL, + expectedValue = "property-value" + ) + ) + } + ) + ) + + assertEquals(expectMatch, match) { "The message ${message.toJson()} was${if (expectMatch) "" else " not"} matched" } + } + + @ParameterizedTest + @MethodSource("messagesWithPropertiesAndMetadata") + fun `matches message with properties and metadata`(message: AnyMessage, expectMatch: Boolean) { + val match = verify( + message, + MqRouterFilterConfiguration( + metadata = MultiMapUtils.newListValuedHashMap().apply { + put( + "test-property", FieldFilterConfiguration( + fieldName = "test-property", + operation = FieldFilterOperation.EQUAL, + expectedValue = "property-value" + ) + ) + + put("message_type", FieldFilterConfiguration( + fieldName = "message_type", + operation = FieldFilterOperation.EQUAL, + expectedValue = "test" + )) + } + ) + ) assertEquals(expectMatch, match) { "The message ${message.toJson()} was${if (expectMatch) "" else " not"} matched" } } companion object { - private val PARSED_MESSAGE_MATCH = AnyMessage.newBuilder().setMessage( - message("test", Direction.FIRST, "test-alias") - ).build() + private fun simpleMessageBuilder(messageType: String, direction: Direction, sessionAlias: String): AnyMessage { + return AnyMessage.newBuilder().setMessage( + message(BOOK_NAME, messageType, direction, sessionAlias) + ).build() + } + + private fun simpleRawMessageBuilder(sessionAlias: String, directionValue: Direction): AnyMessage { + return AnyMessage.newBuilder().setRawMessage( + RawMessage.newBuilder().apply { + metadataBuilder.idBuilder.apply { + connectionIdBuilder.sessionAlias = sessionAlias + direction = directionValue + } + } + ).build() + } + + private fun messageWithFieldsBuilder(messageType: String, direction: Direction, fields: List>): AnyMessage { + return AnyMessage.newBuilder().setMessage( + message(BOOK_NAME, messageType, direction, "test-alias").apply { + fields.forEach { addField(it.first, it.second) } + } + ).build() + } + + private fun messageWithPropertiesBuilder(messageType: String, direction: Direction, properties: List>): AnyMessage { + return AnyMessage.newBuilder().setMessage( + message(BOOK_NAME, messageType, direction, "test-alias").apply { + properties.forEach { metadataBuilder.putProperties(it.first, it.second) } + } + ).build() + } - private val RAW_MESSAGE_MATCH = AnyMessage.newBuilder().setRawMessage( - RawMessage.newBuilder().apply { - metadataBuilder.idBuilder.apply { - connectionIdBuilder.sessionAlias = "test-alias" - direction = Direction.FIRST + private fun rawMessageWithOnePropertyBuilder(propertyKey: String, propertyValue: String): AnyMessage { + return AnyMessage.newBuilder().setRawMessage( + RawMessage.newBuilder().apply { + metadataBuilder.putProperties(propertyKey, propertyValue) } - } - ).build() + ).build() + } - private val PARSED_MESSAGE_MISS_MATCH = AnyMessage.newBuilder().setMessage( - message("test1", Direction.SECOND, "test-alias1") - ).build() + private fun messageWithOnePropertyBuilder(messageType: String, propertyKey: String, propertyValue: String): AnyMessage { + return messageWithPropertiesBuilder(messageType, Direction.FIRST, listOf(Pair(propertyKey, propertyValue))) + } - private val RAW_MESSAGE_MISS_MATCH = AnyMessage.newBuilder().setRawMessage( - RawMessage.newBuilder().apply { - metadataBuilder.idBuilder.apply { - connectionIdBuilder.sessionAlias = "test-alias1" - direction = Direction.SECOND + private fun messageWithProtocolBuilder(protocol: String): AnyMessage { + return AnyMessage.newBuilder().setMessage( + message(BOOK_NAME, "test", Direction.FIRST, "test-alias").apply { + metadataBuilder.protocol = protocol } - } - ).build() + ).build() + } @JvmStatic - fun parsedMessages(): List = listOf( - arguments(PARSED_MESSAGE_MATCH, true), - arguments(PARSED_MESSAGE_MISS_MATCH, false) + fun messageWithProtocol(): List = listOf( + arguments(messageWithProtocolBuilder("HTTP"), true), + arguments(messageWithProtocolBuilder("FTP"), false), ) @JvmStatic fun messages(): List = listOf( - arguments(RAW_MESSAGE_MATCH, true), - arguments(RAW_MESSAGE_MISS_MATCH, false) + arguments(simpleRawMessageBuilder("test-alias", Direction.FIRST), true), + arguments(simpleRawMessageBuilder("test-alias1", Direction.SECOND), false) ) + parsedMessages() + + @JvmStatic + fun parsedMessages(): List = listOf( + arguments(simpleMessageBuilder("test", Direction.FIRST, "test-alias"), true), + arguments(simpleMessageBuilder("test1", Direction.SECOND, "test-alias1"), false) + ) + + @JvmStatic + fun messagesWithProperties(): List = listOf( + arguments(messageWithPropertiesBuilder("test", Direction.FIRST, listOf( + Pair("prop-field1", "prop-value1"), Pair("prop-field2", "prop-value2"))), true), + arguments(messageWithPropertiesBuilder("test", Direction.FIRST, listOf( + Pair("prop-field1", "prop-value-wrong"), Pair("prop-field2", "prop-value2"))), false), + arguments(messageWithPropertiesBuilder("test", Direction.FIRST, listOf( + Pair("prop-field1", "prop-value1"), Pair("prop-field2", "prop-value-wrong"))), false), + arguments(messageWithPropertiesBuilder("test", Direction.FIRST, listOf( + Pair("prop-field1", "prop-value-wrong"), Pair("prop-field2", "prop-value-wrong"))), false) + ) + + @JvmStatic + fun messagesWithMultipleSameFields(): List = listOf( + arguments(messageWithFieldsBuilder("test", Direction.FIRST, + listOf(Pair("test-field", "test-value1"), Pair("test-field", "test-value2"))), true) + ) + + @JvmStatic + fun messagesWithSameFilterFields(): List = listOf( + arguments(messageWithFieldsBuilder("test", Direction.FIRST, listOf(Pair("test-field", "test-value1"))), false), + arguments(messageWithFieldsBuilder("test", Direction.FIRST, listOf(Pair("test-field", "test-value2"))), false) + ) + + @JvmStatic + fun messagesWithMultipleFiltersWithSameFilterField(): List = listOf( + arguments(messageWithFieldsBuilder("test", Direction.FIRST, listOf(Pair("test-field", "test-value1"))), true), + arguments(messageWithFieldsBuilder("test", Direction.FIRST, listOf(Pair("test-field", "test-value2"))), true) + ) + + @JvmStatic + fun multipleFiltersMatch(): List = listOf( + arguments(simpleMessageBuilder("test", Direction.FIRST, "test-alias"), true), + arguments(simpleMessageBuilder("test", Direction.SECOND, "test-alias"), true), + arguments(simpleMessageBuilder("test-wrong", Direction.FIRST, "test-alias"), true), + arguments(simpleMessageBuilder("test-wrong", Direction.SECOND, "test-alias"), false) + ) + + @JvmStatic + fun messagesWithMessageAndMetadataFilters() : List = listOf( + // fields full match + arguments(messageWithFieldsBuilder("test", Direction.FIRST, + listOf(Pair("test-field1", "test-value1"), Pair("test-field2", "test-value2"))), true), + + // metadata mismatch + arguments(messageWithFieldsBuilder("test", Direction.SECOND, + listOf(Pair("test-field1", "test-value1"), Pair("test-field2", "test-value2"))), false), + arguments(messageWithFieldsBuilder("test-wrong", Direction.FIRST, + listOf(Pair("test-field1", "test-value1"), Pair("test-field2", "test-value2"))), false), + + // fields mismatch + arguments(messageWithFieldsBuilder("test", Direction.FIRST, + listOf(Pair("test-field1", "test-value-wrong"), Pair("test-field2", "test-value2"))), false), + arguments(messageWithFieldsBuilder("test", Direction.FIRST, + listOf(Pair("test-field1", "test-value1"), Pair("test-field2", "test-value-wrong"))), false), + arguments(messageWithFieldsBuilder("test", Direction.FIRST, + listOf(Pair("test-field1", "test-value-wrong"), Pair("test-field2", "test-value-wrong"))), false), + + // one field and one metadata mismatch + arguments(messageWithFieldsBuilder("test", Direction.SECOND, + listOf(Pair("test-field1", "test-value-wrong"), Pair("test-field2", "test-value2"))), false), + arguments(messageWithFieldsBuilder("test-wrong", Direction.FIRST, + listOf(Pair("test-field1", "test-value1"), Pair("test-field2", "test-value-wrong"))), false) + ) + + @JvmStatic + fun parsedMessagesBothFilters() : List = listOf( + arguments(messageWithFieldsBuilder("test", Direction.FIRST, + listOf(Pair("test-field1", "test-value1"), Pair("test-field2", "test-value2"))), true), + arguments(messageWithFieldsBuilder("test", Direction.FIRST, + listOf(Pair("test-field1", "test-value-wrong"), Pair("test-field2", "test-value2"))), false), + arguments(messageWithFieldsBuilder("test", Direction.FIRST, + listOf(Pair("test-field1", "test-value1"), Pair("test-field2", "test-value-wrong"))), false), + arguments(messageWithFieldsBuilder("test", Direction.FIRST, + listOf(Pair("test-field1", "test-value-wrong"), Pair("test-field2", "test-value-wrong"))), false) + ) + + @JvmStatic + fun messagesWithOneProperty() : List = listOf( + arguments(messageWithOnePropertyBuilder("test", "test-property", "property-value"), true), + arguments(messageWithOnePropertyBuilder("test", "test-property", "property-value-wrong"), false), + arguments(rawMessageWithOnePropertyBuilder("test-property", "property-value"), true), + arguments(rawMessageWithOnePropertyBuilder("test-property", "property-value-wrong"), false) + ) + + @JvmStatic + fun messagesWithPropertiesAndMetadata() : List = listOf( + arguments(messageWithOnePropertyBuilder("test", "test-property", "property-value"), true), + arguments(messageWithOnePropertyBuilder("test", "test-property", "property-value-wrong"), false), + arguments(messageWithOnePropertyBuilder("test-wrong", "test-property", "property-value"), false) + ) + + @JvmStatic + fun rawMessagesBothFilters() : List = listOf( + arguments(simpleRawMessageBuilder("test-alias", Direction.FIRST), true), + arguments(simpleRawMessageBuilder("test-alias", Direction.SECOND), false), + arguments(simpleRawMessageBuilder("test-alias-wrong-value", Direction.FIRST), false), + arguments(simpleRawMessageBuilder("test-alias-wrong-value", Direction.SECOND), false) + ) } } \ No newline at end of file diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/TestConfirmationMessageListenerWrapper.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/TestConfirmationMessageListenerWrapper.kt index 0f6cd08e3..3bc849e95 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/TestConfirmationMessageListenerWrapper.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/TestConfirmationMessageListenerWrapper.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 Exactpro (Exactpro Systems Limited) + * Copyright 2022-2023 Exactpro (Exactpro Systems Limited) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,20 +26,20 @@ import org.mockito.kotlin.verify class TestConfirmationMessageListenerWrapper { @Test fun `calls confirmation when requested`() { - val listener = ConfirmationMessageListener.wrap { _, _ -> } + val listener = ConfirmationListener.wrap { _, _ -> } mock {}.also { - listener.handle("", 2, it) + listener.handle(DeliveryMetadata(""), 2, it) verify(it, never()).confirm() } } @Test fun `calls confirmation when requested and method throw an exception`() { - val listener = ConfirmationMessageListener.wrap { _, _ -> error("test") } + val listener = ConfirmationListener.wrap { _, _ -> error("test") } mock {}.also { - assertThrows(IllegalStateException::class.java) { listener.handle("", 2, it) }.apply { + assertThrows(IllegalStateException::class.java) { listener.handle(DeliveryMetadata("test"), 2, it) }.apply { assertEquals("test", message) } verify(it, never()).confirm() diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitRouterIntegrationTest.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitRouterIntegrationTest.kt new file mode 100644 index 000000000..d432f2ccc --- /dev/null +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitRouterIntegrationTest.kt @@ -0,0 +1,226 @@ +/* + * Copyright 2023 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.exactpro.th2.common.schema.message.impl.rabbitmq + +import com.exactpro.th2.common.annotations.IntegrationTest +import com.exactpro.th2.common.schema.box.configuration.BoxConfiguration +import com.exactpro.th2.common.schema.message.ManualAckDeliveryCallback +import com.exactpro.th2.common.schema.message.configuration.MessageRouterConfiguration +import com.exactpro.th2.common.schema.message.configuration.QueueConfiguration +import com.exactpro.th2.common.schema.message.impl.context.DefaultMessageRouterContext +import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.ConnectionManagerConfiguration +import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.RabbitMQConfiguration +import com.exactpro.th2.common.schema.message.impl.rabbitmq.connection.ConnectionManager +import com.exactpro.th2.common.schema.message.impl.rabbitmq.custom.RabbitCustomRouter +import com.rabbitmq.client.BuiltinExchangeType +import mu.KotlinLogging +import org.junit.jupiter.api.Assertions.assertNull +import org.mockito.kotlin.mock +import org.testcontainers.containers.RabbitMQContainer +import org.testcontainers.utility.DockerImageName +import java.time.Duration +import java.util.concurrent.ArrayBlockingQueue +import java.util.concurrent.TimeUnit +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +@IntegrationTest +class AbstractRabbitRouterIntegrationTest { + + @Test + fun `receive unconfirmed message after resubscribe`() { + RabbitMQContainer(DockerImageName.parse(RABBITMQ_MANAGEMENT_ALPINE)) + .withExchange(EXCHANGE, BuiltinExchangeType.DIRECT.type, false, false, true, emptyMap()) + .withQueue(QUEUE_NAME, false, true, emptyMap()) + .withBinding( + EXCHANGE, + QUEUE_NAME, emptyMap(), + ROUTING_KEY, "queue" + ) + .use { rabbitMQContainer -> + rabbitMQContainer.start() + K_LOGGER.info { "Started with port ${rabbitMQContainer.amqpPort}, rest ${rabbitMQContainer.httpUrl} ${rabbitMQContainer.adminUsername} ${rabbitMQContainer.adminPassword} " } + + createConnectionManager(rabbitMQContainer).use { firstManager -> + createRouter(firstManager).use { firstRouter -> + val messageA = "test-message-a" + val messageB = "test-message-b" + val messageC = "test-message-c" + val messageD = "test-message-d" + + val queue = ArrayBlockingQueue(4) + + firstRouter.send(messageA) + firstRouter.send(messageB) + firstRouter.send(messageC) + firstRouter.send(messageD) + + connectAndCheck( + rabbitMQContainer, queue, listOf( + Expectation(messageA, false, ManualAckDeliveryCallback.Confirmation::confirm), + Expectation(messageB, false, ManualAckDeliveryCallback.Confirmation::reject), + Expectation(messageC, false) { }, + Expectation(messageD, false) { }, + ) + ) + + connectAndCheck( + rabbitMQContainer, queue, listOf( + Expectation(messageC, true, ManualAckDeliveryCallback.Confirmation::confirm), + Expectation(messageD, true) { }, + ) + ) + + connectAndCheck( + rabbitMQContainer, queue, listOf( + Expectation(messageD, true, ManualAckDeliveryCallback.Confirmation::reject), + ) + ) + + connectAndCheck(rabbitMQContainer, queue, emptyList()) + } + } + } + } + + private fun connectAndCheck( + rabbitMQContainer: RabbitMQContainer, + queue: ArrayBlockingQueue, + expectations: List, + ) { + createConnectionManager(rabbitMQContainer).use { manager -> + createRouter(manager).use { router -> + val monitor = router.subscribeWithManualAck({ deliveryMetadata, message, confirmation -> + queue.put( + Delivery( + message, + deliveryMetadata.isRedelivered, + confirmation + ) + ) + }) + + try { + expectations.forEach { expectation -> + val delivery = assertNotNull(queue.poll(1, TimeUnit.SECONDS)) + assertEquals(expectation.message, delivery.message, "Message") + assertEquals(expectation.redelivery, delivery.redelivery, "Redelivery flag") + expectation.action.invoke(delivery.confirmation) + } + + assertNull(queue.poll(1, TimeUnit.SECONDS)) + } finally { + monitor.unsubscribe() + } + } + + createRouter(manager).use { router -> + val monitor = router.subscribeWithManualAck({ deliveryMetadata, message, confirmation -> + queue.put( + Delivery( + message, + deliveryMetadata.isRedelivered, + confirmation + ) + ) + }) + + try { + // RabbitMQ doesn't resend messages after resubscribe using the same connection and channel + assertNull(queue.poll(1, TimeUnit.SECONDS)) + } finally { + monitor.unsubscribe() + } + } + } + } + + private fun createConnectionManager( + rabbitMQContainer: RabbitMQContainer, + prefetchCount: Int = DEFAULT_PREFETCH_COUNT, + confirmationTimeout: Duration = DEFAULT_CONFIRMATION_TIMEOUT + ) = ConnectionManager( + RabbitMQConfiguration( + host = rabbitMQContainer.host, + vHost = "", + port = rabbitMQContainer.amqpPort, + username = rabbitMQContainer.adminUsername, + password = rabbitMQContainer.adminPassword, + ), + ConnectionManagerConfiguration( + subscriberName = "test", + prefetchCount = prefetchCount, + confirmationTimeout = confirmationTimeout, + ), + ) { + K_LOGGER.error { "Fatal connection problem" } + } + + private fun createRouter(connectionManager: ConnectionManager) = RabbitCustomRouter( + "test-custom-tag", + arrayOf("test-label"), + TestMessageConverter() + ).apply { + init( + DefaultMessageRouterContext( + connectionManager, + mock { }, + MessageRouterConfiguration( + mapOf( + "test" to QueueConfiguration( + routingKey = ROUTING_KEY, + queue = "", + exchange = "test-exchange", + attributes = listOf("publish") + ), + "test1" to QueueConfiguration( + routingKey = "", + queue = QUEUE_NAME, + exchange = EXCHANGE, + attributes = listOf("subscribe") + ), + ) + ), + BoxConfiguration() + ) + ) + } + + companion object { + private val K_LOGGER = KotlinLogging.logger { } + + private const val RABBITMQ_MANAGEMENT_ALPINE = "rabbitmq:3.11.2-management-alpine" + private const val ROUTING_KEY = "routingKey" + private const val QUEUE_NAME = "queue" + private const val EXCHANGE = "test-exchange" + + private const val DEFAULT_PREFETCH_COUNT = 10 + private val DEFAULT_CONFIRMATION_TIMEOUT: Duration = Duration.ofSeconds(1) + + private class Expectation( + val message: String, + val redelivery: Boolean, + val action: ManualAckDeliveryCallback.Confirmation.() -> Unit + ) + + private class Delivery( + val message: String, + val redelivery: Boolean, + val confirmation: ManualAckDeliveryCallback.Confirmation + ) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitRouterTest.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitRouterTest.kt new file mode 100644 index 000000000..e5fa77b6d --- /dev/null +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitRouterTest.kt @@ -0,0 +1,264 @@ +/* + * Copyright 2023 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.exactpro.th2.common.schema.message.impl.rabbitmq + +import com.exactpro.th2.common.event.bean.BaseTest +import com.exactpro.th2.common.schema.message.ExclusiveSubscriberMonitor +import com.exactpro.th2.common.schema.message.configuration.GlobalNotificationConfiguration +import com.exactpro.th2.common.schema.message.configuration.MessageRouterConfiguration +import com.exactpro.th2.common.schema.message.configuration.QueueConfiguration +import com.exactpro.th2.common.schema.message.impl.context.DefaultMessageRouterContext +import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.ConnectionManagerConfiguration +import com.exactpro.th2.common.schema.message.impl.rabbitmq.connection.ConnectionManager +import com.exactpro.th2.common.schema.message.impl.rabbitmq.custom.RabbitCustomRouter +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertAll +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.function.Executable +import org.mockito.kotlin.any +import org.mockito.kotlin.argThat +import org.mockito.kotlin.clearInvocations +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoMoreInteractions +import java.util.concurrent.atomic.AtomicInteger +import kotlin.test.assertNotEquals + +private const val TEST_EXCLUSIVE_QUEUE = "test-exclusive-queue" + +class AbstractRabbitRouterTest { + private val connectionConfiguration = ConnectionManagerConfiguration() + private val managerMonitor: ExclusiveSubscriberMonitor = mock { } + private val connectionManager: ConnectionManager = mock { + on { configuration }.thenReturn(connectionConfiguration) + on { basicConsume(any(), any(), any()) }.thenReturn(managerMonitor) + on { queueDeclare() }.thenAnswer { "$TEST_EXCLUSIVE_QUEUE-${exclusiveQueueCounter.incrementAndGet()}" } + } + + @Nested + inner class Subscribing { + private val router = createRouter( + mapOf( + "test" to QueueConfiguration( + routingKey = "publish", + queue = "", + exchange = "test-exchange", + attributes = listOf("publish", "test") + ), + "test1" to QueueConfiguration( + routingKey = "", + queue = "queue1", + exchange = "test-exchange", + attributes = listOf("subscribe", "1") + ), + "test2" to QueueConfiguration( + routingKey = "", + queue = "queue2", + exchange = "test-exchange", + attributes = listOf("subscribe", "2") + ) + ) + ) + + @AfterEach + fun afterEach() { + verifyNoMoreInteractions(connectionManager) + verifyNoMoreInteractions(managerMonitor) + } + + @Test + fun `subscribes to correct queue`() { + val monitor = router.subscribe(mock { }, "1") + assertNotNull(monitor) { "monitor must not be null" } + + verify(connectionManager).basicConsume(eq("queue1"), any(), any()) + } + + @Test + fun `subscribes with manual ack to correct queue`() { + val monitor = router.subscribeWithManualAck(mock { }, "1") + assertNotNull(monitor) { "monitor must not be null" } + + verify(connectionManager).basicConsume(eq("queue1"), any(), any()) + } + + @Test + fun `subscribes to all matched queues`() { + val monitor = router.subscribeAll(mock { }) + assertNotNull(monitor) { "monitor must not be null" } + + verify(connectionManager).basicConsume(eq("queue1"), any(), any()) + verify(connectionManager).basicConsume(eq("queue2"), any(), any()) + } + + @Test + fun `unsubscribe after subscribe`() { + val monitor = router.subscribe(mock { }, "1") + clearInvocations(connectionManager) + clearInvocations(managerMonitor) + + monitor.unsubscribe() + verify(managerMonitor).unsubscribe() + } + + @Test + fun `unsubscribe after subscribe with manual ack`() { + val monitor = router.subscribeWithManualAck(mock { }, "1") + clearInvocations(connectionManager) + clearInvocations(managerMonitor) + + monitor.unsubscribe() + verify(managerMonitor).unsubscribe() + } + + @Test + fun `unsubscribe after subscribe to exclusive queue`() { + val monitor = router.subscribeExclusive(mock { }) + clearInvocations(connectionManager) + clearInvocations(managerMonitor) + + monitor.unsubscribe() + verify(managerMonitor).unsubscribe() + } + + @Test + fun `subscribes after unsubscribe`() { + router.subscribe(mock { }, "1") + .unsubscribe() + clearInvocations(connectionManager) + clearInvocations(managerMonitor) + + val monitor = router.subscribe(mock { }, "1") + assertNotNull(monitor) { "monitor must not be null" } + verify(connectionManager).basicConsume(eq("queue1"), any(), any()) + } + + @Test + fun `subscribes with manual ack after unsubscribe`() { + router.subscribeWithManualAck(mock { }, "1") + .unsubscribe() + clearInvocations(connectionManager) + clearInvocations(managerMonitor) + + val monitor = router.subscribeWithManualAck(mock { }, "1") + assertNotNull(monitor) { "monitor must not be null" } + verify(connectionManager).basicConsume(eq("queue1"), any(), any()) + } + + @Test + fun `subscribes when subscribtion active`() { + router.subscribe(mock { }, "1") + clearInvocations(connectionManager) + clearInvocations(managerMonitor) + + assertThrows(RuntimeException::class.java) { router.subscribe(mock { }, "1") } + } + + @Test + fun `subscribes with manual ack when subscribtion active`() { + router.subscribeWithManualAck(mock { }, "1") + clearInvocations(connectionManager) + clearInvocations(managerMonitor) + + assertThrows(RuntimeException::class.java) { router.subscribeWithManualAck(mock { }, "1") } + } + + @Test + fun `subscribes to exclusive queue when subscribtion active`() { + val monitorA = router.subscribeExclusive(mock { }) + val monitorB = router.subscribeExclusive(mock { }) + + verify(connectionManager, times(2)).queueDeclare() + verify(connectionManager, times(2)).basicConsume( + argThat { matches(Regex("$TEST_EXCLUSIVE_QUEUE-\\d+")) }, + any(), + any() + ) + assertNotEquals(monitorA.queue, monitorB.queue) + } + + @Test + fun `reports if more that one queue matches`() { + assertThrows(IllegalStateException::class.java) { router.subscribe(mock { }) } + .apply { + assertEquals( + "Found incorrect number of pins [test1, test2] to subscribe operation by attributes [subscribe] and filters, expected 1, actual 2", + message + ) + } + } + + @Test + fun `reports if no queue matches`() { + assertAll( + Executable { + assertThrows(IllegalStateException::class.java) { + router.subscribe( + mock { }, + "unexpected" + ) + } + .apply { + assertEquals( + "Found incorrect number of pins [] to subscribe operation by attributes [unexpected, subscribe] and filters, expected 1, actual 0", + message + ) + } + }, + Executable { + assertThrows(IllegalStateException::class.java) { + router.subscribeAll( + mock { }, + "unexpected" + ) + } + .apply { + assertEquals( + "Found incorrect number of pins [] to subscribe all operation by attributes [unexpected, subscribe] and filters, expected 1 or more, actual 0", + message + ) + } + } + ) + } + } + + private fun createRouter(pins: Map): RabbitCustomRouter = + RabbitCustomRouter( + "test-custom-tag", + arrayOf("test-label"), + TestMessageConverter() + ).apply { + init( + DefaultMessageRouterContext( + connectionManager, + mock { }, + MessageRouterConfiguration(pins, GlobalNotificationConfiguration()), + BaseTest.BOX_CONFIGURATION + ) + ) + } + + companion object { + private val exclusiveQueueCounter = AtomicInteger(0) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/TestMessageConverter.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/TestMessageConverter.kt new file mode 100644 index 000000000..8fba22465 --- /dev/null +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/TestMessageConverter.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2023 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.exactpro.th2.common.schema.message.impl.rabbitmq + +import com.exactpro.th2.common.schema.message.impl.rabbitmq.custom.MessageConverter + +class TestMessageConverter: MessageConverter { + override fun toByteArray(value: String): ByteArray = value.toByteArray() + + override fun fromByteArray(data: ByteArray): String = String(data) + + override fun extractCount(value: String): Int = 1 + + override fun toDebugString(value: String): String = value + + override fun toTraceString(value: String): String = value +} \ No newline at end of file diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt index 956699ea5..4ab7213a4 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 Exactpro (Exactpro Systems Limited) + * Copyright 2023 Exactpro (Exactpro Systems Limited) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,6 +34,7 @@ import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger import kotlin.concurrent.thread +import com.rabbitmq.client.CancelCallback import mu.KotlinLogging import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.Assertions @@ -46,6 +47,8 @@ import org.junit.jupiter.api.Test import org.testcontainers.containers.RabbitMQContainer import org.testcontainers.utility.DockerImageName import org.testcontainers.utility.MountableFile +import java.io.IOException +import kotlin.test.assertFailsWith private val LOGGER = KotlinLogging.logger { } @@ -840,3 +843,93 @@ class TestConnectionManager { } } + + @Test + fun `connection manager exclusive queue test`() { + RabbitMQContainer(DockerImageName.parse("rabbitmq:3.8-management-alpine")) + .use { rabbitMQContainer -> + rabbitMQContainer.start() + LOGGER.info { "Started with port ${rabbitMQContainer.amqpPort}" } + + createConnectionManager(rabbitMQContainer).use { firstManager -> + createConnectionManager(rabbitMQContainer).use { secondManager -> + val queue = firstManager.queueDeclare() + + assertFailsWith("Another connection can subscribe to the $queue queue") { + secondManager.basicConsume(queue, { _, _, _ -> }, {}) + } + + extracted(firstManager, secondManager, queue, 3) + extracted(firstManager, secondManager, queue, 6) + } + } + + } + } + + private fun extracted( + firstManager: ConnectionManager, + secondManager: ConnectionManager, + queue: String, + cycle: Int + ) { + val countDown = CountDownLatch(cycle) + val deliverCallback = ManualAckDeliveryCallback { _, delivery, conformation -> + countDown.countDown() + LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from ${delivery.envelope.exchange}:${delivery.envelope.routingKey}, received ${countDown.count}" } + conformation.confirm() + } + val cancelCallback = CancelCallback { LOGGER.warn { "Canceled $it" } } + + val firstMonitor = firstManager.basicConsume(queue, deliverCallback, cancelCallback) + val secondMonitor = firstManager.basicConsume(queue, deliverCallback, cancelCallback) + + repeat(cycle) { index -> + secondManager.basicPublish( + "", + queue, + null, + "Hello $index".toByteArray(Charsets.UTF_8) + ) + } + + assertTrue( + countDown.await( + 1L, + TimeUnit.SECONDS + ) + ) { "Not all messages were received: ${countDown.count}" } + + assertTrue(firstManager.isAlive) { "Manager should still be alive" } + assertTrue(firstManager.isReady) { "Manager should be ready until the confirmation timeout expires" } + + firstMonitor.unsubscribe() + secondMonitor.unsubscribe() + } + + private fun createConnectionManager( + rabbitMQContainer: RabbitMQContainer, + prefetchCount: Int = DEFAULT_PREFETCH_COUNT, + confirmationTimeout: Duration = DEFAULT_CONFIRMATION_TIMEOUT + ) = ConnectionManager( + RabbitMQConfiguration( + host = rabbitMQContainer.host, + vHost = "", + port = rabbitMQContainer.amqpPort, + username = rabbitMQContainer.adminUsername, + password = rabbitMQContainer.adminPassword, + ), + ConnectionManagerConfiguration( + subscriberName = "test", + prefetchCount = prefetchCount, + confirmationTimeout = confirmationTimeout, + ), + ) { + LOGGER.error { "Fatal connection problem" } + } + + companion object { + private const val DEFAULT_PREFETCH_COUNT = 10 + private val DEFAULT_CONFIRMATION_TIMEOUT: Duration = Duration.ofSeconds(1) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/custom/TestMessageConverterLambdaDelegate.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/custom/TestMessageConverterLambdaDelegate.kt index fcae0fa64..91ad69184 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/custom/TestMessageConverterLambdaDelegate.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/custom/TestMessageConverterLambdaDelegate.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2022 Exactpro (Exactpro Systems Limited) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,8 @@ package com.exactpro.th2.common.schema.message.impl.rabbitmq.custom -import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Assertions.assertArrayEquals +import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertAll diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/custom/TestMessageUtil.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/custom/TestMessageUtil.kt index 639cd343f..1a4b2fb2d 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/custom/TestMessageUtil.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/custom/TestMessageUtil.kt @@ -15,9 +15,11 @@ */ package com.exactpro.th2.common.schema.message.impl.rabbitmq.custom +import com.exactpro.th2.common.event.bean.BaseTest.BOOK_NAME import com.exactpro.th2.common.grpc.Direction import com.exactpro.th2.common.grpc.Direction.SECOND import com.exactpro.th2.common.message.addField +import com.exactpro.th2.common.message.bookName import com.exactpro.th2.common.message.direction import com.exactpro.th2.common.message.fromJson import com.exactpro.th2.common.message.get @@ -59,7 +61,8 @@ class TestMessageUtil { assertTrue(it.hasMetadata()) assertEquals(MESSAGE_TYPE_VALUE, it.metadata.messageType) } - message(MESSAGE_TYPE_VALUE, DIRECTION_VALUE, SESSION_ALIAS_VALUE).build().also { + message(BOOK_NAME, MESSAGE_TYPE_VALUE, DIRECTION_VALUE, SESSION_ALIAS_VALUE).build().also { + assertEquals(BOOK_NAME, it.bookName) assertEquals(MESSAGE_TYPE_VALUE, it.messageType) assertEquals(DIRECTION_VALUE, it.direction) assertEquals(SESSION_ALIAS_VALUE, it.sessionAlias) @@ -82,14 +85,30 @@ class TestMessageUtil { } } + @Test + fun `update book name`() { + val builder = message(BOOK_NAME, MESSAGE_TYPE_VALUE, DIRECTION_VALUE, SESSION_ALIAS_VALUE) + val newBookName = builder.bookName + "Hello" + + builder.apply { + bookName = newBookName + }.also { + assertEquals(newBookName, it.bookName) + assertEquals(MESSAGE_TYPE_VALUE, it.messageType) + assertEquals(DIRECTION_VALUE, it.direction) + assertEquals(SESSION_ALIAS_VALUE, it.sessionAlias) + } + } + @Test fun `update message type`() { - val builder = message(MESSAGE_TYPE_VALUE, DIRECTION_VALUE, SESSION_ALIAS_VALUE) + val builder = message(BOOK_NAME, MESSAGE_TYPE_VALUE, DIRECTION_VALUE, SESSION_ALIAS_VALUE) val newMessageType = builder.messageType + "Hello" builder.apply { messageType = newMessageType }.also { + assertEquals(BOOK_NAME, it.bookName) assertEquals(newMessageType, it.messageType) assertEquals(DIRECTION_VALUE, it.direction) assertEquals(SESSION_ALIAS_VALUE, it.sessionAlias) @@ -98,7 +117,7 @@ class TestMessageUtil { @Test fun `update direction`() { - val builder = message(MESSAGE_TYPE_VALUE, DIRECTION_VALUE, SESSION_ALIAS_VALUE) + val builder = message(BOOK_NAME, MESSAGE_TYPE_VALUE, DIRECTION_VALUE, SESSION_ALIAS_VALUE) val newDirection = Direction.values().asSequence() .filter{ item -> item != Direction.UNRECOGNIZED && item != builder.direction } .first() @@ -106,6 +125,7 @@ class TestMessageUtil { builder.apply { direction = newDirection }.also { + assertEquals(BOOK_NAME, it.bookName) assertEquals(MESSAGE_TYPE_VALUE, it.messageType) assertEquals(newDirection, it.direction) assertEquals(SESSION_ALIAS_VALUE, it.sessionAlias) @@ -114,12 +134,13 @@ class TestMessageUtil { @Test fun `update session alias`() { - val builder = message(MESSAGE_TYPE_VALUE, DIRECTION_VALUE, SESSION_ALIAS_VALUE) + val builder = message(BOOK_NAME, MESSAGE_TYPE_VALUE, DIRECTION_VALUE, SESSION_ALIAS_VALUE) val newSessionAlias = builder.sessionAlias + "Hello" builder.apply { sessionAlias = newSessionAlias }.also { + assertEquals(BOOK_NAME, it.bookName) assertEquals(MESSAGE_TYPE_VALUE, it.messageType) assertEquals(DIRECTION_VALUE, it.direction) assertEquals(newSessionAlias, it.sessionAlias) @@ -128,12 +149,13 @@ class TestMessageUtil { @Test fun `update sequence`() { - val builder = message(MESSAGE_TYPE_VALUE, DIRECTION_VALUE, SESSION_ALIAS_VALUE) + val builder = message(BOOK_NAME, MESSAGE_TYPE_VALUE, DIRECTION_VALUE, SESSION_ALIAS_VALUE) val newSequence = builder.sequence++ builder.apply { sequence = newSequence }.also { + assertEquals(BOOK_NAME, it.bookName) assertEquals(MESSAGE_TYPE_VALUE, it.messageType) assertEquals(DIRECTION_VALUE, it.direction) assertEquals(SESSION_ALIAS_VALUE, it.sessionAlias) @@ -149,7 +171,8 @@ class TestMessageUtil { "id": { "connectionId": { "sessionAlias": "$SESSION_ALIAS_VALUE" - } + }, + "bookName": "$BOOK_NAME" }, "messageType": "$MESSAGE_TYPE_VALUE" }, @@ -160,29 +183,31 @@ class TestMessageUtil { } } """.trimIndent()).also { - assertEquals(it.messageType, MESSAGE_TYPE_VALUE) - assertEquals(it.sessionAlias, SESSION_ALIAS_VALUE) + assertEquals(BOOK_NAME, it.bookName) + assertEquals(MESSAGE_TYPE_VALUE, it.messageType) + assertEquals(SESSION_ALIAS_VALUE, it.sessionAlias) assertEquals(it.getString(FIELD_NAME), FIELD_VALUE) } } @Test fun `to json from json message test`() { - val json = message(MESSAGE_TYPE_VALUE, DIRECTION_VALUE, SESSION_ALIAS_VALUE).apply { + val json = message(BOOK_NAME, MESSAGE_TYPE_VALUE, DIRECTION_VALUE, SESSION_ALIAS_VALUE).apply { addField(FIELD_NAME, FIELD_VALUE) }.toJson() message().fromJson(json).also { - assertEquals(it.messageType, MESSAGE_TYPE_VALUE) - assertEquals(it.direction, DIRECTION_VALUE) - assertEquals(it.sessionAlias, SESSION_ALIAS_VALUE) - assertEquals(it.getString(FIELD_NAME), FIELD_VALUE) + assertEquals(BOOK_NAME, it.bookName) + assertEquals(MESSAGE_TYPE_VALUE, it.messageType) + assertEquals(DIRECTION_VALUE, it.direction) + assertEquals(SESSION_ALIAS_VALUE, it.sessionAlias) + assertEquals(FIELD_VALUE, it.getString(FIELD_NAME)) } } @Test fun `update field`() { - message(MESSAGE_TYPE_VALUE, DIRECTION_VALUE, SESSION_ALIAS_VALUE).apply { + message(BOOK_NAME, MESSAGE_TYPE_VALUE, DIRECTION_VALUE, SESSION_ALIAS_VALUE).apply { addField(FIELD_NAME, FIELD_VALUE) }.updateString(FIELD_NAME) { FIELD_VALUE_2 }.also { assertEquals(it.getString(FIELD_NAME), FIELD_VALUE_2) @@ -191,7 +216,7 @@ class TestMessageUtil { @Test fun `update complex field`() { - message(MESSAGE_TYPE_VALUE, DIRECTION_VALUE, SESSION_ALIAS_VALUE).apply { + message(BOOK_NAME, MESSAGE_TYPE_VALUE, DIRECTION_VALUE, SESSION_ALIAS_VALUE).apply { addField(FIELD_NAME, message() .addField(FIELD_NAME, FIELD_VALUE) .addField(FIELD_NAME_2, listOf(FIELD_VALUE, FIELD_VALUE_2)) @@ -207,7 +232,7 @@ class TestMessageUtil { @Test fun `update or add field test add`() { - message(MESSAGE_TYPE_VALUE, DIRECTION_VALUE, SESSION_ALIAS_VALUE).apply { + message(BOOK_NAME, MESSAGE_TYPE_VALUE, DIRECTION_VALUE, SESSION_ALIAS_VALUE).apply { updateOrAddString(FIELD_NAME) { it ?: FIELD_VALUE } }.also { assertEquals(it.getString(FIELD_NAME), FIELD_VALUE) @@ -216,7 +241,7 @@ class TestMessageUtil { @Test fun `update or add field test update`() { - message(MESSAGE_TYPE_VALUE, DIRECTION_VALUE, SESSION_ALIAS_VALUE).apply { + message(BOOK_NAME, MESSAGE_TYPE_VALUE, DIRECTION_VALUE, SESSION_ALIAS_VALUE).apply { addField(FIELD_NAME, FIELD_VALUE) updateOrAddString(FIELD_NAME) { it ?: FIELD_VALUE_2 } }.also { diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/group/IntegrationTestRabbitMessageBatchRouter.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/group/IntegrationTestRabbitMessageBatchRouter.kt new file mode 100644 index 000000000..963825782 --- /dev/null +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/group/IntegrationTestRabbitMessageBatchRouter.kt @@ -0,0 +1,141 @@ +/* + * Copyright 2022-2022 Exactpro (Exactpro Systems Limited) + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.exactpro.th2.common.schema.message.impl.rabbitmq.group + +import com.exactpro.th2.common.annotations.IntegrationTest +import com.exactpro.th2.common.grpc.MessageGroupBatch +import com.exactpro.th2.common.schema.box.configuration.BoxConfiguration +import com.exactpro.th2.common.schema.message.configuration.MessageRouterConfiguration +import com.exactpro.th2.common.schema.message.impl.context.DefaultMessageRouterContext +import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.ConnectionManagerConfiguration +import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.RabbitMQConfiguration +import com.exactpro.th2.common.schema.message.impl.rabbitmq.connection.ConnectionManager +import com.rabbitmq.client.BuiltinExchangeType +import mu.KotlinLogging +import org.junit.jupiter.api.Test +import org.mockito.kotlin.mock +import org.testcontainers.containers.RabbitMQContainer +import org.testcontainers.utility.DockerImageName +import java.time.Duration +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import kotlin.test.assertTrue + +@IntegrationTest +class IntegrationTestRabbitMessageGroupBatchRouter { + + @Test + fun `subscribe to exclusive queue`() { + RabbitMQContainer(DockerImageName.parse(RABBITMQ_3_8_MANAGEMENT_ALPINE)) + .use { rabbitMQContainer -> + rabbitMQContainer.start() + LOGGER.info { "Started with port ${rabbitMQContainer.amqpPort}" } + + createConnectionManager(rabbitMQContainer).use { firstManager -> + createRouter(firstManager).use { firstRouter -> + createConnectionManager(rabbitMQContainer).use { secondManager -> + createRouter(secondManager).use { secondRouter -> + val counter = CountDownLatch(1) + val monitor = firstRouter.subscribeExclusive { _, _ -> counter.countDown() } + try { + secondRouter.sendExclusive(monitor.queue, MessageGroupBatch.getDefaultInstance()) + assertTrue("Message is not received") { counter.await(1, TimeUnit.SECONDS) } + + } finally { + monitor.unsubscribe() + } + } + } + } + } + } + } + + @Test + fun `send receive message group batch`() { + RabbitMQContainer(DockerImageName.parse(RABBITMQ_3_8_MANAGEMENT_ALPINE)) + .withExchange(EXCHANGE, BuiltinExchangeType.DIRECT.type, false, false, true, emptyMap()) + .withQueue(QUEUE_NAME) + .withBinding(EXCHANGE, QUEUE_NAME, emptyMap(), ROUTING_KEY, "queue") + .use { rabbitMQContainer -> + rabbitMQContainer.start() + LOGGER.info { "Started with port ${rabbitMQContainer.amqpPort}" } + + createConnectionManager(rabbitMQContainer).use { firstManager -> + createRouter(firstManager).use { firstRouter -> + createConnectionManager(rabbitMQContainer).use { secondManager -> + createRouter(secondManager).use { secondRouter -> + val counter = CountDownLatch(1) + val monitor = firstRouter.subscribeExclusive { _, _ -> counter.countDown() } + try { + + secondRouter.sendExclusive(monitor.queue, MessageGroupBatch.getDefaultInstance()) + assertTrue("Message is not received") { counter.await(1, TimeUnit.SECONDS) } + + } finally { + monitor.unsubscribe() + } + } + } + } + } + } + } + + private fun createRouter(connectionManager: ConnectionManager) = RabbitMessageGroupBatchRouter() + .apply { + init( + DefaultMessageRouterContext( + connectionManager, + mock { }, + MessageRouterConfiguration(), + BoxConfiguration() + ) + ) + } + + private fun createConnectionManager( + rabbitMQContainer: RabbitMQContainer, + prefetchCount: Int = DEFAULT_PREFETCH_COUNT, + confirmationTimeout: Duration = DEFAULT_CONFIRMATION_TIMEOUT + ) = ConnectionManager( + RabbitMQConfiguration( + host = rabbitMQContainer.host, + vHost = "", + port = rabbitMQContainer.amqpPort, + username = rabbitMQContainer.adminUsername, + password = rabbitMQContainer.adminPassword, + ), + ConnectionManagerConfiguration( + subscriberName = "test", + prefetchCount = prefetchCount, + confirmationTimeout = confirmationTimeout, + ), + ) { + LOGGER.error { "Fatal connection problem" } + } + + companion object { + private val LOGGER = KotlinLogging.logger { } + + private const val RABBITMQ_3_8_MANAGEMENT_ALPINE = "rabbitmq:3.8-management-alpine" + private const val ROUTING_KEY = "routingKey" + private const val QUEUE_NAME = "queue" + private const val EXCHANGE = "test-exchange" + + private const val DEFAULT_PREFETCH_COUNT = 10 + private val DEFAULT_CONFIRMATION_TIMEOUT: Duration = Duration.ofSeconds(1) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/group/TestRabbitMessageGroupBatchRouter.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/group/TestRabbitMessageBatchRouter.kt similarity index 72% rename from src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/group/TestRabbitMessageGroupBatchRouter.kt rename to src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/group/TestRabbitMessageBatchRouter.kt index 4b48e4510..e6840fec6 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/group/TestRabbitMessageGroupBatchRouter.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/group/TestRabbitMessageBatchRouter.kt @@ -16,15 +16,18 @@ package com.exactpro.th2.common.schema.message.impl.rabbitmq.group +import com.exactpro.th2.common.event.bean.BaseTest.BOOK_NAME +import com.exactpro.th2.common.event.bean.BaseTest.BOX_CONFIGURATION import com.exactpro.th2.common.grpc.Direction import com.exactpro.th2.common.grpc.MessageGroup import com.exactpro.th2.common.grpc.MessageGroupBatch import com.exactpro.th2.common.message.message import com.exactpro.th2.common.message.plusAssign import com.exactpro.th2.common.schema.message.MessageRouter -import com.exactpro.th2.common.schema.message.SubscriberMonitor +import com.exactpro.th2.common.schema.message.ExclusiveSubscriberMonitor import com.exactpro.th2.common.schema.message.configuration.FieldFilterConfiguration import com.exactpro.th2.common.schema.message.configuration.FieldFilterOperation +import com.exactpro.th2.common.schema.message.configuration.GlobalNotificationConfiguration import com.exactpro.th2.common.schema.message.configuration.MessageRouterConfiguration import com.exactpro.th2.common.schema.message.configuration.MqRouterFilterConfiguration import com.exactpro.th2.common.schema.message.configuration.QueueConfiguration @@ -32,6 +35,8 @@ import com.exactpro.th2.common.schema.message.impl.context.DefaultMessageRouterC import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.ConnectionManagerConfiguration import com.exactpro.th2.common.schema.message.impl.rabbitmq.connection.ConnectionManager import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertArrayEquals +import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.function.Executable @@ -41,14 +46,15 @@ import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never +import org.mockito.kotlin.times import org.mockito.kotlin.verify class TestRabbitMessageGroupBatchRouter { private val connectionConfiguration = ConnectionManagerConfiguration() - private val monitor: SubscriberMonitor = mock { } + private val managerMonitor: ExclusiveSubscriberMonitor = mock { } private val connectionManager: ConnectionManager = mock { on { configuration }.thenReturn(connectionConfiguration) - on { basicConsume(any(), any(), any()) }.thenReturn(monitor) + on { basicConsume(any(), any(), any()) }.thenReturn(managerMonitor) } @Nested @@ -96,12 +102,33 @@ class TestRabbitMessageGroupBatchRouter { ) )) + @Test + fun `publishes message group batch with metadata`() { + val batch = MessageGroupBatch.newBuilder().apply { + metadataBuilder.apply { + externalQueue = "externalQueue" + } + addGroupsBuilder().apply { + this += message("test-book", "test-message", Direction.FIRST, "test-alias") + } + }.build() + + router.send(batch, "test") + + val captor = argumentCaptor() + verify(connectionManager).basicPublish(eq("test-exchange"), eq("test2"), anyOrNull(), captor.capture()) + val publishedBytes = captor.firstValue + assertArrayEquals(batch.toByteArray(), publishedBytes) { + "Unexpected batch published: ${MessageGroupBatch.parseFrom(publishedBytes)}" + } + } + @Test fun `does not publish anything if all messages are filtered`() { router.send( MessageGroupBatch.newBuilder() .addGroups(MessageGroup.newBuilder() - .apply { this += message("test-message1", Direction.FIRST, "test-alias") } + .apply { this += message(BOOK_NAME, "test-message1", Direction.FIRST, "test-alias") } ).build() ) @@ -112,24 +139,24 @@ class TestRabbitMessageGroupBatchRouter { fun `publishes to the correct pin according to attributes`() { val batch = MessageGroupBatch.newBuilder() .addGroups(MessageGroup.newBuilder() - .apply { this += message("test-message", Direction.FIRST, "test-alias") } + .apply { this += message(BOOK_NAME, "test-message", Direction.FIRST, "test-alias") } ).build() router.send(batch, "test") val captor = argumentCaptor() verify(connectionManager).basicPublish(eq("test-exchange"), eq("test2"), anyOrNull(), captor.capture()) val publishedBytes = captor.firstValue - Assertions.assertArrayEquals(batch.toByteArray(), publishedBytes) { + assertArrayEquals(batch.toByteArray(), publishedBytes) { "Unexpected batch published: ${MessageGroupBatch.parseFrom(publishedBytes)}" } } @Test fun `reports about extra pins matches the publication`() { - Assertions.assertThrows(IllegalStateException::class.java) { + assertThrows(IllegalStateException::class.java) { router.send(MessageGroupBatch.newBuilder() .addGroups(MessageGroup.newBuilder() - .apply { this += message("test-message", Direction.FIRST, "test-alias") } + .apply { this += message(BOOK_NAME, "test-message", Direction.FIRST, "test-alias") } ).build()) }.apply { Assertions.assertEquals( @@ -141,10 +168,10 @@ class TestRabbitMessageGroupBatchRouter { @Test fun `reports about no pins matches the publication`() { - Assertions.assertThrows(IllegalStateException::class.java) { + assertThrows(IllegalStateException::class.java) { router.send(MessageGroupBatch.newBuilder() .addGroups(MessageGroup.newBuilder() - .apply { this += message("test-message", Direction.FIRST, "test-alias") } + .apply { this += message(BOOK_NAME, "test-message", Direction.FIRST, "test-alias") } ).build(), "unexpected" ) @@ -160,7 +187,7 @@ class TestRabbitMessageGroupBatchRouter { fun `publishes to all correct pin according to attributes`() { val batch = MessageGroupBatch.newBuilder() .addGroups(MessageGroup.newBuilder() - .apply { this += message("test-message", Direction.FIRST, "test-alias") } + .apply { this += message(BOOK_NAME, "test-message", Direction.FIRST, "test-alias") } ).build() router.sendAll(batch) @@ -171,13 +198,13 @@ class TestRabbitMessageGroupBatchRouter { Assertions.assertAll( Executable { val publishedBytes = captor.firstValue - Assertions.assertArrayEquals(originalBytes, publishedBytes) { + assertArrayEquals(originalBytes, publishedBytes) { "Unexpected batch published: ${MessageGroupBatch.parseFrom(publishedBytes)}" } }, Executable { val publishedBytes = captor.secondValue - Assertions.assertArrayEquals(originalBytes, publishedBytes) { + assertArrayEquals(originalBytes, publishedBytes) { "Unexpected batch published: ${MessageGroupBatch.parseFrom(publishedBytes)}" } } @@ -193,7 +220,7 @@ class TestRabbitMessageGroupBatchRouter { routingKey = "publish", queue = "", exchange = "test-exchange", - attributes = listOf("publish") + attributes = listOf("publish", "test") ), "test1" to QueueConfiguration( routingKey = "", @@ -211,25 +238,29 @@ class TestRabbitMessageGroupBatchRouter { ) @Test - fun `subscribes to correct queue`() { - val monitor = router.subscribe(mock { }, "1") - Assertions.assertNotNull(monitor) { "monitor must not be null" } - - verify(connectionManager).basicConsume(eq("queue1"), any(), any()) - } + fun `publishes message group batch with metadata`() { + val batch = MessageGroupBatch.newBuilder().apply { + metadataBuilder.apply { + externalQueue = "externalQueue" + } + addGroupsBuilder().apply { + this += message("test-book", "test-message", Direction.FIRST, "test-alias") + } + }.build() - @Test - fun `subscribes to all matched queues`() { - val monitor = router.subscribeAll(mock { }) - Assertions.assertNotNull(monitor) { "monitor must not be null" } + router.send(batch, "test") - verify(connectionManager).basicConsume(eq("queue1"), any(), any()) - verify(connectionManager).basicConsume(eq("queue2"), any(), any()) + val captor = argumentCaptor() + verify(connectionManager).basicPublish(eq("test-exchange"), eq("publish"), anyOrNull(), captor.capture()) + val publishedBytes = captor.firstValue + assertArrayEquals(batch.toByteArray(), publishedBytes) { + "Unexpected batch published: ${MessageGroupBatch.parseFrom(publishedBytes)}" + } } @Test fun `reports if more that one queue matches`() { - Assertions.assertThrows(IllegalStateException::class.java) { router.subscribe(mock { }) } + assertThrows(IllegalStateException::class.java) { router.subscribe(mock { }) } .apply { Assertions.assertEquals( "Found incorrect number of pins [test1, test2] to subscribe operation by attributes [subscribe] and filters, expected 1, actual 2", @@ -242,7 +273,7 @@ class TestRabbitMessageGroupBatchRouter { fun `reports if no queue matches`() { Assertions.assertAll( Executable { - Assertions.assertThrows(IllegalStateException::class.java) { router.subscribe(mock { }, "unexpected") } + assertThrows(IllegalStateException::class.java) { router.subscribe(mock { }, "unexpected") } .apply { Assertions.assertEquals( "Found incorrect number of pins [] to subscribe operation by attributes [unexpected, subscribe] and filters, expected 1, actual 0", @@ -251,7 +282,7 @@ class TestRabbitMessageGroupBatchRouter { } }, Executable { - Assertions.assertThrows(IllegalStateException::class.java) { router.subscribeAll(mock { }, "unexpected") } + assertThrows(IllegalStateException::class.java) { router.subscribeAll(mock { }, "unexpected") } .apply { Assertions.assertEquals( "Found incorrect number of pins [] to subscribe all operation by attributes [unexpected, subscribe] and filters, expected 1 or more, actual 0", @@ -268,7 +299,8 @@ class TestRabbitMessageGroupBatchRouter { init(DefaultMessageRouterContext( connectionManager, mock { }, - MessageRouterConfiguration(pins) + MessageRouterConfiguration(pins, GlobalNotificationConfiguration()), + BOX_CONFIGURATION )) } -} \ No newline at end of file +} diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/CodecsTest.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/CodecsTest.kt new file mode 100644 index 000000000..c4c706bdc --- /dev/null +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/CodecsTest.kt @@ -0,0 +1,169 @@ +/* + * Copyright 2023 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.exactpro.th2.common.schema.message.impl.rabbitmq.transport + +import io.netty.buffer.ByteBufUtil +import io.netty.buffer.Unpooled +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.DynamicTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestFactory +import java.time.Instant +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime +import java.time.format.DateTimeFormatter +import java.time.temporal.TemporalAccessor + +class CodecsTest { + + @Test + fun `decode encode test`() { + val buffer = Unpooled.buffer() + + val message1 = RawMessage( + id = MessageId( + sessionAlias = "alias1", + direction = Direction.INCOMING, + sequence = 1, + subsequence = mutableListOf(1, 2), + timestamp = Instant.now() + ), + metadata = mutableMapOf( + "prop1" to "value1", + "prop2" to "value2" + ), + protocol = "proto1", + body = Unpooled.wrappedBuffer(byteArrayOf(1, 2, 3, 4)) + ) + + val message2 = RawMessage( + id = MessageId( + sessionAlias = "alias2", + direction = Direction.OUTGOING, + sequence = 2, + subsequence = mutableListOf(3, 4), + timestamp = Instant.now() + ), + metadata = mutableMapOf( + "prop3" to "value3", + "prop4" to "value4" + ), + protocol = "proto2", + body = Unpooled.wrappedBuffer(byteArrayOf(5, 6, 7, 8)) + ) + + val message3 = ParsedMessage( + id = MessageId( + sessionAlias = "alias3", + direction = Direction.OUTGOING, + sequence = 3, + subsequence = mutableListOf(5, 6), + timestamp = Instant.now() + ), + metadata = mutableMapOf( + "prop5" to "value6", + "prop7" to "value8" + ), + protocol = "proto3", + type = "some-type", + rawBody = Unpooled.buffer().apply { writeCharSequence("{}", Charsets.UTF_8) } + ) + + val batch = GroupBatch( + book = "book1", + sessionGroup = "group1", + groups = mutableListOf(MessageGroup(mutableListOf(message1, message2, message3))) + ) + + GroupBatchCodec.encode(batch, buffer) + val decodedBatch = GroupBatchCodec.decode(buffer) + + assertEquals(batch, decodedBatch) + } + + @Test + fun `raw body is updated in parsed message when body is changed`() { + val parsedMessage = ParsedMessage.builder().apply { + idBuilder() + .setSessionAlias("alias1") + .setDirection(Direction.INCOMING) + .setSequence(1) + .addSubsequence(1) + .setTimestamp(Instant.now()) + setType("test") + setBody( + linkedMapOf( + "field" to 42, + "another" to "test_data", + ) + ) + }.build() + + val dest = Unpooled.buffer() + ParsedMessageCodec.encode(parsedMessage, dest) + val decoded = ParsedMessageCodec.decode(dest) + assertEquals(0, dest.readableBytes()) { "unexpected bytes left: ${ByteBufUtil.hexDump(dest)}" } + + assertEquals(parsedMessage, decoded, "unexpected parsed result decoded") + assertEquals( + Unpooled.buffer().apply { + writeCharSequence("{\"field\":42,\"another\":\"test_data\"}", Charsets.UTF_8) + }, + decoded.rawBody, + "unexpected raw body", + ) + } + + @TestFactory + fun dateTypesTests(): Collection { + LocalTime.parse("16:36:38.035420").toString() + val testData = listOf String>>( + LocalDate.now() to TemporalAccessor::toString, + LocalTime.now() to DateTimeFormatter.ISO_LOCAL_TIME::format, + // Check case when LocalTime.toString() around nanos to 1000 + LocalTime.parse("16:36:38.035420") to DateTimeFormatter.ISO_LOCAL_TIME::format, + LocalDateTime.now() to DateTimeFormatter.ISO_LOCAL_DATE_TIME::format, + Instant.now() to TemporalAccessor::toString, + ) + return testData.map { (value, formatter) -> + DynamicTest.dynamicTest("serializes ${value::class.simpleName} as field") { + val parsedMessage = ParsedMessage.builder().apply { + setId(MessageId.DEFAULT) + setType("test") + setBody( + linkedMapOf( + "field" to value, + ) + ) + }.build() + + val dest = Unpooled.buffer() + ParsedMessageCodec.encode(parsedMessage, dest) + val decoded = ParsedMessageCodec.decode(dest) + assertEquals(0, dest.readableBytes()) { "unexpected bytes left: ${ByteBufUtil.hexDump(dest)}" } + + assertEquals(parsedMessage, decoded, "unexpected parsed result decoded") + assertEquals( + "{\"field\":\"${formatter(value)}\"}", + decoded.rawBody.toString(Charsets.UTF_8), + "unexpected raw body", + ) + } + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/ParsedMessageTest.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/ParsedMessageTest.kt new file mode 100644 index 000000000..23d092e27 --- /dev/null +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/ParsedMessageTest.kt @@ -0,0 +1,201 @@ +/* + * Copyright 2023 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.exactpro.th2.common.schema.message.impl.rabbitmq.transport + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertAll +import java.time.Instant +import kotlin.random.Random +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class ParsedMessageTest { + + private val testBody = hashMapOf("test-field" to "test-filed-value") + private val testMetadata = hashMapOf("test-property" to "test-property-value") + private val testTimestamp = Instant.now() + + @Test + fun `builder test`() { + val builder = ParsedMessage.builder().apply { + setType(TEST_MESSAGE_TYPE) + setProtocol(TEST_PROTOCOL) + setBody(testBody) + setMetadata(testMetadata) + idBuilder().apply { + setSessionAlias(TEST_SESSION_ALIAS) + setDirection(Direction.OUTGOING) + setSequence(TEST_SEQUENCE) + setSubsequence(TEST_SUB_SEQUENCE) + setTimestamp(testTimestamp) + } + setEventId(EventId.builder().apply { + setBook(TEST_BOOK) + setScope(TEST_SCOPE) + setTimestamp(testTimestamp) + setId(TEST_EVENT_ID) + }.build()) + } + + with(builder) { + assertEquals(TEST_MESSAGE_TYPE, type) + assertEquals(TEST_PROTOCOL, protocol) + with(bodyBuilder()) { + assertEquals(testBody.size, size) + assertAll(testBody.map { (key, value) -> + { assertEquals(value, get(key), "Check '$key' field") } + }) + } + with(metadataBuilder()) { + assertEquals(testMetadata.size, size) + assertAll(testMetadata.map { (key, value) -> + { assertEquals(value, get(key), "Check '$key' field") } + }) + } + with(idBuilder()) { + assertEquals(TEST_SESSION_ALIAS, sessionAlias) + assertEquals(Direction.OUTGOING, direction) + assertEquals(TEST_SEQUENCE, sequence) + with(subsequenceBuilder()) { + assertEquals(TEST_SUB_SEQUENCE.size, size) + assertAll(TEST_SUB_SEQUENCE.mapIndexed { index, value -> + { assertEquals(value, get(index), "Check '$index' index") } + }) + } + assertEquals(testTimestamp, timestamp) + } + with(assertNotNull(eventId)) { + assertEquals(TEST_BOOK, book) + assertEquals(TEST_SCOPE, scope) + assertEquals(testTimestamp, testTimestamp) + assertEquals(TEST_EVENT_ID, id) + } + } + + with(builder.build()) { + assertEquals(TEST_MESSAGE_TYPE, type) + assertEquals(TEST_PROTOCOL, protocol) + assertEquals(testBody, body) + assertEquals(testMetadata, metadata) + with(id) { + assertEquals(TEST_SESSION_ALIAS, sessionAlias) + assertEquals(Direction.OUTGOING, direction) + assertEquals(TEST_SEQUENCE, sequence) + assertEquals(TEST_SUB_SEQUENCE, subsequence) + assertEquals(testTimestamp, timestamp) + } + with(assertNotNull(eventId)) { + assertEquals(TEST_BOOK, book) + assertEquals(TEST_SCOPE, scope) + assertEquals(testTimestamp, testTimestamp) + assertEquals(TEST_EVENT_ID, id) + } + } + } + + @Test + fun `toBuilder test`() { + val message = ParsedMessage.builder().apply { + setType(TEST_MESSAGE_TYPE) + setProtocol(TEST_PROTOCOL) + setBody(testBody) + setMetadata(testMetadata) + idBuilder().apply { + setSessionAlias(TEST_SESSION_ALIAS) + setDirection(Direction.OUTGOING) + setSequence(TEST_SEQUENCE) + setSubsequence(TEST_SUB_SEQUENCE) + setTimestamp(testTimestamp) + } + setEventId(EventId.builder().apply { + setBook(TEST_BOOK) + setScope(TEST_SCOPE) + setTimestamp(testTimestamp) + setId(TEST_EVENT_ID) + }.build()) + }.build() + + val builder = message.toBuilder() + with(builder) { + assertEquals(TEST_MESSAGE_TYPE, type) + assertEquals(TEST_PROTOCOL, protocol) + with(bodyBuilder()) { + assertEquals(testBody.size, size) + assertAll(testBody.map { (key, value) -> + { assertEquals(value, get(key), "Check '$key' field") } + }) + } + with(metadataBuilder()) { + assertEquals(testMetadata.size, size) + assertAll(testMetadata.map { (key, value) -> + { assertEquals(value, get(key), "Check '$key' field") } + }) + } + with(idBuilder()) { + assertEquals(TEST_SESSION_ALIAS, sessionAlias) + assertEquals(Direction.OUTGOING, direction) + assertEquals(TEST_SEQUENCE, sequence) + with(subsequenceBuilder()) { + assertEquals(TEST_SUB_SEQUENCE.size, size) + assertAll(TEST_SUB_SEQUENCE.mapIndexed { index, value -> + { assertEquals(value, get(index), "Check '$index' index") } + }) + } + assertEquals(testTimestamp, timestamp) + } + with(assertNotNull(eventId)) { + assertEquals(TEST_BOOK, book) + assertEquals(TEST_SCOPE, scope) + assertEquals(testTimestamp, testTimestamp) + assertEquals(TEST_EVENT_ID, id) + } + } + + with(builder.build()) { + assertEquals(TEST_MESSAGE_TYPE, type) + assertEquals(TEST_PROTOCOL, protocol) + assertEquals(testBody, body) + assertEquals(testMetadata, metadata) + with(id) { + assertEquals(TEST_SESSION_ALIAS, sessionAlias) + assertEquals(Direction.OUTGOING, direction) + assertEquals(TEST_SEQUENCE, sequence) + assertEquals(TEST_SUB_SEQUENCE, subsequence) + assertEquals(testTimestamp, timestamp) + } + with(assertNotNull(eventId)) { + assertEquals(TEST_BOOK, book) + assertEquals(TEST_SCOPE, scope) + assertEquals(testTimestamp, testTimestamp) + assertEquals(TEST_EVENT_ID, id) + } + } + } + + companion object { + private const val TEST_BOOK = "test-book" + private const val TEST_SCOPE = "test-scope" + private const val TEST_EVENT_ID = "test-event-id" + private const val TEST_PROTOCOL = "test-protocol" + private const val TEST_SESSION_ALIAS = "test-session-alias" + private const val TEST_MESSAGE_TYPE = "test-message-type" + private val TEST_SEQUENCE = Random.nextLong(from = Long.MIN_VALUE + 1, until = Long.MAX_VALUE) + private val TEST_SUB_SEQUENCE = listOf( + Random.nextInt(from = 0, until = Int.MAX_VALUE), + Random.nextInt(from = 0, until = Int.MAX_VALUE), + ) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/TransportGroupBatchRouterIntegrationTest.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/TransportGroupBatchRouterIntegrationTest.kt new file mode 100644 index 000000000..ffbbd5702 --- /dev/null +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/TransportGroupBatchRouterIntegrationTest.kt @@ -0,0 +1,147 @@ +/* + * Copyright 2023 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.exactpro.th2.common.schema.message.impl.rabbitmq.transport + +import com.exactpro.th2.common.annotations.IntegrationTest +import com.exactpro.th2.common.schema.box.configuration.BoxConfiguration +import com.exactpro.th2.common.schema.message.configuration.MessageRouterConfiguration +import com.exactpro.th2.common.schema.message.impl.context.DefaultMessageRouterContext +import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.ConnectionManagerConfiguration +import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.RabbitMQConfiguration +import com.exactpro.th2.common.schema.message.impl.rabbitmq.connection.ConnectionManager +import com.rabbitmq.client.BuiltinExchangeType +import mu.KotlinLogging +import org.junit.jupiter.api.Test +import org.mockito.kotlin.mock +import org.testcontainers.containers.RabbitMQContainer +import org.testcontainers.utility.DockerImageName +import java.time.Duration +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import kotlin.test.assertTrue + +@IntegrationTest +class TransportGroupBatchRouterIntegrationTest { + + @Test + fun `subscribe to exclusive queue`() { + RabbitMQContainer(DockerImageName.parse(RABBITMQ_3_8_MANAGEMENT_ALPINE)) + .use { rabbitMQContainer -> + rabbitMQContainer.start() + LOGGER.info { "Started with port ${rabbitMQContainer.amqpPort}" } + + createConnectionManager(rabbitMQContainer).use { firstManager -> + createRouter(firstManager).use { firstRouter -> + createConnectionManager(rabbitMQContainer).use { secondManager -> + createRouter(secondManager).use { secondRouter -> + val counter = CountDownLatch(1) + val monitor = firstRouter.subscribeExclusive { _, _ -> counter.countDown() } + try { + secondRouter.sendExclusive(monitor.queue, GroupBatch.builder() + .setBook("") + .setSessionGroup("") + .build()) + assertTrue("Message is not received") { counter.await(1, TimeUnit.SECONDS) } + + } finally { + monitor.unsubscribe() + } + } + } + } + } + } + } + + @Test + fun `send receive message group batch`() { + RabbitMQContainer(DockerImageName.parse(RABBITMQ_3_8_MANAGEMENT_ALPINE)) + .withExchange(EXCHANGE, BuiltinExchangeType.DIRECT.type, false, false, true, emptyMap()) + .withQueue(QUEUE_NAME) + .withBinding(EXCHANGE, QUEUE_NAME, emptyMap(), ROUTING_KEY, "queue") + .use { rabbitMQContainer -> + rabbitMQContainer.start() + LOGGER.info { "Started with port ${rabbitMQContainer.amqpPort}" } + + createConnectionManager(rabbitMQContainer).use { firstManager -> + createRouter(firstManager).use { firstRouter -> + createConnectionManager(rabbitMQContainer).use { secondManager -> + createRouter(secondManager).use { secondRouter -> + val counter = CountDownLatch(1) + val monitor = firstRouter.subscribeExclusive { _, _ -> counter.countDown() } + try { + + secondRouter.sendExclusive(monitor.queue, GroupBatch.builder() + .setBook("") + .setSessionGroup("") + .build()) + assertTrue("Message is not received") { counter.await(1, TimeUnit.SECONDS) } + + } finally { + monitor.unsubscribe() + } + } + } + } + } + } + } + + private fun createRouter(connectionManager: ConnectionManager) = TransportGroupBatchRouter() + .apply { + init( + DefaultMessageRouterContext( + connectionManager, + mock { }, + MessageRouterConfiguration(), + BoxConfiguration() + ) + ) + } + + private fun createConnectionManager( + rabbitMQContainer: RabbitMQContainer, + prefetchCount: Int = DEFAULT_PREFETCH_COUNT, + confirmationTimeout: Duration = DEFAULT_CONFIRMATION_TIMEOUT + ) = ConnectionManager( + RabbitMQConfiguration( + host = rabbitMQContainer.host, + vHost = "", + port = rabbitMQContainer.amqpPort, + username = rabbitMQContainer.adminUsername, + password = rabbitMQContainer.adminPassword, + ), + ConnectionManagerConfiguration( + subscriberName = "test", + prefetchCount = prefetchCount, + confirmationTimeout = confirmationTimeout, + ), + ) { + LOGGER.error { "Fatal connection problem" } + } + + companion object { + private val LOGGER = KotlinLogging.logger { } + + private const val RABBITMQ_3_8_MANAGEMENT_ALPINE = "rabbitmq:3.8-management-alpine" + private const val ROUTING_KEY = "routingKey" + private const val QUEUE_NAME = "queue" + private const val EXCHANGE = "test-exchange" + + private const val DEFAULT_PREFETCH_COUNT = 10 + private val DEFAULT_CONFIRMATION_TIMEOUT: Duration = Duration.ofSeconds(1) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/TransportGroupBatchRouterTest.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/TransportGroupBatchRouterTest.kt new file mode 100644 index 000000000..27987afc2 --- /dev/null +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/TransportGroupBatchRouterTest.kt @@ -0,0 +1,315 @@ +/* + * Copyright 2023 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.exactpro.th2.common.schema.message.impl.rabbitmq.transport + +import com.exactpro.th2.common.event.bean.BaseTest.* +import com.exactpro.th2.common.grpc.MessageGroupBatch +import com.exactpro.th2.common.schema.message.ExclusiveSubscriberMonitor +import com.exactpro.th2.common.schema.message.MessageRouter +import com.exactpro.th2.common.schema.message.configuration.* +import com.exactpro.th2.common.schema.message.impl.context.DefaultMessageRouterContext +import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.ConnectionManagerConfiguration +import com.exactpro.th2.common.schema.message.impl.rabbitmq.connection.ConnectionManager +import com.exactpro.th2.common.schema.message.impl.rabbitmq.transport.TransportGroupBatchRouter.Companion.TRANSPORT_GROUP_ATTRIBUTE +import io.netty.buffer.Unpooled +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertArrayEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.function.Executable +import org.mockito.kotlin.* +import java.time.Instant + +class TransportGroupBatchRouterTest { + private val connectionConfiguration = ConnectionManagerConfiguration() + private val managerMonitor: ExclusiveSubscriberMonitor = mock { } + private val connectionManager: ConnectionManager = mock { + on { configuration }.thenReturn(connectionConfiguration) + on { basicConsume(any(), any(), any()) }.thenReturn(managerMonitor) + } + + @Nested + inner class Publishing { + private val router = createRouter( + mapOf( + "test-pine" to QueueConfiguration( + routingKey = "", + queue = "subscribe", + exchange = "test-exchange", + attributes = listOf("subscribe", TRANSPORT_GROUP_ATTRIBUTE) + ), + "test-pin1" to QueueConfiguration( + routingKey = "test", + queue = "", + exchange = "test-exchange", + attributes = listOf("publish", TRANSPORT_GROUP_ATTRIBUTE), + filters = listOf( + MqRouterFilterConfiguration( + metadata = listOf( + FieldFilterConfiguration( + fieldName = "message_type", + expectedValue = "test-message", + operation = FieldFilterOperation.EQUAL + ) + ) + ) + ) + ), + "test-pin2" to QueueConfiguration( + routingKey = "test2", + queue = "", + exchange = "test-exchange", + attributes = listOf("publish", TRANSPORT_GROUP_ATTRIBUTE, "test"), + filters = listOf( + MqRouterFilterConfiguration( + metadata = listOf( + FieldFilterConfiguration( + fieldName = "message_type", + expectedValue = "test-message", + operation = FieldFilterOperation.EQUAL + ) + ) + ) + ) + ) + ) + ) + + @Test + fun `publishes message group batch with metadata`() { + val batch = createGroupBatch("test-message") + + router.send(batch, "test") + + val captor = argumentCaptor() + verify(connectionManager).basicPublish(eq("test-exchange"), eq("test2"), anyOrNull(), captor.capture()) + val publishedBytes = captor.firstValue + assertArrayEquals(batch.toByteArray(), publishedBytes) { + "Unexpected batch published: ${MessageGroupBatch.parseFrom(publishedBytes)}" + } + } + + @Test + fun `does not publish anything if all messages are filtered`() { + router.send(createGroupBatch("test-message1")) + + verify(connectionManager, never()).basicPublish(any(), any(), anyOrNull(), any()) + } + + @Test + fun `publishes to the correct pin according to attributes`() { + val batch = createGroupBatch("test-message") + router.send(batch, "test") + + val captor = argumentCaptor() + verify(connectionManager).basicPublish(eq("test-exchange"), eq("test2"), anyOrNull(), captor.capture()) + val publishedBytes = captor.firstValue + assertArrayEquals(batch.toByteArray(), publishedBytes) { + "Unexpected batch published: ${MessageGroupBatch.parseFrom(publishedBytes)}" + } + } + + @Test + fun `reports about extra pins matches the publication`() { + assertThrows(IllegalStateException::class.java) { + router.send(createGroupBatch("test-message")) + }.apply { + Assertions.assertEquals( + "Found incorrect number of pins [test-pin1, test-pin2] to the send operation by attributes [publish, $TRANSPORT_GROUP_ATTRIBUTE] and filters, expected 1, actual 2", + message + ) + } + } + + @Test + fun `reports about no pins matches the publication`() { + assertThrows(IllegalStateException::class.java) { + router.send(TRANSPORT_BATCH, "unexpected") + }.apply { + Assertions.assertEquals( + "Found incorrect number of pins [] to the send operation by attributes [unexpected, publish, $TRANSPORT_GROUP_ATTRIBUTE] and filters, expected 1, actual 0", + message + ) + } + } + + @Test + fun `publishes to all correct pin according to attributes`() { + val batch = createGroupBatch("test-message") + router.sendAll(batch) + + val captor = argumentCaptor() + verify(connectionManager).basicPublish(eq("test-exchange"), eq("test"), anyOrNull(), captor.capture()) + verify(connectionManager).basicPublish(eq("test-exchange"), eq("test2"), anyOrNull(), captor.capture()) + val originalBytes = batch.toByteArray() + Assertions.assertAll( + Executable { + val publishedBytes = captor.firstValue + assertArrayEquals(originalBytes, publishedBytes) { + "Unexpected batch published: ${MessageGroupBatch.parseFrom(publishedBytes)}" + } + }, + Executable { + val publishedBytes = captor.secondValue + assertArrayEquals(originalBytes, publishedBytes) { + "Unexpected batch published: ${MessageGroupBatch.parseFrom(publishedBytes)}" + } + } + ) + } + } + + @Nested + inner class Subscribing { + private val router = createRouter( + mapOf( + "test" to QueueConfiguration( + routingKey = "publish", + queue = "", + exchange = "test-exchange", + attributes = listOf("publish", TRANSPORT_GROUP_ATTRIBUTE, "test") + ), + "test1" to QueueConfiguration( + routingKey = "", + queue = "queue1", + exchange = "test-exchange", + attributes = listOf("subscribe", TRANSPORT_GROUP_ATTRIBUTE, "1") + ), + "test2" to QueueConfiguration( + routingKey = "", + queue = "queue2", + exchange = "test-exchange", + attributes = listOf("subscribe", TRANSPORT_GROUP_ATTRIBUTE, "2") + ) + ) + ) + + @Test + fun `publishes message group batch with metadata`() { + val batch = createGroupBatch("test-message") + router.send(batch, "test") + + val captor = argumentCaptor() + verify(connectionManager).basicPublish(eq("test-exchange"), eq("publish"), anyOrNull(), captor.capture()) + val publishedBytes = captor.firstValue + assertArrayEquals(batch.toByteArray(), publishedBytes) { + "Unexpected batch published: ${MessageGroupBatch.parseFrom(publishedBytes)}" + } + } + + @Test + fun `reports if more that one queue matches`() { + assertThrows(IllegalStateException::class.java) { router.subscribe(mock { }) } + .apply { + Assertions.assertEquals( + "Found incorrect number of pins [test1, test2] to subscribe operation by attributes [subscribe, $TRANSPORT_GROUP_ATTRIBUTE] and filters, expected 1, actual 2", + message + ) + } + } + + @Test + fun `reports if no queue matches`() { + Assertions.assertAll( + Executable { + assertThrows(IllegalStateException::class.java) { + router.subscribe( + mock { }, + "unexpected" + ) + } + .apply { + Assertions.assertEquals( + "Found incorrect number of pins [] to subscribe operation by attributes [unexpected, subscribe, $TRANSPORT_GROUP_ATTRIBUTE] and filters, expected 1, actual 0", + message + ) + } + }, + Executable { + assertThrows(IllegalStateException::class.java) { + router.subscribeAll( + mock { }, + "unexpected" + ) + } + .apply { + Assertions.assertEquals( + "Found incorrect number of pins [] to subscribe all operation by attributes [unexpected, subscribe, $TRANSPORT_GROUP_ATTRIBUTE] and filters, expected 1 or more, actual 0", + message + ) + } + } + ) + } + } + + private fun createGroupBatch(messageType: String) = GroupBatch( + BOOK_NAME, + SESSION_GROUP, + mutableListOf( + MessageGroup( + mutableListOf( + ParsedMessage( + MessageId.builder() + .setSessionAlias(SESSION_ALIAS) + .setDirection(Direction.INCOMING) + .setSequence(1) + .setTimestamp(Instant.now()) + .build(), + type = messageType + ) + ) + ) + ) + ) + + private fun createRouter(pins: Map): MessageRouter = + TransportGroupBatchRouter().apply { + init( + DefaultMessageRouterContext( + connectionManager, + mock { }, + MessageRouterConfiguration(pins, GlobalNotificationConfiguration()), + BOX_CONFIGURATION + ) + ) + } + + companion object { + private val TRANSPORT_BATCH = GroupBatch( + BOOK_NAME, + SESSION_GROUP, + mutableListOf( + MessageGroup( + mutableListOf( + RawMessage( + MessageId.builder() + .setSessionAlias(SESSION_ALIAS) + .setDirection(Direction.INCOMING) + .setSequence(1) + .setTimestamp(Instant.now()) + .build(), + body = Unpooled.wrappedBuffer(byteArrayOf(1, 2, 3)) + ) + ) + ) + ) + ) + } +} diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/TransportUtilsTest.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/TransportUtilsTest.kt new file mode 100644 index 000000000..619c8dcf8 --- /dev/null +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/TransportUtilsTest.kt @@ -0,0 +1,280 @@ +/* + * Copyright 2023 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.exactpro.th2.common.schema.message.impl.rabbitmq.transport + +import com.exactpro.th2.common.schema.filter.strategy.impl.AbstractTh2MsgFilterStrategy.BOOK_KEY +import com.exactpro.th2.common.schema.filter.strategy.impl.AbstractTh2MsgFilterStrategy.DIRECTION_KEY +import com.exactpro.th2.common.schema.filter.strategy.impl.AbstractTh2MsgFilterStrategy.MESSAGE_TYPE_KEY +import com.exactpro.th2.common.schema.filter.strategy.impl.AbstractTh2MsgFilterStrategy.PROTOCOL_KEY +import com.exactpro.th2.common.schema.filter.strategy.impl.AbstractTh2MsgFilterStrategy.SESSION_ALIAS_KEY +import com.exactpro.th2.common.schema.filter.strategy.impl.AbstractTh2MsgFilterStrategy.SESSION_GROUP_KEY +import com.exactpro.th2.common.schema.message.configuration.FieldFilterConfiguration +import com.exactpro.th2.common.schema.message.configuration.FieldFilterOperation.EQUAL +import com.exactpro.th2.common.schema.message.configuration.FieldFilterOperation.NOT_EMPTY +import com.exactpro.th2.common.schema.message.configuration.FieldFilterOperation.NOT_EQUAL +import com.exactpro.th2.common.schema.message.configuration.FieldFilterOperation.WILDCARD +import com.exactpro.th2.common.schema.message.configuration.MqRouterFilterConfiguration +import com.exactpro.th2.common.schema.message.configuration.RouterFilter +import com.exactpro.th2.common.util.emptyMultiMap +import org.apache.commons.collections4.MultiMapUtils +import org.junit.jupiter.api.DynamicTest +import org.junit.jupiter.api.TestFactory +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import java.time.Instant +import kotlin.test.assertNull +import kotlin.test.assertSame + +class TransportUtilsTest { + + private val bookA = "bookA" + private val bookB = "bookB" + + private val groupA = "groupA" + private val groupB = "groupB" + + private val msgType = "msg-type" + + private val directionA = "SECOND" + private val directionB = "FIRST" + private val protocolA = "protocolA" + private val protocolB = "protocolB" + + private val routerFilters = listOf( + MqRouterFilterConfiguration( + MultiMapUtils.newListValuedHashMap().apply { + putAll( + BOOK_KEY, listOf( + FieldFilterConfiguration(BOOK_KEY, bookA, EQUAL), + FieldFilterConfiguration(BOOK_KEY, "*A", WILDCARD), + FieldFilterConfiguration(BOOK_KEY, null, NOT_EMPTY), + FieldFilterConfiguration(BOOK_KEY, bookB, NOT_EQUAL) + ) + ) + put(SESSION_GROUP_KEY, FieldFilterConfiguration(SESSION_GROUP_KEY, "*A", WILDCARD)) + put(SESSION_ALIAS_KEY, FieldFilterConfiguration(SESSION_ALIAS_KEY, null, NOT_EMPTY)) + put(MESSAGE_TYPE_KEY, FieldFilterConfiguration(MESSAGE_TYPE_KEY, null, NOT_EMPTY)) + put(DIRECTION_KEY, FieldFilterConfiguration(DIRECTION_KEY, directionB, NOT_EQUAL)) + put(PROTOCOL_KEY, FieldFilterConfiguration(PROTOCOL_KEY, protocolA, EQUAL)) + }, + emptyMultiMap() + ), + MqRouterFilterConfiguration( + MultiMapUtils.newListValuedHashMap().apply { + putAll( + BOOK_KEY, listOf( + FieldFilterConfiguration(BOOK_KEY, bookB, EQUAL), + FieldFilterConfiguration(BOOK_KEY, "*B", WILDCARD), + FieldFilterConfiguration(BOOK_KEY, null, NOT_EMPTY), + FieldFilterConfiguration(BOOK_KEY, bookA, NOT_EQUAL) + ) + ) + put(SESSION_GROUP_KEY, FieldFilterConfiguration(SESSION_GROUP_KEY, "*B", WILDCARD)) + put(SESSION_ALIAS_KEY, FieldFilterConfiguration(SESSION_ALIAS_KEY, null, NOT_EMPTY)) + put(MESSAGE_TYPE_KEY, FieldFilterConfiguration(MESSAGE_TYPE_KEY, null, NOT_EMPTY)) + put(DIRECTION_KEY, FieldFilterConfiguration(DIRECTION_KEY, directionA, NOT_EQUAL)) + put(PROTOCOL_KEY, FieldFilterConfiguration(PROTOCOL_KEY, protocolB, EQUAL)) + }, + emptyMultiMap() + ) + ) + + @ParameterizedTest + @ValueSource(strings = ["", "data"]) + fun `empty filter test`(strValue: String) { + val group = GroupBatch.builder() + .setBook(strValue) + .setSessionGroup(strValue) + .build() + assertSame(group, listOf().filter(group)) + + } + + @TestFactory + fun `filter test`(): Collection { + return listOf( + DynamicTest.dynamicTest("empty batch") { + val batch = GroupBatch.builder() + .setBook("") + .setSessionGroup("") + .addGroup( + MessageGroup.builder() + .addMessage( + ParsedMessage.builder() + .setId( + MessageId.builder() + .setSessionAlias("") + .setDirection(Direction.OUTGOING) + .setSequence(1) + .setTimestamp(Instant.now()) + .build() + ) + .setType("") + .setBody(emptyMap()) + .build() + ) + .build() + ).build() + assertNull(routerFilters.filter(batch)) + }, + DynamicTest.dynamicTest("only book match") { + val batch = GroupBatch.builder() + .setBook(bookA) + .setSessionGroup("") + .addGroup( + MessageGroup.builder() + .addMessage( + ParsedMessage.builder() + .setId( + MessageId.builder() + .setSessionAlias("") + .setDirection(Direction.OUTGOING) + .setSequence(1) + .setTimestamp(Instant.now()) + .build() + ) + .setType("") + .setBody(emptyMap()) + .build() + ) + .build() + ).build() + assertNull(routerFilters.filter(batch)) + }, + DynamicTest.dynamicTest("only book and group match") { + val batch = GroupBatch.builder() + .setBook(bookA) + .setSessionGroup(groupA) + .addGroup( + MessageGroup.builder() + .addMessage( + ParsedMessage.builder() + .setId( + MessageId.builder() + .setSessionAlias("") + .setDirection(Direction.OUTGOING) + .setSequence(1) + .setTimestamp(Instant.now()) + .build() + ) + .setType("") + .setBody(emptyMap()) + .build() + ) + .build() + ).build() + assertNull(routerFilters.filter(batch)) + }, + DynamicTest.dynamicTest("only book, group, protocol match") { + val batch = GroupBatch.builder() + .setBook(bookA) + .setSessionGroup(groupA) + .addGroup( + MessageGroup.builder() + .addMessage( + ParsedMessage.builder() + .setProtocol(protocolA) + .setId( + MessageId.builder() + .setSessionAlias("") + .setDirection(Direction.OUTGOING) + .setSequence(1) + .setTimestamp(Instant.now()) + .build() + ) + .setType("") + .setBody(emptyMap()) + .build() + ) + .build() + ).build() + assertNull(routerFilters.filter(batch)) + }, + DynamicTest.dynamicTest("with partial message match") { + val batch = GroupBatch.builder() + .setBook(bookA) + .setSessionGroup(groupA) + .addGroup( + MessageGroup.builder() + .addMessage( + ParsedMessage.builder() + .setProtocol(protocolA) + .setId( + MessageId.builder() + .setSessionAlias("") + .setDirection(Direction.OUTGOING) + .setSequence(1) + .setTimestamp(Instant.now()) + .build() + ) + .setType(msgType) + .setBody(emptyMap()) + .build() + ) + .build() + ).build() + assertNull(routerFilters.filter(batch)) + }, + DynamicTest.dynamicTest("full match for A") { + val batch = GroupBatch.builder() + .setBook(bookA) + .setSessionGroup(groupA) + .addGroup( + MessageGroup.builder() + .addMessage( + ParsedMessage.builder() + .setType(msgType) + .setBody(emptyMap()) + .setProtocol(protocolA) + .apply { + idBuilder() + .setSessionAlias("alias") + .setDirection(Direction.OUTGOING) + .setSequence(1) + .setTimestamp(Instant.now()) + }.build() + ) + .build() + ).build() + assertSame(batch, routerFilters.filter(batch)) + }, + DynamicTest.dynamicTest("full match for B") { + val batch = GroupBatch.builder() + .setBook(bookB) + .setSessionGroup(groupB) + .addGroup( + MessageGroup.builder() + .addMessage( + ParsedMessage.builder() + .setType(msgType) + .setBody(emptyMap()) + .setProtocol(protocolB) + .apply { + idBuilder() + .setSessionAlias("alias") + .setDirection(Direction.INCOMING) + .setSequence(1) + .setTimestamp(Instant.now()) + }.build() + ) + .build() + ).build() + assertSame(batch, routerFilters.filter(batch)) + }, + ) + } +} \ No newline at end of file diff --git a/src/test/resources/log4j2.properties b/src/test/resources/log4j2.properties new file mode 100644 index 000000000..db0ed801f --- /dev/null +++ b/src/test/resources/log4j2.properties @@ -0,0 +1,40 @@ +################################################################################ +# Copyright 2022 Exactpro (Exactpro Systems Limited) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +################################################################################ +name = CommonJConfig +# Logging level related to initialization of Log4j +status = warn +# Package where to search plugins +packages = io.prometheus.client.log4j2 + +# Console appender configuration +appender.console.type = Console +appender.console.name = ConsoleLogger +appender.console.layout.type = PatternLayout +appender.console.layout.pattern = %d{dd MMM yyyy HH:mm:ss,SSS} %-6p [%-15t] %c - %m%n + +# Prometheus appender plugin configuration +appender.Prometheus.name = Prometheus +appender.Prometheus.type = Prometheus + +logger.prometheusLogger.name= prometheusLogger +logger.prometheusLogger.level= INFO +logger.prometheusLogger.appenderRef.ReportingAppender.ref= Prometheus + +# Root logger level +rootLogger.level = INFO +# Root logger referring to console appender +rootLogger.appenderRef.stdout.ref = ConsoleLogger + diff --git a/src/test/resources/test_common_factory_load_configs/custom.json b/src/test/resources/test_common_factory_load_configs/custom.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/src/test/resources/test_common_factory_load_configs/custom.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/test/resources/test_json_configurations/cassandra_storage_settings.json b/src/test/resources/test_json_configurations/cassandra_storage_settings.json new file mode 100644 index 000000000..873e938c8 --- /dev/null +++ b/src/test/resources/test_json_configurations/cassandra_storage_settings.json @@ -0,0 +1,21 @@ +{ + "timeout": 4999, + "keyspace": "test-keyspace", + "keyspaceReplicationFactor": 1, + "maxParallelQueries": 1, + "resultPageSize": 2, + "maxMessageBatchSize": 3, + "maxUncompressedMessageBatchSize": 5, + "maxTestEventBatchSize": 8, + "maxUncompressedTestEventSize": 13, + "sessionsCacheSize": 21, + "scopesCacheSize": 34, + "pageSessionsCacheSize": 55, + "pageScopesCacheSize": 89, + "sessionStatisticsCacheSize": 144, + "pageGroupsCacheSize": 233, + "groupsCacheSize": 377, + "eventBatchDurationCacheSize": 610, + "counterPersistenceInterval": 987, + "composingServiceThreads": 1597 +} \ No newline at end of file diff --git a/src/test/resources/test_json_configurations/cradle_confidential.json b/src/test/resources/test_json_configurations/cradle_confidential.json index e42ae5261..b38a5c6ee 100644 --- a/src/test/resources/test_json_configurations/cradle_confidential.json +++ b/src/test/resources/test_json_configurations/cradle_confidential.json @@ -4,6 +4,5 @@ "keyspace": "keyspace", "port": 1234, "username": "user", - "password": "pass", - "cradleInstanceName": "instance" + "password": "pass" } \ No newline at end of file diff --git a/src/test/resources/test_json_configurations/cradle_non_confidential.json b/src/test/resources/test_json_configurations/cradle_non_confidential.json index e9967c45b..f423c1080 100644 --- a/src/test/resources/test_json_configurations/cradle_non_confidential.json +++ b/src/test/resources/test_json_configurations/cradle_non_confidential.json @@ -1,7 +1,8 @@ { - "timeout": 888, "pageSize": 111, "cradleMaxEventBatchSize": 123, "cradleMaxMessageBatchSize": 321, - "prepareStorage": false + "prepareStorage": false, + "statisticsPersistenceIntervalMillis": 5000, + "maxUncompressedEventBatchSize": 1280002 } \ No newline at end of file diff --git a/src/test/resources/test_json_configurations/cradle_non_confidential_combo.json b/src/test/resources/test_json_configurations/cradle_non_confidential_combo.json new file mode 100644 index 000000000..e8dbb4098 --- /dev/null +++ b/src/test/resources/test_json_configurations/cradle_non_confidential_combo.json @@ -0,0 +1,27 @@ +{ + "timeout": 4999, + "pageSize": 111, + "cradleMaxEventBatchSize": 123, + "cradleMaxMessageBatchSize": 321, + "prepareStorage": false, + "statisticsPersistenceIntervalMillis": 5000, + "maxUncompressedEventBatchSize": 1280002, + "keyspace": "test-keyspace", + "keyspaceReplicationFactor": 1, + "maxParallelQueries": 1, + "resultPageSize": 2, + "maxMessageBatchSize": 3, + "maxUncompressedMessageBatchSize": 5, + "maxTestEventBatchSize": 8, + "maxUncompressedTestEventSize": 13, + "sessionsCacheSize": 21, + "scopesCacheSize": 34, + "pageSessionsCacheSize": 55, + "pageScopesCacheSize": 89, + "sessionStatisticsCacheSize": 144, + "pageGroupsCacheSize": 233, + "groupsCacheSize": 377, + "eventBatchDurationCacheSize": 610, + "counterPersistenceInterval": 987, + "composingServiceThreads": 1597 +} \ No newline at end of file diff --git a/src/test/resources/test_json_configurations/grpc.json b/src/test/resources/test_json_configurations/grpc.json index 314389830..62e30d1a8 100644 --- a/src/test/resources/test_json_configurations/grpc.json +++ b/src/test/resources/test_json_configurations/grpc.json @@ -23,5 +23,6 @@ "host": "host123", "port": 1234, "workers": 58 - } + }, + "keepAliveInterval": 400 } \ No newline at end of file diff --git a/src/test/resources/test_json_configurations/message_router.json b/src/test/resources/test_json_configurations/message_router.json index dc8a69397..bb5e0cf2f 100644 --- a/src/test/resources/test_json_configurations/message_router.json +++ b/src/test/resources/test_json_configurations/message_router.json @@ -23,8 +23,8 @@ { "metadata": [ { - "fieldName": "session_alias", - "value": "test_session_alias", + "field-name": "session_alias", + "expected-value": "test_session_alias", "operation": "EQUAL" } ], diff --git a/src/test/resources/test_message_event_id_builders/box.json b/src/test/resources/test_message_event_id_builders/box.json new file mode 100644 index 000000000..20827da53 --- /dev/null +++ b/src/test/resources/test_message_event_id_builders/box.json @@ -0,0 +1,4 @@ +{ + "boxName": "config_box", + "bookName": "config_book" +} \ No newline at end of file diff --git a/src/testFixtures/kotlin/com/exactpro/th2/common/TestUtils.kt b/src/testFixtures/kotlin/com/exactpro/th2/common/TestUtils.kt index e21bf44c0..52605159b 100644 --- a/src/testFixtures/kotlin/com/exactpro/th2/common/TestUtils.kt +++ b/src/testFixtures/kotlin/com/exactpro/th2/common/TestUtils.kt @@ -175,6 +175,7 @@ fun Message.assertDouble(name: String, expected: Double? = null): Double { return actual } +@Suppress("UNCHECKED_CAST") fun Message.assertValue(name: String, expected: T? = null): T { this.assertContains(name) val actual = when (expected) { @@ -185,7 +186,7 @@ fun Message.assertValue(name: String, expected: T? = null): T { is List<*> -> this.getList(name) is String -> this.getString(name) null -> this[name] - else -> error("Cannot assert $name field value. Expected value type is not supported: ${expected!!::class.simpleName}") + else -> error("Cannot assert $name field value. Expected value type is not supported: ${expected.let { it::class.simpleName }}") }!! expected?.let { Assertions.assertEquals(expected, actual) {"Unexpected $name field value"} @@ -194,11 +195,11 @@ fun Message.assertValue(name: String, expected: T? = null): T { } private fun Message.withTimestamp(ts: Timestamp) = toBuilder().apply { - metadataBuilder.timestamp = ts + metadataBuilder.idBuilder.timestamp = ts }.build()!! private fun RawMessage.withTimestamp(ts: Timestamp) = toBuilder().apply { - metadataBuilder.timestamp = ts + metadataBuilder.idBuilder.timestamp = ts }.build()!! private fun AssertionFailedError.rewrap(additional: String) = AssertionFailedError( diff --git a/src/test/kotlin/com/exactpro/th2/common/annotations/IntegrationTest.kt b/src/testFixtures/kotlin/com/exactpro/th2/common/annotations/IntegrationTest.kt similarity index 100% rename from src/test/kotlin/com/exactpro/th2/common/annotations/IntegrationTest.kt rename to src/testFixtures/kotlin/com/exactpro/th2/common/annotations/IntegrationTest.kt diff --git a/suppressions.xml b/suppressions.xml new file mode 100644 index 000000000..de58f6de3 --- /dev/null +++ b/suppressions.xml @@ -0,0 +1,16 @@ + + + + + + ^pkg:maven/com\.exactpro\.th2/task-utils@.*$ + cpe:/a:utils_project:utils + + + + + .*/auto-value-1.10.1.jar/META-INF/maven/com.google.guava/guava/pom.xml + CVE-2023-2976 + CVE-2020-8908 + + \ No newline at end of file From 34c9ebe3769628a7668b47f2676c6283b84e3883 Mon Sep 17 00:00:00 2001 From: Oleg Smelov Date: Mon, 23 Oct 2023 13:28:19 +0400 Subject: [PATCH 29/51] fixes after merge --- .../rabbitmq/AbstractRabbitSubscriber.java | 2 +- .../connection/ConnectionManager.java | 24 +++++++- .../AbstractRabbitRouterIntegrationTest.kt | 7 +-- .../connection/TestConnectionManager.kt | 58 +++++++++---------- ...IntegrationTestRabbitMessageBatchRouter.kt | 5 +- ...ransportGroupBatchRouterIntegrationTest.kt | 5 +- 6 files changed, 55 insertions(+), 46 deletions(-) diff --git a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitSubscriber.java b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitSubscriber.java index a2f45266c..b9fa6cd7b 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitSubscriber.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitSubscriber.java @@ -153,7 +153,7 @@ private void subscribe() { consumerMonitor.updateAndGet(previous -> previous == EMPTY_INITIALIZER ? Suppliers.memoize(this::basicConsume) : previous) - .get(); // initialize subscribtion + .get(); // initialize subscription } catch (Exception e) { throw new IllegalStateException("Can not start listening", e); } diff --git a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java index 4340260e8..ac1a9251e 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java @@ -58,6 +58,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Collections; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -331,7 +332,7 @@ public boolean isOpen() { } @Override - public void close() { + public void close() throws IOException { if (connectionIsClosed.getAndSet(true)) { LOGGER.info("Connection manager already closed"); return; @@ -350,6 +351,10 @@ public void close() { } } + for (ChannelHolder channelHolder: channelsByPin.values()) { + channelHolder.channel.abort(); + } + shutdownExecutor(sharedExecutor, closeTimeout, "rabbit-shared"); shutdownExecutor(channelChecker, closeTimeout, "channel-checker"); } @@ -598,9 +603,9 @@ public RabbitMqSubscriberMonitor(ChannelHolder holder, @Override public void unsubscribe() throws IOException { holder.withLock(false, channel -> { - channelsByPin.values().remove(holder); +// channelsByPin.values().remove(holder); action.execute(channel, tag); - channel.abort(); +// channel.abort(); }); } } @@ -778,6 +783,19 @@ private static Iterator handleAndSleep( return iterator; } + public T mapWithLock(ChannelMapper mapper) throws IOException { + lock.lock(); + try { + Channel channel = getChannel(); + return mapper.map(channel); + } catch (IOException e) { + LOGGER.error("Operation failure on the {} channel", channel.getChannelNumber(), e); + throw e; + } finally { + lock.unlock(); + } + } + /** * Decreases the number of unacked messages. * If the number of unacked messages is less than {@link #maxCount} diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitRouterIntegrationTest.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitRouterIntegrationTest.kt index d432f2ccc..c63395c95 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitRouterIntegrationTest.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitRouterIntegrationTest.kt @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.exactpro.th2.common.schema.message.impl.rabbitmq import com.exactpro.th2.common.annotations.IntegrationTest @@ -166,9 +167,7 @@ class AbstractRabbitRouterIntegrationTest { prefetchCount = prefetchCount, confirmationTimeout = confirmationTimeout, ), - ) { - K_LOGGER.error { "Fatal connection problem" } - } + ) private fun createRouter(connectionManager: ConnectionManager) = RabbitCustomRouter( "test-custom-tag", @@ -203,7 +202,7 @@ class AbstractRabbitRouterIntegrationTest { companion object { private val K_LOGGER = KotlinLogging.logger { } - private const val RABBITMQ_MANAGEMENT_ALPINE = "rabbitmq:3.11.2-management-alpine" + private const val RABBITMQ_MANAGEMENT_ALPINE = "rabbitmq:3.8-management-alpine" private const val ROUTING_KEY = "routingKey" private const val QUEUE_NAME = "queue" private const val EXCHANGE = "test-exchange" diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt index 4ab7213a4..058fa2a9e 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.exactpro.th2.common.schema.message.impl.rabbitmq.connection import com.exactpro.th2.common.annotations.IntegrationTest @@ -43,6 +44,7 @@ import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertNotEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.testcontainers.containers.RabbitMQContainer import org.testcontainers.utility.DockerImageName @@ -820,33 +822,12 @@ class TestConnectionManager { configuration ) - companion object { - - private const val RABBIT_IMAGE_NAME = "rabbitmq:3.8-management-alpine" - private lateinit var rabbit: RabbitMQContainer - private const val PREFETCH_COUNT = 10 - private val CONFIRMATION_TIMEOUT = Duration.ofSeconds(1) - - @BeforeAll - @JvmStatic - fun initRabbit() { - rabbit = - RabbitMQContainer(DockerImageName.parse(RABBIT_IMAGE_NAME)) - rabbit.start() - } - - @AfterAll - @JvmStatic - fun closeRabbit() { - rabbit.close() - } - } - -} - @Test + @Disabled + // TODO: this test is no more relevant + // TODO: we need to change test scenario or remove it fun `connection manager exclusive queue test`() { - RabbitMQContainer(DockerImageName.parse("rabbitmq:3.8-management-alpine")) + RabbitMQContainer(DockerImageName.parse(RABBIT_IMAGE_NAME)) .use { rabbitMQContainer -> rabbitMQContainer.start() LOGGER.info { "Started with port ${rabbitMQContainer.amqpPort}" } @@ -909,8 +890,8 @@ class TestConnectionManager { private fun createConnectionManager( rabbitMQContainer: RabbitMQContainer, - prefetchCount: Int = DEFAULT_PREFETCH_COUNT, - confirmationTimeout: Duration = DEFAULT_CONFIRMATION_TIMEOUT + prefetchCount: Int = PREFETCH_COUNT, + confirmationTimeout: Duration = CONFIRMATION_TIMEOUT ) = ConnectionManager( RabbitMQConfiguration( host = rabbitMQContainer.host, @@ -924,12 +905,25 @@ class TestConnectionManager { prefetchCount = prefetchCount, confirmationTimeout = confirmationTimeout, ), - ) { - LOGGER.error { "Fatal connection problem" } - } + ) companion object { - private const val DEFAULT_PREFETCH_COUNT = 10 - private val DEFAULT_CONFIRMATION_TIMEOUT: Duration = Duration.ofSeconds(1) + private const val RABBIT_IMAGE_NAME = "rabbitmq:3.8-management-alpine" + private lateinit var rabbit: RabbitMQContainer + private const val PREFETCH_COUNT = 10 + private val CONFIRMATION_TIMEOUT = Duration.ofSeconds(1) + + @BeforeAll + @JvmStatic + fun initRabbit() { + rabbit = RabbitMQContainer(DockerImageName.parse(RABBIT_IMAGE_NAME)) + rabbit.start() + } + + @AfterAll + @JvmStatic + fun closeRabbit() { + rabbit.close() + } } } \ No newline at end of file diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/group/IntegrationTestRabbitMessageBatchRouter.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/group/IntegrationTestRabbitMessageBatchRouter.kt index 963825782..c6a5f65b1 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/group/IntegrationTestRabbitMessageBatchRouter.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/group/IntegrationTestRabbitMessageBatchRouter.kt @@ -12,6 +12,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.exactpro.th2.common.schema.message.impl.rabbitmq.group import com.exactpro.th2.common.annotations.IntegrationTest @@ -123,9 +124,7 @@ class IntegrationTestRabbitMessageGroupBatchRouter { prefetchCount = prefetchCount, confirmationTimeout = confirmationTimeout, ), - ) { - LOGGER.error { "Fatal connection problem" } - } + ) companion object { private val LOGGER = KotlinLogging.logger { } diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/TransportGroupBatchRouterIntegrationTest.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/TransportGroupBatchRouterIntegrationTest.kt index ffbbd5702..c553b8e68 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/TransportGroupBatchRouterIntegrationTest.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/TransportGroupBatchRouterIntegrationTest.kt @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.exactpro.th2.common.schema.message.impl.rabbitmq.transport import com.exactpro.th2.common.annotations.IntegrationTest @@ -129,9 +130,7 @@ class TransportGroupBatchRouterIntegrationTest { prefetchCount = prefetchCount, confirmationTimeout = confirmationTimeout, ), - ) { - LOGGER.error { "Fatal connection problem" } - } + ) companion object { private val LOGGER = KotlinLogging.logger { } From 3d8f7eff92608c14a706137dac0e8ed438e78a32 Mon Sep 17 00:00:00 2001 From: Oleg Smelov Date: Mon, 23 Oct 2023 17:16:25 +0400 Subject: [PATCH 30/51] fixes after review version update --- README.md | 14 ++++++++++---- gradle.properties | 5 +++-- .../schema/grpc/router/AbstractGrpcRouter.java | 3 +-- .../rabbitmq/connection/ConnectionManager.java | 7 ++++--- .../configuration/RabbitMQConfiguration.kt | 3 ++- 5 files changed, 20 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 1224e99c1..f8eac4807 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# th2 common library (Java) (5.6.1) +# th2 common library (Java) (5.7.0) ## Usage @@ -491,6 +491,14 @@ dependencies { ## Release notes +### 5.7.0-dev + ++ Added retry in case of a RabbitMQ channel or connection error (when possible). ++ Added InterruptedException to basicConsume method signature. ++ Added additional logging for RabbitMQ errors. ++ Fixed connection recovery delay time. ++ Integration tests for these scenarios. + ### 5.6.0-dev #### Added: @@ -591,8 +599,6 @@ dependencies { + `com.exactpro.th2.common.event.Event.toProto...()` by `parentEventId`/`bookName`/`(bookName + scope)` + Added `isRedelivered` flag to message ---- - ### 3.44.1 + Remove unused dependency @@ -948,4 +954,4 @@ dependencies { ### 3.0.1 + metrics related to time measurement of an incoming message handling (Raw / Parsed / Event) migrated to - Prometheus [histogram](https://prometheus.io/docs/concepts/metric_types/#histogram) + Prometheus [histogram](https://prometheus.io/docs/concepts/metric_types/#histogram) \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 5962a2f74..7ffb89415 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # -release_version=5.6.0 + +release_version=5.7.0 description='th2 common library (Java)' vcs_url=https://github.com/th2-net/th2-common-j -kapt.include.compile.classpath=false +kapt.include.compile.classpath=false \ No newline at end of file diff --git a/src/main/java/com/exactpro/th2/common/schema/grpc/router/AbstractGrpcRouter.java b/src/main/java/com/exactpro/th2/common/schema/grpc/router/AbstractGrpcRouter.java index 1ee5fca67..3774fb596 100644 --- a/src/main/java/com/exactpro/th2/common/schema/grpc/router/AbstractGrpcRouter.java +++ b/src/main/java/com/exactpro/th2/common/schema/grpc/router/AbstractGrpcRouter.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2023 Exactpro (Exactpro Systems Limited) * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -41,7 +41,6 @@ import java.util.Objects; import java.util.concurrent.ExecutorService; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; diff --git a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java index ac1a9251e..ba822ecaf 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java @@ -77,7 +77,7 @@ public class ConnectionManager implements AutoCloseable { public static final String EMPTY_ROUTING_KEY = ""; private static final Logger LOGGER = LoggerFactory.getLogger(ConnectionManager.class); - public final Connection connection; + private final Connection connection; private final Map channelsByPin = new ConcurrentHashMap<>(); private final AtomicBoolean connectionIsClosed = new AtomicBoolean(false); private final ConnectionManagerConfiguration configuration; @@ -264,8 +264,9 @@ private void recoverSubscriptionsOfChannel(Channel channel) { .stream() .filter(entry -> Objects.nonNull(entry.getValue().channel) && channel.getChannelNumber() == entry.getValue().channel.getChannelNumber()) .findAny(); - if (pinToChannelHolderOptional.isPresent()) { - var pinIdToChannelHolder = pinToChannelHolderOptional.get(); + + var pinIdToChannelHolder = pinToChannelHolderOptional.orElse(null); + if (pinIdToChannelHolder != null) { PinId pinId = pinIdToChannelHolder.getKey(); ChannelHolder channelHolder = pinIdToChannelHolder.getValue(); diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/configuration/RabbitMQConfiguration.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/configuration/RabbitMQConfiguration.kt index 7eac817db..ecd0f433a 100644 --- a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/configuration/RabbitMQConfiguration.kt +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/configuration/RabbitMQConfiguration.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2023 Exactpro (Exactpro Systems Limited) * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -64,6 +64,7 @@ data class ConnectionManagerConfiguration( data class RetryingDelay(val tryNumber: Int, val delay: Int) { companion object { + @JvmStatic fun getRecoveryDelay( numberOfTries: Int, minTime: Int, From 0b4ccbf9b1c5fe186c8284d546b46e5e5b0b60b5 Mon Sep 17 00:00:00 2001 From: Oleg Smelov Date: Wed, 25 Oct 2023 15:53:49 +0400 Subject: [PATCH 31/51] `isAutomaticRecoveryEnabled` added to `ConnectionManagerConfiguration` integration tests constants extracted to `ContainerConstants` class rabbitmq docker image version upgraded --- .../connection/ConnectionManager.java | 2 +- .../configuration/RabbitMQConfiguration.kt | 3 +- .../schema/message/ContainerConstants.kt | 16 ++++ .../AbstractRabbitRouterIntegrationTest.kt | 17 ++-- .../connection/TestConnectionManager.kt | 82 +++++++++---------- ...IntegrationTestRabbitMessageBatchRouter.kt | 21 ++--- ...ransportGroupBatchRouterIntegrationTest.kt | 19 ++--- 7 files changed, 84 insertions(+), 76 deletions(-) create mode 100644 src/test/kotlin/com/exactpro/th2/common/schema/message/ContainerConstants.kt diff --git a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java index ba822ecaf..9771512ed 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java @@ -191,7 +191,7 @@ private void turnOffReadiness(Throwable exception) { } }); - factory.setAutomaticRecoveryEnabled(true); + factory.setAutomaticRecoveryEnabled(configuration.isAutomaticRecoveryEnabled()); factory.setConnectionRecoveryTriggeringCondition(shutdownSignal -> !connectionIsClosed.get()); factory.setRecoveryDelayHandler(recoveryAttempts -> { diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/configuration/RabbitMQConfiguration.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/configuration/RabbitMQConfiguration.kt index ecd0f433a..aa6ce558c 100644 --- a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/configuration/RabbitMQConfiguration.kt +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/configuration/RabbitMQConfiguration.kt @@ -41,7 +41,8 @@ data class ConnectionManagerConfiguration( var retryTimeDeviationPercent: Int = 10, val messageRecursionLimit: Int = 100, val workingThreads: Int = 1, - val confirmationTimeout: Duration = Duration.ofMinutes(5) + val confirmationTimeout: Duration = Duration.ofMinutes(5), + val isAutomaticRecoveryEnabled: Boolean = true ) : Configuration() { init { check(workingThreads > 0) { "expected 'workingThreads' greater than 0 but was $workingThreads" } diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/ContainerConstants.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/ContainerConstants.kt new file mode 100644 index 000000000..957063094 --- /dev/null +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/ContainerConstants.kt @@ -0,0 +1,16 @@ +package com.exactpro.th2.common.schema.message + +import org.testcontainers.utility.DockerImageName +import java.time.Duration + +class ContainerConstants { + companion object { + val RABBITMQ_IMAGE_NAME: DockerImageName = DockerImageName.parse("rabbitmq:3.12.7-management-alpine") + const val ROUTING_KEY = "routingKey" + const val QUEUE_NAME = "queue" + const val EXCHANGE = "test-exchange" + + const val DEFAULT_PREFETCH_COUNT = 10 + val DEFAULT_CONFIRMATION_TIMEOUT: Duration = Duration.ofSeconds(1) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitRouterIntegrationTest.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitRouterIntegrationTest.kt index c63395c95..340720942 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitRouterIntegrationTest.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitRouterIntegrationTest.kt @@ -18,6 +18,12 @@ package com.exactpro.th2.common.schema.message.impl.rabbitmq import com.exactpro.th2.common.annotations.IntegrationTest import com.exactpro.th2.common.schema.box.configuration.BoxConfiguration +import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.DEFAULT_CONFIRMATION_TIMEOUT +import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.DEFAULT_PREFETCH_COUNT +import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.EXCHANGE +import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.QUEUE_NAME +import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.RABBITMQ_IMAGE_NAME +import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.ROUTING_KEY import com.exactpro.th2.common.schema.message.ManualAckDeliveryCallback import com.exactpro.th2.common.schema.message.configuration.MessageRouterConfiguration import com.exactpro.th2.common.schema.message.configuration.QueueConfiguration @@ -31,7 +37,6 @@ import mu.KotlinLogging import org.junit.jupiter.api.Assertions.assertNull import org.mockito.kotlin.mock import org.testcontainers.containers.RabbitMQContainer -import org.testcontainers.utility.DockerImageName import java.time.Duration import java.util.concurrent.ArrayBlockingQueue import java.util.concurrent.TimeUnit @@ -44,7 +49,7 @@ class AbstractRabbitRouterIntegrationTest { @Test fun `receive unconfirmed message after resubscribe`() { - RabbitMQContainer(DockerImageName.parse(RABBITMQ_MANAGEMENT_ALPINE)) + RabbitMQContainer(RABBITMQ_IMAGE_NAME) .withExchange(EXCHANGE, BuiltinExchangeType.DIRECT.type, false, false, true, emptyMap()) .withQueue(QUEUE_NAME, false, true, emptyMap()) .withBinding( @@ -202,14 +207,6 @@ class AbstractRabbitRouterIntegrationTest { companion object { private val K_LOGGER = KotlinLogging.logger { } - private const val RABBITMQ_MANAGEMENT_ALPINE = "rabbitmq:3.8-management-alpine" - private const val ROUTING_KEY = "routingKey" - private const val QUEUE_NAME = "queue" - private const val EXCHANGE = "test-exchange" - - private const val DEFAULT_PREFETCH_COUNT = 10 - private val DEFAULT_CONFIRMATION_TIMEOUT: Duration = Duration.ofSeconds(1) - private class Expectation( val message: String, val redelivery: Boolean, diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt index 058fa2a9e..11af50b92 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt @@ -17,6 +17,9 @@ package com.exactpro.th2.common.schema.message.impl.rabbitmq.connection import com.exactpro.th2.common.annotations.IntegrationTest +import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.DEFAULT_CONFIRMATION_TIMEOUT +import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.DEFAULT_PREFETCH_COUNT +import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.RABBITMQ_IMAGE_NAME import com.exactpro.th2.common.schema.message.ManualAckDeliveryCallback import com.exactpro.th2.common.schema.message.SubscriberMonitor import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.ConnectionManagerConfiguration @@ -47,7 +50,6 @@ import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.testcontainers.containers.RabbitMQContainer -import org.testcontainers.utility.DockerImageName import org.testcontainers.utility.MountableFile import java.io.IOException import kotlin.test.assertFailsWith @@ -68,13 +70,13 @@ class TestConnectionManager { declareQueue(rabbit, queueName) declareFanoutExchangeWithBinding(rabbit, exchange, queueName) LOGGER.info { "Started with port ${it.amqpPort}" } - val queue = ArrayBlockingQueue(PREFETCH_COUNT) - val countDown = CountDownLatch(PREFETCH_COUNT) + val queue = ArrayBlockingQueue(DEFAULT_PREFETCH_COUNT) + val countDown = CountDownLatch(DEFAULT_PREFETCH_COUNT) createConnectionManager( it, ConnectionManagerConfiguration( subscriberName = "test", - prefetchCount = PREFETCH_COUNT, - confirmationTimeout = CONFIRMATION_TIMEOUT, + prefetchCount = DEFAULT_PREFETCH_COUNT, + confirmationTimeout = DEFAULT_CONFIRMATION_TIMEOUT, ) ).use { manager -> manager.basicConsume(queueName, { _, delivery, ack -> @@ -85,7 +87,7 @@ class TestConnectionManager { LOGGER.info { "Canceled $it" } } - repeat(PREFETCH_COUNT + 1) { index -> + repeat(DEFAULT_PREFETCH_COUNT + 1) { index -> manager.basicPublish(exchange, routingKey, null, "Hello $index".toByteArray(Charsets.UTF_8)) } @@ -94,7 +96,7 @@ class TestConnectionManager { assertTrue(manager.isAlive) { "Manager should still be alive" } assertTrue(manager.isReady) { "Manager should be ready until the confirmation timeout expires" } - Thread.sleep(CONFIRMATION_TIMEOUT.toMillis() + 100/*just in case*/) // wait for confirmation timeout + Thread.sleep(DEFAULT_CONFIRMATION_TIMEOUT.toMillis() + 100/*just in case*/) // wait for confirmation timeout assertTrue(manager.isAlive) { "Manager should still be alive" } assertFalse(manager.isReady) { "Manager should not be ready" } @@ -107,7 +109,7 @@ class TestConnectionManager { val receivedData = generateSequence { queue.poll(10L, TimeUnit.MILLISECONDS) } .onEach(ManualAckDeliveryCallback.Confirmation::confirm) .count() - assertEquals(PREFETCH_COUNT, receivedData) { "Unexpected number of messages received" } + assertEquals(DEFAULT_PREFETCH_COUNT, receivedData) { "Unexpected number of messages received" } } } } @@ -122,8 +124,8 @@ class TestConnectionManager { rabbitMQContainer, ConnectionManagerConfiguration( subscriberName = "test", - prefetchCount = PREFETCH_COUNT, - confirmationTimeout = CONFIRMATION_TIMEOUT, + prefetchCount = DEFAULT_PREFETCH_COUNT, + confirmationTimeout = DEFAULT_CONFIRMATION_TIMEOUT, minConnectionRecoveryTimeout = 100, maxConnectionRecoveryTimeout = 200, maxRecoveryAttempts = 5 @@ -196,8 +198,8 @@ class TestConnectionManager { it, ConnectionManagerConfiguration( subscriberName = "test", - prefetchCount = PREFETCH_COUNT, - confirmationTimeout = CONFIRMATION_TIMEOUT, + prefetchCount = DEFAULT_PREFETCH_COUNT, + confirmationTimeout = DEFAULT_CONFIRMATION_TIMEOUT, minConnectionRecoveryTimeout = 100, maxConnectionRecoveryTimeout = 200, maxRecoveryAttempts = 5 @@ -251,7 +253,7 @@ class TestConnectionManager { val configFilename = "rabbitmq_it.conf" val queueName = "queue4" - RabbitMQContainer(DockerImageName.parse(RABBIT_IMAGE_NAME)) + RabbitMQContainer(RABBITMQ_IMAGE_NAME) .withRabbitMQConfig(MountableFile.forClasspathResource(configFilename)) .withQueue(queueName) .use { @@ -262,8 +264,8 @@ class TestConnectionManager { it, ConnectionManagerConfiguration( subscriberName = "test", - prefetchCount = PREFETCH_COUNT, - confirmationTimeout = CONFIRMATION_TIMEOUT, + prefetchCount = DEFAULT_PREFETCH_COUNT, + confirmationTimeout = DEFAULT_CONFIRMATION_TIMEOUT, minConnectionRecoveryTimeout = 100, maxConnectionRecoveryTimeout = 200, maxRecoveryAttempts = 5 @@ -316,7 +318,7 @@ class TestConnectionManager { val configFilename = "rabbitmq_it.conf" val queueNames = arrayOf("separate_queues1", "separate_queues2", "separate_queues3") - RabbitMQContainer(DockerImageName.parse(RABBIT_IMAGE_NAME)) + RabbitMQContainer(RABBITMQ_IMAGE_NAME) .withRabbitMQConfig(MountableFile.forClasspathResource(configFilename)) .withQueue(queueNames[0]) .withQueue(queueNames[1]) @@ -333,8 +335,8 @@ class TestConnectionManager { it, ConnectionManagerConfiguration( subscriberName = "test", - prefetchCount = PREFETCH_COUNT, - confirmationTimeout = CONFIRMATION_TIMEOUT, + prefetchCount = DEFAULT_PREFETCH_COUNT, + confirmationTimeout = DEFAULT_CONFIRMATION_TIMEOUT, minConnectionRecoveryTimeout = 100, maxConnectionRecoveryTimeout = 200, maxRecoveryAttempts = 5 @@ -410,7 +412,7 @@ class TestConnectionManager { fun `connection manager receives a messages after container restart`() { val queueName = "queue5" val amqpPort = 5672 - val container = object : RabbitMQContainer(DockerImageName.parse(RABBIT_IMAGE_NAME)) { + val container = object : RabbitMQContainer(RABBITMQ_IMAGE_NAME) { fun addFixedPort(hostPort: Int, containerPort: Int) { super.addFixedExposedPort(hostPort, containerPort) } @@ -433,8 +435,8 @@ class TestConnectionManager { ), ConnectionManagerConfiguration( subscriberName = "test", - prefetchCount = PREFETCH_COUNT, - confirmationTimeout = CONFIRMATION_TIMEOUT, + prefetchCount = DEFAULT_PREFETCH_COUNT, + confirmationTimeout = DEFAULT_CONFIRMATION_TIMEOUT, minConnectionRecoveryTimeout = 1000, maxConnectionRecoveryTimeout = 2000, connectionTimeout = 1000, @@ -489,8 +491,8 @@ class TestConnectionManager { it, ConnectionManagerConfiguration( subscriberName = "test", - prefetchCount = PREFETCH_COUNT, - confirmationTimeout = CONFIRMATION_TIMEOUT, + prefetchCount = DEFAULT_PREFETCH_COUNT, + confirmationTimeout = DEFAULT_CONFIRMATION_TIMEOUT, minConnectionRecoveryTimeout = 10000, maxConnectionRecoveryTimeout = 20000, connectionTimeout = 10000, @@ -538,7 +540,7 @@ class TestConnectionManager { val routingKey = "routingKey7" - RabbitMQContainer(DockerImageName.parse(RABBIT_IMAGE_NAME)) + RabbitMQContainer(RABBITMQ_IMAGE_NAME) .withRabbitMQConfig(MountableFile.forClasspathResource(configFilename)) .withExchange(exchange, BuiltinExchangeType.FANOUT.type, false, false, true, emptyMap()) .withQueue(queueName) @@ -551,8 +553,8 @@ class TestConnectionManager { it, ConnectionManagerConfiguration( subscriberName = "test", - prefetchCount = PREFETCH_COUNT, - confirmationTimeout = CONFIRMATION_TIMEOUT, + prefetchCount = DEFAULT_PREFETCH_COUNT, + confirmationTimeout = DEFAULT_CONFIRMATION_TIMEOUT, minConnectionRecoveryTimeout = 100, maxConnectionRecoveryTimeout = 200, maxRecoveryAttempts = 5 @@ -612,8 +614,8 @@ class TestConnectionManager { it, ConnectionManagerConfiguration( subscriberName = "test", - prefetchCount = PREFETCH_COUNT, - confirmationTimeout = CONFIRMATION_TIMEOUT, + prefetchCount = DEFAULT_PREFETCH_COUNT, + confirmationTimeout = DEFAULT_CONFIRMATION_TIMEOUT, minConnectionRecoveryTimeout = 100, maxConnectionRecoveryTimeout = 2000, connectionTimeout = 1000, @@ -663,8 +665,8 @@ class TestConnectionManager { it, ConnectionManagerConfiguration( subscriberName = "test", - prefetchCount = PREFETCH_COUNT, - confirmationTimeout = CONFIRMATION_TIMEOUT, + prefetchCount = DEFAULT_PREFETCH_COUNT, + confirmationTimeout = DEFAULT_CONFIRMATION_TIMEOUT, minConnectionRecoveryTimeout = 100, maxConnectionRecoveryTimeout = 2000, connectionTimeout = 1000, @@ -718,7 +720,7 @@ class TestConnectionManager { val configFilename = "rabbitmq_it.conf" val queueName = "queue" - RabbitMQContainer(DockerImageName.parse(RABBIT_IMAGE_NAME)) + RabbitMQContainer(RABBITMQ_IMAGE_NAME) .withRabbitMQConfig(MountableFile.forClasspathResource(configFilename)) .withQueue(queueName) .use { @@ -729,8 +731,8 @@ class TestConnectionManager { it, ConnectionManagerConfiguration( subscriberName = "test", - prefetchCount = PREFETCH_COUNT, - confirmationTimeout = CONFIRMATION_TIMEOUT, + prefetchCount = DEFAULT_PREFETCH_COUNT, + confirmationTimeout = DEFAULT_CONFIRMATION_TIMEOUT, minConnectionRecoveryTimeout = 100, maxConnectionRecoveryTimeout = 200, maxRecoveryAttempts = 5 @@ -809,7 +811,6 @@ class TestConnectionManager { assertEquals(target, func(), message) } - private fun createConnectionManager(container: RabbitMQContainer, configuration: ConnectionManagerConfiguration) = ConnectionManager( RabbitMQConfiguration( @@ -827,7 +828,7 @@ class TestConnectionManager { // TODO: this test is no more relevant // TODO: we need to change test scenario or remove it fun `connection manager exclusive queue test`() { - RabbitMQContainer(DockerImageName.parse(RABBIT_IMAGE_NAME)) + RabbitMQContainer(RABBITMQ_IMAGE_NAME) .use { rabbitMQContainer -> rabbitMQContainer.start() LOGGER.info { "Started with port ${rabbitMQContainer.amqpPort}" } @@ -890,8 +891,9 @@ class TestConnectionManager { private fun createConnectionManager( rabbitMQContainer: RabbitMQContainer, - prefetchCount: Int = PREFETCH_COUNT, - confirmationTimeout: Duration = CONFIRMATION_TIMEOUT + prefetchCount: Int = DEFAULT_PREFETCH_COUNT, + confirmationTimeout: Duration = DEFAULT_CONFIRMATION_TIMEOUT, + isAutomaticRecoveryEnabled: Boolean = true ) = ConnectionManager( RabbitMQConfiguration( host = rabbitMQContainer.host, @@ -904,19 +906,17 @@ class TestConnectionManager { subscriberName = "test", prefetchCount = prefetchCount, confirmationTimeout = confirmationTimeout, + isAutomaticRecoveryEnabled = isAutomaticRecoveryEnabled ), ) companion object { - private const val RABBIT_IMAGE_NAME = "rabbitmq:3.8-management-alpine" private lateinit var rabbit: RabbitMQContainer - private const val PREFETCH_COUNT = 10 - private val CONFIRMATION_TIMEOUT = Duration.ofSeconds(1) @BeforeAll @JvmStatic fun initRabbit() { - rabbit = RabbitMQContainer(DockerImageName.parse(RABBIT_IMAGE_NAME)) + rabbit = RabbitMQContainer(RABBITMQ_IMAGE_NAME) rabbit.start() } diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/group/IntegrationTestRabbitMessageBatchRouter.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/group/IntegrationTestRabbitMessageBatchRouter.kt index c6a5f65b1..1808c6b80 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/group/IntegrationTestRabbitMessageBatchRouter.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/group/IntegrationTestRabbitMessageBatchRouter.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022-2022 Exactpro (Exactpro Systems Limited) + * Copyright 2022-2023 Exactpro (Exactpro Systems Limited) * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -18,6 +18,12 @@ package com.exactpro.th2.common.schema.message.impl.rabbitmq.group import com.exactpro.th2.common.annotations.IntegrationTest import com.exactpro.th2.common.grpc.MessageGroupBatch import com.exactpro.th2.common.schema.box.configuration.BoxConfiguration +import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.DEFAULT_CONFIRMATION_TIMEOUT +import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.DEFAULT_PREFETCH_COUNT +import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.EXCHANGE +import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.QUEUE_NAME +import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.RABBITMQ_IMAGE_NAME +import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.ROUTING_KEY import com.exactpro.th2.common.schema.message.configuration.MessageRouterConfiguration import com.exactpro.th2.common.schema.message.impl.context.DefaultMessageRouterContext import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.ConnectionManagerConfiguration @@ -28,7 +34,6 @@ import mu.KotlinLogging import org.junit.jupiter.api.Test import org.mockito.kotlin.mock import org.testcontainers.containers.RabbitMQContainer -import org.testcontainers.utility.DockerImageName import java.time.Duration import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit @@ -39,7 +44,7 @@ class IntegrationTestRabbitMessageGroupBatchRouter { @Test fun `subscribe to exclusive queue`() { - RabbitMQContainer(DockerImageName.parse(RABBITMQ_3_8_MANAGEMENT_ALPINE)) + RabbitMQContainer(RABBITMQ_IMAGE_NAME) .use { rabbitMQContainer -> rabbitMQContainer.start() LOGGER.info { "Started with port ${rabbitMQContainer.amqpPort}" } @@ -66,7 +71,7 @@ class IntegrationTestRabbitMessageGroupBatchRouter { @Test fun `send receive message group batch`() { - RabbitMQContainer(DockerImageName.parse(RABBITMQ_3_8_MANAGEMENT_ALPINE)) + RabbitMQContainer(RABBITMQ_IMAGE_NAME) .withExchange(EXCHANGE, BuiltinExchangeType.DIRECT.type, false, false, true, emptyMap()) .withQueue(QUEUE_NAME) .withBinding(EXCHANGE, QUEUE_NAME, emptyMap(), ROUTING_KEY, "queue") @@ -128,13 +133,5 @@ class IntegrationTestRabbitMessageGroupBatchRouter { companion object { private val LOGGER = KotlinLogging.logger { } - - private const val RABBITMQ_3_8_MANAGEMENT_ALPINE = "rabbitmq:3.8-management-alpine" - private const val ROUTING_KEY = "routingKey" - private const val QUEUE_NAME = "queue" - private const val EXCHANGE = "test-exchange" - - private const val DEFAULT_PREFETCH_COUNT = 10 - private val DEFAULT_CONFIRMATION_TIMEOUT: Duration = Duration.ofSeconds(1) } } \ No newline at end of file diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/TransportGroupBatchRouterIntegrationTest.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/TransportGroupBatchRouterIntegrationTest.kt index c553b8e68..0e26c045b 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/TransportGroupBatchRouterIntegrationTest.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/TransportGroupBatchRouterIntegrationTest.kt @@ -18,6 +18,12 @@ package com.exactpro.th2.common.schema.message.impl.rabbitmq.transport import com.exactpro.th2.common.annotations.IntegrationTest import com.exactpro.th2.common.schema.box.configuration.BoxConfiguration +import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.DEFAULT_CONFIRMATION_TIMEOUT +import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.DEFAULT_PREFETCH_COUNT +import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.EXCHANGE +import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.QUEUE_NAME +import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.RABBITMQ_IMAGE_NAME +import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.ROUTING_KEY import com.exactpro.th2.common.schema.message.configuration.MessageRouterConfiguration import com.exactpro.th2.common.schema.message.impl.context.DefaultMessageRouterContext import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.ConnectionManagerConfiguration @@ -28,7 +34,6 @@ import mu.KotlinLogging import org.junit.jupiter.api.Test import org.mockito.kotlin.mock import org.testcontainers.containers.RabbitMQContainer -import org.testcontainers.utility.DockerImageName import java.time.Duration import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit @@ -39,7 +44,7 @@ class TransportGroupBatchRouterIntegrationTest { @Test fun `subscribe to exclusive queue`() { - RabbitMQContainer(DockerImageName.parse(RABBITMQ_3_8_MANAGEMENT_ALPINE)) + RabbitMQContainer(RABBITMQ_IMAGE_NAME) .use { rabbitMQContainer -> rabbitMQContainer.start() LOGGER.info { "Started with port ${rabbitMQContainer.amqpPort}" } @@ -69,7 +74,7 @@ class TransportGroupBatchRouterIntegrationTest { @Test fun `send receive message group batch`() { - RabbitMQContainer(DockerImageName.parse(RABBITMQ_3_8_MANAGEMENT_ALPINE)) + RabbitMQContainer(RABBITMQ_IMAGE_NAME) .withExchange(EXCHANGE, BuiltinExchangeType.DIRECT.type, false, false, true, emptyMap()) .withQueue(QUEUE_NAME) .withBinding(EXCHANGE, QUEUE_NAME, emptyMap(), ROUTING_KEY, "queue") @@ -134,13 +139,5 @@ class TransportGroupBatchRouterIntegrationTest { companion object { private val LOGGER = KotlinLogging.logger { } - - private const val RABBITMQ_3_8_MANAGEMENT_ALPINE = "rabbitmq:3.8-management-alpine" - private const val ROUTING_KEY = "routingKey" - private const val QUEUE_NAME = "queue" - private const val EXCHANGE = "test-exchange" - - private const val DEFAULT_PREFETCH_COUNT = 10 - private val DEFAULT_CONFIRMATION_TIMEOUT: Duration = Duration.ofSeconds(1) } } \ No newline at end of file From 767ee07ac2726195371625cdbc24019242e198c4 Mon Sep 17 00:00:00 2001 From: Oleg Smelov Date: Fri, 27 Oct 2023 01:54:20 +0400 Subject: [PATCH 32/51] fixes after review --- .../connection/ConnectionManager.java | 38 +++--- .../configuration/RabbitMQConfiguration.kt | 6 +- .../schema/message/ContainerConstants.kt | 32 +++-- .../AbstractRabbitRouterIntegrationTest.kt | 12 +- .../connection/TestConnectionManager.kt | 88 ++++++-------- ...IntegrationTestRabbitMessageBatchRouter.kt | 14 +-- ...ransportGroupBatchRouterIntegrationTest.kt | 12 +- .../common/util/RabbitTestContainerUtil.kt | 111 ++++++++---------- src/test/resources/rabbitmq_it.conf | 2 +- 9 files changed, 148 insertions(+), 167 deletions(-) diff --git a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java index 9771512ed..3855718ce 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java @@ -1,5 +1,6 @@ /* * Copyright 2020-2023 Exactpro (Exactpro Systems Limited) + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -84,9 +85,9 @@ public class ConnectionManager implements AutoCloseable { private final String subscriberName; private final AtomicInteger nextSubscriberId = new AtomicInteger(1); private final ExecutorService sharedExecutor; - private final ScheduledExecutorService channelChecker = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryBuilder() - .setNameFormat("channel-checker-%d") - .build()); + private final ScheduledExecutorService channelChecker = Executors.newSingleThreadScheduledExecutor( + new ThreadFactoryBuilder().setNameFormat("channel-checker-%d").build() + ); private final HealthMetrics metrics = new HealthMetrics(this); @@ -191,7 +192,6 @@ private void turnOffReadiness(Throwable exception) { } }); - factory.setAutomaticRecoveryEnabled(configuration.isAutomaticRecoveryEnabled()); factory.setConnectionRecoveryTriggeringCondition(shutdownSignal -> !connectionIsClosed.get()); factory.setRecoveryDelayHandler(recoveryAttempts -> { @@ -201,7 +201,7 @@ private void turnOffReadiness(Throwable exception) { int deviationPercent = connectionManagerConfiguration.getRetryTimeDeviationPercent(); LOGGER.debug("Try to recovery connection to RabbitMQ. Count tries = {}", recoveryAttempts); - int recoveryDelay = RetryingDelay.Companion.getRecoveryDelay(recoveryAttempts, minTime, maxTime, maxRecoveryAttempts, deviationPercent); + int recoveryDelay = RetryingDelay.getRecoveryDelay(recoveryAttempts, minTime, maxTime, maxRecoveryAttempts, deviationPercent); if (recoveryAttempts >= maxRecoveryAttempts && metrics.getLivenessMonitor().isEnabled()) { LOGGER.info("Set RabbitMQ liveness to false. Can't recover connection"); metrics.getLivenessMonitor().disable(); @@ -247,7 +247,7 @@ private void addShutdownListenerToChannel(Channel channel, Boolean withRecovery) String errorString = errorBuilder.toString(); LOGGER.warn(errorString); if (withRecovery && errorString.contains("PRECONDITION_FAILED")) { - recoverSubscriptionsOfChannel(channel); + recoverSubscriptionsOfChannel(channel.getChannelNumber()); } } } @@ -255,15 +255,14 @@ private void addShutdownListenerToChannel(Channel channel, Boolean withRecovery) }); } - private void recoverSubscriptionsOfChannel(Channel channel) { + private void recoverSubscriptionsOfChannel(int channelNumber) { channelChecker.execute(() -> { try { - var pinToChannelHolderOptional = - channelsByPin - .entrySet() - .stream() - .filter(entry -> Objects.nonNull(entry.getValue().channel) && channel.getChannelNumber() == entry.getValue().channel.getChannelNumber()) - .findAny(); + var pinToChannelHolderOptional = channelsByPin + .entrySet() + .stream() + .filter(entry -> Objects.nonNull(entry.getValue().channel) && channelNumber == entry.getValue().channel.getChannelNumber()) + .findAny(); var pinIdToChannelHolder = pinToChannelHolderOptional.orElse(null); if (pinIdToChannelHolder != null) { @@ -341,6 +340,10 @@ public void close() throws IOException { LOGGER.info("Closing connection manager"); + for (ChannelHolder channelHolder: channelsByPin.values()) { + channelHolder.channel.abort(); + } + int closeTimeout = configuration.getConnectionCloseTimeout(); if (connection.isOpen()) { try { @@ -352,10 +355,6 @@ public void close() throws IOException { } } - for (ChannelHolder channelHolder: channelsByPin.values()) { - channelHolder.channel.abort(); - } - shutdownExecutor(sharedExecutor, closeTimeout, "rabbit-shared"); shutdownExecutor(channelChecker, closeTimeout, "channel-checker"); } @@ -423,7 +422,6 @@ public void confirm() throws IOException { Confirmation confirmation = OnlyOnceConfirmation.wrap("from " + routingKey + " to " + queue, wrappedConfirmation); - holder.withLock(() -> holder.acquireAndSubmitCheck(() -> channelChecker.schedule(() -> { holder.withLock(() -> { @@ -728,16 +726,16 @@ public void withLock(boolean waitForRecovery, ChannelConsumer consumer) throws I } public void retryingPublishWithLock(ChannelConsumer consumer, ConnectionManagerConfiguration configuration) throws InterruptedException { - Iterator iterator = configuration.createRetryingDelaySequence().iterator(); lock.lock(); + Iterator iterator = configuration.createRetryingDelaySequence().iterator(); try { - var currentValue = iterator.next(); Channel tempChannel = getChannel(true); while (true) { try { consumer.consume(tempChannel); break; } catch (IOException | ShutdownSignalException e) { + var currentValue = iterator.next(); int recoveryDelay = currentValue.getDelay(); LOGGER.warn("Retrying publishing #{}, waiting for {}ms, then recreating channel. Reason: {}", currentValue.getTryNumber(), recoveryDelay, e); TimeUnit.MILLISECONDS.sleep(recoveryDelay); diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/configuration/RabbitMQConfiguration.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/configuration/RabbitMQConfiguration.kt index aa6ce558c..f2b21be42 100644 --- a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/configuration/RabbitMQConfiguration.kt +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/configuration/RabbitMQConfiguration.kt @@ -42,7 +42,6 @@ data class ConnectionManagerConfiguration( val messageRecursionLimit: Int = 100, val workingThreads: Int = 1, val confirmationTimeout: Duration = Duration.ofMinutes(5), - val isAutomaticRecoveryEnabled: Boolean = true ) : Configuration() { init { check(workingThreads > 0) { "expected 'workingThreads' greater than 0 but was $workingThreads" } @@ -60,7 +59,6 @@ data class ConnectionManagerConfiguration( )) } } - } data class RetryingDelay(val tryNumber: Int, val delay: Int) { @@ -75,7 +73,9 @@ data class RetryingDelay(val tryNumber: Int, val delay: Int) { ): Int { return if (numberOfTries <= maxRecoveryAttempts) { getRecoveryDelayWithIncrement(numberOfTries, minTime, maxTime, maxRecoveryAttempts) - } else getRecoveryDelayWithDeviation(maxTime, deviationPercent) + } else { + getRecoveryDelayWithDeviation(maxTime, deviationPercent) + } } private fun getRecoveryDelayWithDeviation(maxTime: Int, deviationPercent: Int): Int { diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/ContainerConstants.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/ContainerConstants.kt index 957063094..0ab0fc502 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/ContainerConstants.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/ContainerConstants.kt @@ -1,16 +1,30 @@ +/* + * Copyright 2023 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.exactpro.th2.common.schema.message import org.testcontainers.utility.DockerImageName import java.time.Duration -class ContainerConstants { - companion object { - val RABBITMQ_IMAGE_NAME: DockerImageName = DockerImageName.parse("rabbitmq:3.12.7-management-alpine") - const val ROUTING_KEY = "routingKey" - const val QUEUE_NAME = "queue" - const val EXCHANGE = "test-exchange" +object ContainerConstants { + @JvmField val RABBITMQ_IMAGE_NAME: DockerImageName = DockerImageName.parse("rabbitmq:3.12.7-management-alpine") + const val ROUTING_KEY = "routingKey" + const val QUEUE_NAME = "queue" + const val EXCHANGE = "test-exchange" - const val DEFAULT_PREFETCH_COUNT = 10 - val DEFAULT_CONFIRMATION_TIMEOUT: Duration = Duration.ofSeconds(1) - } + const val DEFAULT_PREFETCH_COUNT = 10 + @JvmField val DEFAULT_CONFIRMATION_TIMEOUT: Duration = Duration.ofSeconds(1) } \ No newline at end of file diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitRouterIntegrationTest.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitRouterIntegrationTest.kt index 340720942..2a0864404 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitRouterIntegrationTest.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/AbstractRabbitRouterIntegrationTest.kt @@ -18,12 +18,12 @@ package com.exactpro.th2.common.schema.message.impl.rabbitmq import com.exactpro.th2.common.annotations.IntegrationTest import com.exactpro.th2.common.schema.box.configuration.BoxConfiguration -import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.DEFAULT_CONFIRMATION_TIMEOUT -import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.DEFAULT_PREFETCH_COUNT -import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.EXCHANGE -import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.QUEUE_NAME -import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.RABBITMQ_IMAGE_NAME -import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.ROUTING_KEY +import com.exactpro.th2.common.schema.message.ContainerConstants.DEFAULT_CONFIRMATION_TIMEOUT +import com.exactpro.th2.common.schema.message.ContainerConstants.DEFAULT_PREFETCH_COUNT +import com.exactpro.th2.common.schema.message.ContainerConstants.EXCHANGE +import com.exactpro.th2.common.schema.message.ContainerConstants.QUEUE_NAME +import com.exactpro.th2.common.schema.message.ContainerConstants.RABBITMQ_IMAGE_NAME +import com.exactpro.th2.common.schema.message.ContainerConstants.ROUTING_KEY import com.exactpro.th2.common.schema.message.ManualAckDeliveryCallback import com.exactpro.th2.common.schema.message.configuration.MessageRouterConfiguration import com.exactpro.th2.common.schema.message.configuration.QueueConfiguration diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt index 11af50b92..0715dbfd0 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023 Exactpro (Exactpro Systems Limited) + * Copyright 2022-2023 Exactpro (Exactpro Systems Limited) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,20 +17,20 @@ package com.exactpro.th2.common.schema.message.impl.rabbitmq.connection import com.exactpro.th2.common.annotations.IntegrationTest -import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.DEFAULT_CONFIRMATION_TIMEOUT -import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.DEFAULT_PREFETCH_COUNT -import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.RABBITMQ_IMAGE_NAME +import com.exactpro.th2.common.schema.message.ContainerConstants.DEFAULT_CONFIRMATION_TIMEOUT +import com.exactpro.th2.common.schema.message.ContainerConstants.DEFAULT_PREFETCH_COUNT +import com.exactpro.th2.common.schema.message.ContainerConstants.RABBITMQ_IMAGE_NAME import com.exactpro.th2.common.schema.message.ManualAckDeliveryCallback import com.exactpro.th2.common.schema.message.SubscriberMonitor import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.ConnectionManagerConfiguration import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.RabbitMQConfiguration -import com.exactpro.th2.common.util.RabbitTestContainerUtil.Companion.declareFanoutExchangeWithBinding -import com.exactpro.th2.common.util.RabbitTestContainerUtil.Companion.declareQueue -import com.exactpro.th2.common.util.RabbitTestContainerUtil.Companion.getChannelsInfo -import com.exactpro.th2.common.util.RabbitTestContainerUtil.Companion.getQueuesInfo -import com.exactpro.th2.common.util.RabbitTestContainerUtil.Companion.getSubscribedChannelsCount -import com.exactpro.th2.common.util.RabbitTestContainerUtil.Companion.putMessageInQueue -import com.exactpro.th2.common.util.RabbitTestContainerUtil.Companion.restartContainer +import com.exactpro.th2.common.util.declareFanoutExchangeWithBinding +import com.exactpro.th2.common.util.declareQueue +import com.exactpro.th2.common.util.getChannelsInfo +import com.exactpro.th2.common.util.getQueuesInfo +import com.exactpro.th2.common.util.getSubscribedChannelsCount +import com.exactpro.th2.common.util.putMessageInQueue +import com.exactpro.th2.common.util.restartContainer import com.rabbitmq.client.BuiltinExchangeType import java.time.Duration import java.util.concurrent.ArrayBlockingQueue @@ -47,19 +47,12 @@ import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertNotEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeAll -import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.testcontainers.containers.RabbitMQContainer import org.testcontainers.utility.MountableFile -import java.io.IOException -import kotlin.test.assertFailsWith - -private val LOGGER = KotlinLogging.logger { } - @IntegrationTest class TestConnectionManager { - @Test fun `connection manager reports unacked messages when confirmation timeout elapsed`() { val routingKey = "routingKey1" @@ -467,12 +460,10 @@ class TestConnectionManager { LOGGER.info { "Publication finished!" } LOGGER.info { getQueuesInfo(it) } - consume.assertComplete("Wrong number of received messages") - assertTrue( - getQueuesInfo(it).toString().contains("$queueName\t0") - ) { "There should be no messages left in the queue" } - + assertTrue(getQueuesInfo(it).toString().contains("$queueName\t0")) { + "There should be no messages left in the queue" + } } } } @@ -694,6 +685,7 @@ class TestConnectionManager { } for (i in 1..5) { putMessageInQueue(it, queueName) + Thread.sleep(1000) } @@ -777,9 +769,9 @@ class TestConnectionManager { assertEquals(0, getSubscribedChannelsCount(it, queueName)) assertEquals(2, counter.get()) { "Wrong number of received messages" } - assertTrue( - queuesListExecResult.toString().contains("$queueName\t1") - ) { "There should a message left in the queue" } + assertTrue(queuesListExecResult.toString().contains("$queueName\t1")) { + "There should a message left in the queue" + } } finally { Assertions.assertNotNull(thread) Assertions.assertDoesNotThrow { @@ -824,29 +816,25 @@ class TestConnectionManager { ) @Test - @Disabled - // TODO: this test is no more relevant - // TODO: we need to change test scenario or remove it fun `connection manager exclusive queue test`() { - RabbitMQContainer(RABBITMQ_IMAGE_NAME) - .use { rabbitMQContainer -> - rabbitMQContainer.start() - LOGGER.info { "Started with port ${rabbitMQContainer.amqpPort}" } - - createConnectionManager(rabbitMQContainer).use { firstManager -> - createConnectionManager(rabbitMQContainer).use { secondManager -> - val queue = firstManager.queueDeclare() - - assertFailsWith("Another connection can subscribe to the $queue queue") { - secondManager.basicConsume(queue, { _, _, _ -> }, {}) - } - - extracted(firstManager, secondManager, queue, 3) - extracted(firstManager, secondManager, queue, 6) - } + RabbitMQContainer(RABBITMQ_IMAGE_NAME).use { rabbitMQContainer -> + rabbitMQContainer.start() + LOGGER.info { "Started with port ${rabbitMQContainer.amqpPort}" } + + createConnectionManager(rabbitMQContainer).use { firstManager -> + createConnectionManager(rabbitMQContainer).use { secondManager -> + val queue = firstManager.queueDeclare() + val consumerThread = thread { secondManager.basicConsume(queue, { _, _, _ -> }, {}) } + consumerThread.join(5_000) + val isAlive = consumerThread.isAlive + consumerThread.stop() + assertTrue(isAlive) { "Another connection can subscribe to the $queue queue" } + + extracted(firstManager, secondManager, queue, 3) + extracted(firstManager, secondManager, queue, 6) } - } + } } private fun extracted( @@ -893,7 +881,6 @@ class TestConnectionManager { rabbitMQContainer: RabbitMQContainer, prefetchCount: Int = DEFAULT_PREFETCH_COUNT, confirmationTimeout: Duration = DEFAULT_CONFIRMATION_TIMEOUT, - isAutomaticRecoveryEnabled: Boolean = true ) = ConnectionManager( RabbitMQConfiguration( host = rabbitMQContainer.host, @@ -905,12 +892,13 @@ class TestConnectionManager { ConnectionManagerConfiguration( subscriberName = "test", prefetchCount = prefetchCount, - confirmationTimeout = confirmationTimeout, - isAutomaticRecoveryEnabled = isAutomaticRecoveryEnabled - ), + confirmationTimeout = confirmationTimeout + ) ) companion object { + private val LOGGER = KotlinLogging.logger { } + private lateinit var rabbit: RabbitMQContainer @BeforeAll diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/group/IntegrationTestRabbitMessageBatchRouter.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/group/IntegrationTestRabbitMessageBatchRouter.kt index 1808c6b80..b9e953ac5 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/group/IntegrationTestRabbitMessageBatchRouter.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/group/IntegrationTestRabbitMessageBatchRouter.kt @@ -1,5 +1,6 @@ /* * Copyright 2022-2023 Exactpro (Exactpro Systems Limited) + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -18,12 +19,12 @@ package com.exactpro.th2.common.schema.message.impl.rabbitmq.group import com.exactpro.th2.common.annotations.IntegrationTest import com.exactpro.th2.common.grpc.MessageGroupBatch import com.exactpro.th2.common.schema.box.configuration.BoxConfiguration -import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.DEFAULT_CONFIRMATION_TIMEOUT -import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.DEFAULT_PREFETCH_COUNT -import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.EXCHANGE -import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.QUEUE_NAME -import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.RABBITMQ_IMAGE_NAME -import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.ROUTING_KEY +import com.exactpro.th2.common.schema.message.ContainerConstants.DEFAULT_CONFIRMATION_TIMEOUT +import com.exactpro.th2.common.schema.message.ContainerConstants.DEFAULT_PREFETCH_COUNT +import com.exactpro.th2.common.schema.message.ContainerConstants.EXCHANGE +import com.exactpro.th2.common.schema.message.ContainerConstants.QUEUE_NAME +import com.exactpro.th2.common.schema.message.ContainerConstants.RABBITMQ_IMAGE_NAME +import com.exactpro.th2.common.schema.message.ContainerConstants.ROUTING_KEY import com.exactpro.th2.common.schema.message.configuration.MessageRouterConfiguration import com.exactpro.th2.common.schema.message.impl.context.DefaultMessageRouterContext import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.ConnectionManagerConfiguration @@ -41,7 +42,6 @@ import kotlin.test.assertTrue @IntegrationTest class IntegrationTestRabbitMessageGroupBatchRouter { - @Test fun `subscribe to exclusive queue`() { RabbitMQContainer(RABBITMQ_IMAGE_NAME) diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/TransportGroupBatchRouterIntegrationTest.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/TransportGroupBatchRouterIntegrationTest.kt index 0e26c045b..873004c00 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/TransportGroupBatchRouterIntegrationTest.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/transport/TransportGroupBatchRouterIntegrationTest.kt @@ -18,12 +18,12 @@ package com.exactpro.th2.common.schema.message.impl.rabbitmq.transport import com.exactpro.th2.common.annotations.IntegrationTest import com.exactpro.th2.common.schema.box.configuration.BoxConfiguration -import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.DEFAULT_CONFIRMATION_TIMEOUT -import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.DEFAULT_PREFETCH_COUNT -import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.EXCHANGE -import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.QUEUE_NAME -import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.RABBITMQ_IMAGE_NAME -import com.exactpro.th2.common.schema.message.ContainerConstants.Companion.ROUTING_KEY +import com.exactpro.th2.common.schema.message.ContainerConstants.DEFAULT_CONFIRMATION_TIMEOUT +import com.exactpro.th2.common.schema.message.ContainerConstants.DEFAULT_PREFETCH_COUNT +import com.exactpro.th2.common.schema.message.ContainerConstants.EXCHANGE +import com.exactpro.th2.common.schema.message.ContainerConstants.QUEUE_NAME +import com.exactpro.th2.common.schema.message.ContainerConstants.RABBITMQ_IMAGE_NAME +import com.exactpro.th2.common.schema.message.ContainerConstants.ROUTING_KEY import com.exactpro.th2.common.schema.message.configuration.MessageRouterConfiguration import com.exactpro.th2.common.schema.message.impl.context.DefaultMessageRouterContext import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.ConnectionManagerConfiguration diff --git a/src/test/kotlin/com/exactpro/th2/common/util/RabbitTestContainerUtil.kt b/src/test/kotlin/com/exactpro/th2/common/util/RabbitTestContainerUtil.kt index 721376daa..8d5e82e01 100644 --- a/src/test/kotlin/com/exactpro/th2/common/util/RabbitTestContainerUtil.kt +++ b/src/test/kotlin/com/exactpro/th2/common/util/RabbitTestContainerUtil.kt @@ -1,5 +1,6 @@ /* - * Copyright 2020-2022 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2023 Exactpro (Exactpro Systems Limited) + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -13,82 +14,62 @@ * limitations under the License. */ +@file:JvmName("RabbitTestContainerUtil") + package com.exactpro.th2.common.util import kotlin.random.Random import org.testcontainers.containers.Container import org.testcontainers.containers.RabbitMQContainer -class RabbitTestContainerUtil { - companion object { - fun declareQueue(rabbit: RabbitMQContainer, queueName: String): Container.ExecResult? { - return execCommandWithSplit(rabbit, "rabbitmqadmin declare queue name=$queueName durable=false") +fun declareQueue(rabbit: RabbitMQContainer, queueName: String): Container.ExecResult? = + execCommandWithSplit(rabbit, "rabbitmqadmin declare queue name=$queueName durable=false") - } - - fun declareFanoutExchangeWithBinding( - rabbit: RabbitMQContainer, - exchangeName: String, - destinationQueue: String - ) { - execCommandWithSplit(rabbit, "rabbitmqadmin declare exchange name=$exchangeName type=fanout") - execCommandWithSplit( - rabbit, - "rabbitmqadmin declare binding source=$exchangeName destination_type=queue destination=$destinationQueue" - ) - } +fun declareFanoutExchangeWithBinding(rabbit: RabbitMQContainer, exchangeName: String, destinationQueue: String) { + execCommandWithSplit(rabbit, "rabbitmqadmin declare exchange name=$exchangeName type=fanout") + execCommandWithSplit( + rabbit, + "rabbitmqadmin declare binding source=$exchangeName destination_type=queue destination=$destinationQueue" + ) +} - fun putMessageInQueue(rabbit: RabbitMQContainer, queueName: String): Container.ExecResult? { - return execCommandWithSplit( - rabbit, - """rabbitmqadmin publish exchange=amq.default routing_key=$queueName payload="hello-${ - Random.nextInt( - 0, - 1000 - ) - }"""" +fun putMessageInQueue(rabbit: RabbitMQContainer, queueName: String): Container.ExecResult? = + execCommandWithSplit( + rabbit, + """rabbitmqadmin publish exchange=amq.default routing_key=$queueName payload="hello-${ + Random.nextInt( + 0, + 1000 ) - } + }"""" + ) - fun getQueuesInfo(rabbit: RabbitMQContainer): Container.ExecResult? { - return execCommandWithSplit(rabbit, "rabbitmqctl list_queues") - } +fun getQueuesInfo(rabbit: RabbitMQContainer): Container.ExecResult? = + execCommandWithSplit(rabbit, "rabbitmqctl list_queues") - fun getChannelsInfo(rabbit: RabbitMQContainer): String { - return execCommandWithSplit(rabbit, "rabbitmqctl list_consumers").toString() +fun getChannelsInfo(rabbit: RabbitMQContainer): String = + execCommandWithSplit(rabbit, "rabbitmqctl list_consumers").toString() - } +fun getSubscribedChannelsCount(rabbitMQContainer: RabbitMQContainer, queue: String): Int = + getChannelsInfo(rabbitMQContainer).countMatches(queue) - fun getSubscribedChannelsCount( - rabbitMQContainer: RabbitMQContainer, - queue: String, - ): Int { - val channelsInfo = getChannelsInfo(rabbitMQContainer) - return channelsInfo.countMatches(queue) - } - - fun restartContainer(rabbit: RabbitMQContainer) { - val tag: String = rabbit.containerId - val snapshotId: String = rabbit.dockerClient.commitCmd(tag) - .withRepository("temp-rabbit") - .withTag(tag).exec() - rabbit.stop() - rabbit.dockerImageName = snapshotId - rabbit.start() - } - - private fun execCommandWithSplit(rabbit: RabbitMQContainer, command: String): Container.ExecResult? { - return rabbit.execInContainer( - *command.split(" ").toTypedArray() - ) - } +fun restartContainer(rabbit: RabbitMQContainer) { + val tag: String = rabbit.containerId + val snapshotId: String = rabbit.dockerClient + .commitCmd(tag) + .withRepository("temp-rabbit") + .withTag(tag) + .exec() + rabbit.stop() + rabbit.dockerImageName = snapshotId + rabbit.start() +} - private fun String.countMatches(pattern: String): Int { - return this.substringAfter("active\targuments") - .split(pattern) - .dropLastWhile { s -> s.isEmpty() } - .toTypedArray().size - 1 - } +private fun execCommandWithSplit(rabbit: RabbitMQContainer, command: String): Container.ExecResult? = + rabbit.execInContainer(*command.split(" ").toTypedArray()) - } -} \ No newline at end of file +private fun String.countMatches(pattern: String): Int = + substringAfter("active\targuments") + .split(pattern) + .dropLastWhile { s -> s.isEmpty() } + .toTypedArray().size - 1 \ No newline at end of file diff --git a/src/test/resources/rabbitmq_it.conf b/src/test/resources/rabbitmq_it.conf index e6ef5a8e0..fba65f59c 100644 --- a/src/test/resources/rabbitmq_it.conf +++ b/src/test/resources/rabbitmq_it.conf @@ -1,2 +1,2 @@ -consumer_timeout = 50000 +consumer_timeout = 5000 loopback_users.guest = false \ No newline at end of file From 57b2d02d10f65bfb5d54cc672f0bcd17edb9b812 Mon Sep 17 00:00:00 2001 From: Oleg Smelov Date: Mon, 30 Oct 2023 15:21:32 +0400 Subject: [PATCH 33/51] fix: cancelled consumer channel resubscribed during recovery --- .../connection/ConnectionManager.java | 44 +++++++++++-------- .../connection/TestConnectionManager.kt | 3 -- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java index 3855718ce..eb0fb1e13 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java @@ -50,6 +50,7 @@ import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -240,7 +241,7 @@ private void addShutdownListenerToChannel(Channel channel, Boolean withRecovery) if (reason instanceof AMQImpl.Channel.Close) { var castedReason = (AMQImpl.Channel.Close) reason; if (castedReason.getReplyCode() != 200) { - StringBuilder errorBuilder = new StringBuilder("RabbitMQ soft error occupied: "); + StringBuilder errorBuilder = new StringBuilder("RabbitMQ soft error occurred: "); castedReason.appendArgumentDebugStringTo(errorBuilder); errorBuilder.append(" on channel "); errorBuilder.append(channelCause); @@ -255,30 +256,37 @@ private void addShutdownListenerToChannel(Channel channel, Boolean withRecovery) }); } + private @Nullable Map.Entry getChannelHolderByChannelNumber(int channelNumber) { + return channelsByPin + .entrySet() + .stream() + .filter(entry -> Objects.nonNull(entry.getValue().channel) && channelNumber == entry.getValue().channel.getChannelNumber()) + .findAny() + .orElse(null); + } + private void recoverSubscriptionsOfChannel(int channelNumber) { channelChecker.execute(() -> { try { - var pinToChannelHolderOptional = channelsByPin - .entrySet() - .stream() - .filter(entry -> Objects.nonNull(entry.getValue().channel) && channelNumber == entry.getValue().channel.getChannelNumber()) - .findAny(); - - var pinIdToChannelHolder = pinToChannelHolderOptional.orElse(null); + var pinIdToChannelHolder = getChannelHolderByChannelNumber(channelNumber); if (pinIdToChannelHolder != null) { PinId pinId = pinIdToChannelHolder.getKey(); ChannelHolder channelHolder = pinIdToChannelHolder.getValue(); SubscriptionCallbacks subscriptionCallbacks = channelHolder.subscriptionCallbacks; if (subscriptionCallbacks != null) { - LOGGER.info("Changing channel for holder with pin id: " + pinId.toString()); channelsByPin.remove(pinId); - basicConsume(pinId.queue, subscriptionCallbacks.deliverCallback, subscriptionCallbacks.cancelCallback); + if (channelHolder.isSubscribed) { + LOGGER.info("Changing channel for holder with pin id: " + pinId.toString()); + basicConsume(pinId.queue, subscriptionCallbacks.deliverCallback, subscriptionCallbacks.cancelCallback); + } } } - } catch (IOException | InterruptedException e) { + } catch (IOException e) { LOGGER.warn("Exception during channel's subscriptions recovery", e); throw new RuntimeException(e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } }); } @@ -441,6 +449,7 @@ public void confirm() throws IOException { } }, cancelCallback), configuration); + holder.isSubscribed = true; return new RabbitMqSubscriberMonitor(holder, queue, tag, this::basicCancel); } @@ -577,17 +586,13 @@ private static void basicReject(Channel channel, long deliveryTag) throws IOExce channel.basicReject(deliveryTag, false); } - private class RabbitMqSubscriberMonitor implements ExclusiveSubscriberMonitor { - + private static class RabbitMqSubscriberMonitor implements ExclusiveSubscriberMonitor { private final ChannelHolder holder; private final String queue; private final String tag; private final CancelAction action; - public RabbitMqSubscriberMonitor(ChannelHolder holder, - String queue, - String tag, - CancelAction action) { + public RabbitMqSubscriberMonitor(ChannelHolder holder, String queue, String tag, CancelAction action) { this.holder = holder; this.queue = queue; this.tag = tag; @@ -602,9 +607,8 @@ public RabbitMqSubscriberMonitor(ChannelHolder holder, @Override public void unsubscribe() throws IOException { holder.withLock(false, channel -> { -// channelsByPin.values().remove(holder); action.execute(channel, tag); -// channel.abort(); + holder.isSubscribed = false; }); } } @@ -679,6 +683,8 @@ private static class ChannelHolder { private Future check; @GuardedBy("lock") private Channel channel; + @GuardedBy("lock") + public boolean isSubscribed = false; public ChannelHolder( Supplier supplier, diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt index 0715dbfd0..e4f4eee75 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt @@ -761,9 +761,6 @@ class TestConnectionManager { LOGGER.info { "Sleeping..." } Thread.sleep(63000) - LOGGER.info { "queues list: \n ${getQueuesInfo(it)}" } - - val queuesListExecResult = getQueuesInfo(it) LOGGER.info { "queues list: \n $queuesListExecResult" } From 7fc34cc76ca8d5bd3aa02c591333691dba32d292 Mon Sep 17 00:00:00 2001 From: Oleg Smelov Date: Wed, 1 Nov 2023 02:37:51 +0400 Subject: [PATCH 34/51] fixed catching ShutdownSignalException exceptions review fixes --- .../connection/ConnectionManager.java | 44 +++++++++++++------ .../connection/TestConnectionManager.kt | 17 +++---- src/test/resources/rabbitmq_it.conf | 7 ++- 3 files changed, 43 insertions(+), 25 deletions(-) diff --git a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java index eb0fb1e13..3893ffddb 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java @@ -276,15 +276,14 @@ private void recoverSubscriptionsOfChannel(int channelNumber) { SubscriptionCallbacks subscriptionCallbacks = channelHolder.subscriptionCallbacks; if (subscriptionCallbacks != null) { channelsByPin.remove(pinId); - if (channelHolder.isSubscribed) { + if (channelHolder.subscribed()) { LOGGER.info("Changing channel for holder with pin id: " + pinId.toString()); basicConsume(pinId.queue, subscriptionCallbacks.deliverCallback, subscriptionCallbacks.cancelCallback); } } } } catch (IOException e) { - LOGGER.warn("Exception during channel's subscriptions recovery", e); - throw new RuntimeException(e); + LOGGER.warn("Failed to recovery channel's subscriptions", e); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } @@ -449,7 +448,6 @@ public void confirm() throws IOException { } }, cancelCallback), configuration); - holder.isSubscribed = true; return new RabbitMqSubscriberMonitor(holder, queue, tag, this::basicCancel); } @@ -606,10 +604,7 @@ public RabbitMqSubscriberMonitor(ChannelHolder holder, String queue, String tag, @Override public void unsubscribe() throws IOException { - holder.withLock(false, channel -> { - action.execute(channel, tag); - holder.isSubscribed = false; - }); + holder.unsubscribeWithLock(tag, action); } } @@ -684,7 +679,7 @@ private static class ChannelHolder { @GuardedBy("lock") private Channel channel; @GuardedBy("lock") - public boolean isSubscribed = false; + private boolean isSubscribed = false; public ChannelHolder( Supplier supplier, @@ -696,6 +691,7 @@ public ChannelHolder( this.maxCount = maxCount; this.subscriptionCallbacks = null; } + public ChannelHolder( Supplier supplier, BiConsumer reconnectionChecker, @@ -753,19 +749,25 @@ public void retryingPublishWithLock(ChannelConsumer consumer, ConnectionManagerC } } - public T retryingConsumeWithLock(ChannelMapper mapper, ConnectionManagerConfiguration configuration) throws InterruptedException { + public T retryingConsumeWithLock(ChannelMapper mapper, ConnectionManagerConfiguration configuration) throws InterruptedException, IOException { lock.lock(); try { Iterator iterator = null; Channel tempChannel = getChannel(); while (true) { try { - return mapper.map(tempChannel); + var tag = mapper.map(tempChannel); + isSubscribed = true; + return tag; } catch (IOException e) { iterator = handleAndSleep(configuration, iterator, "Retrying consume", e); - } catch (ShutdownSignalException e) { - iterator = handleAndSleep(configuration, iterator, "Retrying consume", e); - tempChannel = recreateChannel(); + var reason = tempChannel.getCloseReason(); + if (reason != null) { + if (reason.getMessage().contains("reply-text=RESOURCE_LOCKED")) { + throw e; + } + tempChannel = recreateChannel(); + } } } } finally { @@ -773,6 +775,16 @@ public T retryingConsumeWithLock(ChannelMapper mapper, ConnectionManagerC } } + public void unsubscribeWithLock(String tag, CancelAction action) throws IOException { + lock.lock(); + try { + action.execute(channel, tag); + isSubscribed = false; + } finally { + lock.unlock(); + } + } + @NotNull private static Iterator handleAndSleep( ConnectionManagerConfiguration configuration, @@ -869,6 +881,10 @@ private Channel getChannel(boolean waitForRecovery) { reconnectionChecker.accept(channel, waitForRecovery); return channel; } + + public boolean subscribed() { + return isSubscribed; + } } private interface ChannelMapper { diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt index e4f4eee75..76b00eae0 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt @@ -50,6 +50,8 @@ import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test import org.testcontainers.containers.RabbitMQContainer import org.testcontainers.utility.MountableFile +import java.io.IOException +import kotlin.test.assertFailsWith @IntegrationTest class TestConnectionManager { @@ -406,13 +408,9 @@ class TestConnectionManager { val queueName = "queue5" val amqpPort = 5672 val container = object : RabbitMQContainer(RABBITMQ_IMAGE_NAME) { - fun addFixedPort(hostPort: Int, containerPort: Int) { - super.addFixedExposedPort(hostPort, containerPort) - } + init { super.addFixedExposedPort(amqpPort, amqpPort) } } - container - .addFixedPort(amqpPort, amqpPort) container .withQueue(queueName) .use { @@ -821,11 +819,10 @@ class TestConnectionManager { createConnectionManager(rabbitMQContainer).use { firstManager -> createConnectionManager(rabbitMQContainer).use { secondManager -> val queue = firstManager.queueDeclare() - val consumerThread = thread { secondManager.basicConsume(queue, { _, _, _ -> }, {}) } - consumerThread.join(5_000) - val isAlive = consumerThread.isAlive - consumerThread.stop() - assertTrue(isAlive) { "Another connection can subscribe to the $queue queue" } + + assertFailsWith("Another connection can subscribe to the $queue queue") { + secondManager.basicConsume(queue, { _, _, _ -> }, {}) + } extracted(firstManager, secondManager, queue, 3) extracted(firstManager, secondManager, queue, 6) diff --git a/src/test/resources/rabbitmq_it.conf b/src/test/resources/rabbitmq_it.conf index fba65f59c..2942e21de 100644 --- a/src/test/resources/rabbitmq_it.conf +++ b/src/test/resources/rabbitmq_it.conf @@ -1,2 +1,7 @@ -consumer_timeout = 5000 +# According to rabbitMQ docs: Values lower than one minute are not supported +# Whether the timeout should be enforced is evaluated periodically, at one minute intervals +# (https://www.rabbitmq.com/consumers.html#acknowledgement-timeout) +# Actually, timeouts less than a minute are applied as expected contrary to the documentation (RabbitMQ 3.12.7). +# Using small timeouts to reduce testing time +consumer_timeout = 1000 loopback_users.guest = false \ No newline at end of file From 3c2e1d7a0095e8e577c7187f22ed9327a0a35e8c Mon Sep 17 00:00:00 2001 From: Oleg Smelov Date: Mon, 6 Nov 2023 14:09:45 +0400 Subject: [PATCH 35/51] channel is not recovered by ConnectionManager if it's connection is closed checks added to ConnectionManagerConfiguration --- .../connection/ConnectionManager.java | 11 ++++-- .../configuration/RabbitMQConfiguration.kt | 34 +++++++++++-------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java index 3893ffddb..142d8485c 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java @@ -284,6 +284,7 @@ private void recoverSubscriptionsOfChannel(int channelNumber) { } } catch (IOException e) { LOGGER.warn("Failed to recovery channel's subscriptions", e); + // this code executed in executor service and exception thrown here will not be handled anywhere } catch (InterruptedException e) { Thread.currentThread().interrupt(); } @@ -739,9 +740,15 @@ public void retryingPublishWithLock(ChannelConsumer consumer, ConnectionManagerC } catch (IOException | ShutdownSignalException e) { var currentValue = iterator.next(); int recoveryDelay = currentValue.getDelay(); - LOGGER.warn("Retrying publishing #{}, waiting for {}ms, then recreating channel. Reason: {}", currentValue.getTryNumber(), recoveryDelay, e); + LOGGER.warn("Retrying publishing #{}, waiting for {}ms. Reason: {}", currentValue.getTryNumber(), recoveryDelay, e); TimeUnit.MILLISECONDS.sleep(recoveryDelay); - tempChannel = recreateChannel(); + + // we should not recover channel if it's connection is closed, because if we'll do it channel + // will be also auto recovered by RabbitMQ client during recovering of connection, and we'll + // get two new channels instead of one closed. + if (!tempChannel.isOpen() && tempChannel.getConnection().isOpen()) { + tempChannel = recreateChannel(); + } } } } finally { diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/configuration/RabbitMQConfiguration.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/configuration/RabbitMQConfiguration.kt index f2b21be42..e5f960465 100644 --- a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/configuration/RabbitMQConfiguration.kt +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/configuration/RabbitMQConfiguration.kt @@ -21,29 +21,33 @@ import java.time.Duration import java.util.concurrent.ThreadLocalRandom data class RabbitMQConfiguration( - @JsonProperty(required = true) var host: String, - @JsonProperty(required = true) @get:JsonProperty("vHost") var vHost: String, - @JsonProperty(required = true) var port: Int = 5672, - @JsonProperty(required = true) var username: String, - @JsonProperty(required = true) var password: String, + @JsonProperty(required = true) val host: String, + @JsonProperty(required = true) @get:JsonProperty("vHost") val vHost: String, + @JsonProperty(required = true) val port: Int = 5672, + @JsonProperty(required = true) val username: String, + @JsonProperty(required = true) val password: String, @Deprecated(message = "Please use subscriber name from ConnectionManagerConfiguration") - var subscriberName: String? = null, //FIXME: Remove in future version - var exchangeName: String? = null) : Configuration() + val subscriberName: String? = null, //FIXME: Remove in future version + val exchangeName: String? = null +) : Configuration() data class ConnectionManagerConfiguration( - var subscriberName: String? = null, - var connectionTimeout: Int = -1, - var connectionCloseTimeout: Int = 10000, - var maxRecoveryAttempts: Int = 5, - var minConnectionRecoveryTimeout: Int = 10000, - var maxConnectionRecoveryTimeout: Int = 60000, + val subscriberName: String? = null, + val connectionTimeout: Int = -1, + val connectionCloseTimeout: Int = 10000, + val maxRecoveryAttempts: Int = 5, + val minConnectionRecoveryTimeout: Int = 10000, + val maxConnectionRecoveryTimeout: Int = 60000, val prefetchCount: Int = 10, - var retryTimeDeviationPercent: Int = 10, + val retryTimeDeviationPercent: Int = 10, val messageRecursionLimit: Int = 100, val workingThreads: Int = 1, - val confirmationTimeout: Duration = Duration.ofMinutes(5), + val confirmationTimeout: Duration = Duration.ofMinutes(5) ) : Configuration() { init { + check(maxRecoveryAttempts > 0) { "expected 'maxRecoveryAttempts' greater than 0 but was $maxRecoveryAttempts" } + check(minConnectionRecoveryTimeout > 0) { "expected 'minConnectionRecoveryTimeout' greater than 0 but was $minConnectionRecoveryTimeout" } + check(maxConnectionRecoveryTimeout >= minConnectionRecoveryTimeout) { "expected 'maxConnectionRecoveryTimeout' ($maxConnectionRecoveryTimeout) no less than 'minConnectionRecoveryTimeout' ($minConnectionRecoveryTimeout)" } check(workingThreads > 0) { "expected 'workingThreads' greater than 0 but was $workingThreads" } check(!confirmationTimeout.run { isNegative || isZero }) { "expected 'confirmationTimeout' greater than 0 but was $confirmationTimeout" } } From 0def228010e1fe26968e755a3c61ab8e3feb00c5 Mon Sep 17 00:00:00 2001 From: Oleg Smelov Date: Tue, 7 Nov 2023 12:12:56 +0400 Subject: [PATCH 36/51] recovering channel after NOT_FOUND error added `double recovery` of subscriber channels prevention --- .../rabbitmq/connection/ConnectionManager.java | 18 ++++++++++++++++-- .../grpc/router/impl/DefaultGrpcRouterTest.kt | 5 +++++ .../connection/TestConnectionManager.kt | 11 +++++++---- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java index 142d8485c..689f43ed8 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java @@ -247,8 +247,14 @@ private void addShutdownListenerToChannel(Channel channel, Boolean withRecovery) errorBuilder.append(channelCause); String errorString = errorBuilder.toString(); LOGGER.warn(errorString); - if (withRecovery && errorString.contains("PRECONDITION_FAILED")) { - recoverSubscriptionsOfChannel(channel.getChannelNumber()); + if (withRecovery && + (errorString.contains("reply-text=PRECONDITION_FAILED") + || errorString.contains("reply-text=NOT_FOUND")) + ) { + int channelNumber = channel.getChannelNumber(); + var pinIdToChannelHolder = getChannelHolderByChannelNumber(channelNumber); + if (pinIdToChannelHolder != null && !pinIdToChannelHolder.getValue().subscribing()) + recoverSubscriptionsOfChannel(channelNumber); } } } @@ -681,6 +687,8 @@ private static class ChannelHolder { private Channel channel; @GuardedBy("lock") private boolean isSubscribed = false; + @GuardedBy("lock") + private boolean isSubscribing = false; public ChannelHolder( Supplier supplier, @@ -758,6 +766,7 @@ public void retryingPublishWithLock(ChannelConsumer consumer, ConnectionManagerC public T retryingConsumeWithLock(ChannelMapper mapper, ConnectionManagerConfiguration configuration) throws InterruptedException, IOException { lock.lock(); + isSubscribing = true; try { Iterator iterator = null; Channel tempChannel = getChannel(); @@ -778,6 +787,7 @@ public T retryingConsumeWithLock(ChannelMapper mapper, ConnectionManagerC } } } finally { + isSubscribing = false; lock.unlock(); } } @@ -892,6 +902,10 @@ private Channel getChannel(boolean waitForRecovery) { public boolean subscribed() { return isSubscribed; } + + public boolean subscribing() { + return isSubscribing; + } } private interface ChannelMapper { diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/grpc/router/impl/DefaultGrpcRouterTest.kt b/src/test/kotlin/com/exactpro/th2/common/schema/grpc/router/impl/DefaultGrpcRouterTest.kt index 9f4c8eeb9..8b14abc8c 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/grpc/router/impl/DefaultGrpcRouterTest.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/grpc/router/impl/DefaultGrpcRouterTest.kt @@ -354,6 +354,11 @@ internal class DefaultGrpcRouterTest { ) ) } + + /* FIXME: this test sometimes failing with TimeoutException + Suppressed: java.lang.IllegalStateException: 'Executor' can't be stopped + at shutdownGracefully (DefaultGrpcRouterTest.kt:718) + */ @Test override fun `server terminated intermediate session (retry true)`() { val clientServerBaton = Baton("client-server") diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt index 76b00eae0..ceeb18ccd 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt @@ -30,7 +30,6 @@ import com.exactpro.th2.common.util.getChannelsInfo import com.exactpro.th2.common.util.getQueuesInfo import com.exactpro.th2.common.util.getSubscribedChannelsCount import com.exactpro.th2.common.util.putMessageInQueue -import com.exactpro.th2.common.util.restartContainer import com.rabbitmq.client.BuiltinExchangeType import java.time.Duration import java.util.concurrent.ArrayBlockingQueue @@ -412,9 +411,9 @@ class TestConnectionManager { } container - .withQueue(queueName) .use { it.start() + declareQueue(it, queueName) LOGGER.info { "Started with port ${it.amqpPort}" } ConnectionManager( RabbitMQConfiguration( @@ -445,8 +444,12 @@ class TestConnectionManager { LOGGER.info { "Rabbit address- ${it.host}:${it.amqpPort}" } LOGGER.info { "Restarting the container" } - restartContainer(it) - Thread.sleep(5000) + it.stop() + Thread.sleep(5_000) + it.start() + Thread.sleep(5_000) + declareQueue(it, queueName) + Thread.sleep(5_000) LOGGER.info { "Rabbit address after restart - ${it.host}:${it.amqpPort}" } LOGGER.info { getQueuesInfo(it) } From e79d84b113f9939975d5e0c682e508ab72b05499 Mon Sep 17 00:00:00 2001 From: Oleg Smelov <45400511+lumber1000@users.noreply.github.com> Date: Tue, 7 Nov 2023 12:35:44 +0400 Subject: [PATCH 37/51] comment proofreading Co-authored-by: Oleg Smirnov --- .../message/impl/rabbitmq/connection/ConnectionManager.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java index 689f43ed8..b8e06c7f5 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java @@ -751,9 +751,9 @@ public void retryingPublishWithLock(ChannelConsumer consumer, ConnectionManagerC LOGGER.warn("Retrying publishing #{}, waiting for {}ms. Reason: {}", currentValue.getTryNumber(), recoveryDelay, e); TimeUnit.MILLISECONDS.sleep(recoveryDelay); - // we should not recover channel if it's connection is closed, because if we'll do it channel - // will be also auto recovered by RabbitMQ client during recovering of connection, and we'll - // get two new channels instead of one closed. + // We should not recover the channel if its connection is closed + // If we do that the channel will be also auto recovered by RabbitMQ client + // during connection recovery, and we will get two new channels instead of one closed. if (!tempChannel.isOpen() && tempChannel.getConnection().isOpen()) { tempChannel = recreateChannel(); } From 385e4846d74090b619784faaecc64046f0d5f9e5 Mon Sep 17 00:00:00 2001 From: Oleg Smelov Date: Tue, 7 Nov 2023 15:03:23 +0400 Subject: [PATCH 38/51] fixes after review --- .../connection/ConnectionManager.java | 41 ++++++++++--------- .../connection/TestConnectionManager.kt | 1 - 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java index b8e06c7f5..a90ca9979 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java @@ -232,6 +232,11 @@ private void turnOffReadiness(Throwable exception) { } } + private final static List recoverableErrors = List.of( + "reply-text=NOT_FOUND", + "reply-text=PRECONDITION_FAILED" + ); + private void addShutdownListenerToChannel(Channel channel, Boolean withRecovery) { channel.addShutdownListener(cause -> { LOGGER.debug("Closing the channel: ", cause); @@ -247,14 +252,12 @@ private void addShutdownListenerToChannel(Channel channel, Boolean withRecovery) errorBuilder.append(channelCause); String errorString = errorBuilder.toString(); LOGGER.warn(errorString); - if (withRecovery && - (errorString.contains("reply-text=PRECONDITION_FAILED") - || errorString.contains("reply-text=NOT_FOUND")) - ) { + if (withRecovery && recoverableErrors.stream().anyMatch(errorString::contains)) { int channelNumber = channel.getChannelNumber(); var pinIdToChannelHolder = getChannelHolderByChannelNumber(channelNumber); - if (pinIdToChannelHolder != null && !pinIdToChannelHolder.getValue().subscribing()) + if ((pinIdToChannelHolder != null) && (pinIdToChannelHolder.getValue().state() != ChannelState.SUBSCRIBING)) { recoverSubscriptionsOfChannel(channelNumber); + } } } } @@ -282,7 +285,7 @@ private void recoverSubscriptionsOfChannel(int channelNumber) { SubscriptionCallbacks subscriptionCallbacks = channelHolder.subscriptionCallbacks; if (subscriptionCallbacks != null) { channelsByPin.remove(pinId); - if (channelHolder.subscribed()) { + if (channelHolder.state == ChannelState.SUBSCRIBED) { LOGGER.info("Changing channel for holder with pin id: " + pinId.toString()); basicConsume(pinId.queue, subscriptionCallbacks.deliverCallback, subscriptionCallbacks.cancelCallback); } @@ -673,6 +676,12 @@ public String toString() { } } + private enum ChannelState { + UNSUBSCRIBED, + SUBSCRIBING, + SUBSCRIBED + } + private static class ChannelHolder { private final Lock lock = new ReentrantLock(); private final Supplier supplier; @@ -686,9 +695,7 @@ private static class ChannelHolder { @GuardedBy("lock") private Channel channel; @GuardedBy("lock") - private boolean isSubscribed = false; - @GuardedBy("lock") - private boolean isSubscribing = false; + private ChannelState state = ChannelState.UNSUBSCRIBED; public ChannelHolder( Supplier supplier, @@ -766,20 +773,21 @@ public void retryingPublishWithLock(ChannelConsumer consumer, ConnectionManagerC public T retryingConsumeWithLock(ChannelMapper mapper, ConnectionManagerConfiguration configuration) throws InterruptedException, IOException { lock.lock(); - isSubscribing = true; + state = ChannelState.SUBSCRIBING; try { Iterator iterator = null; Channel tempChannel = getChannel(); while (true) { try { var tag = mapper.map(tempChannel); - isSubscribed = true; + state = ChannelState.SUBSCRIBED; return tag; } catch (IOException e) { iterator = handleAndSleep(configuration, iterator, "Retrying consume", e); var reason = tempChannel.getCloseReason(); if (reason != null) { if (reason.getMessage().contains("reply-text=RESOURCE_LOCKED")) { + state = ChannelState.UNSUBSCRIBED; throw e; } tempChannel = recreateChannel(); @@ -787,7 +795,6 @@ public T retryingConsumeWithLock(ChannelMapper mapper, ConnectionManagerC } } } finally { - isSubscribing = false; lock.unlock(); } } @@ -796,7 +803,7 @@ public void unsubscribeWithLock(String tag, CancelAction action) throws IOExcept lock.lock(); try { action.execute(channel, tag); - isSubscribed = false; + state = ChannelState.UNSUBSCRIBED; } finally { lock.unlock(); } @@ -899,12 +906,8 @@ private Channel getChannel(boolean waitForRecovery) { return channel; } - public boolean subscribed() { - return isSubscribed; - } - - public boolean subscribing() { - return isSubscribing; + public ChannelState state() { + return state; } } diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt index ceeb18ccd..65a66afcc 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt @@ -445,7 +445,6 @@ class TestConnectionManager { LOGGER.info { "Restarting the container" } it.stop() - Thread.sleep(5_000) it.start() Thread.sleep(5_000) declareQueue(it, queueName) From 3a295375cace70e86006bef53b60961d773a388e Mon Sep 17 00:00:00 2001 From: Oleg Smelov Date: Thu, 9 Nov 2023 09:55:15 +0400 Subject: [PATCH 39/51] locking reworked --- .../connection/ConnectionManager.java | 105 +++++++++--------- 1 file changed, 55 insertions(+), 50 deletions(-) diff --git a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java index a90ca9979..f43d99ad8 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java @@ -252,12 +252,14 @@ private void addShutdownListenerToChannel(Channel channel, Boolean withRecovery) errorBuilder.append(channelCause); String errorString = errorBuilder.toString(); LOGGER.warn(errorString); + + var pinIdToChannelHolder = getChannelHolderByChannel(channel); + if (pinIdToChannelHolder == null) return; + if (withRecovery && recoverableErrors.stream().anyMatch(errorString::contains)) { - int channelNumber = channel.getChannelNumber(); - var pinIdToChannelHolder = getChannelHolderByChannelNumber(channelNumber); - if ((pinIdToChannelHolder != null) && (pinIdToChannelHolder.getValue().state() != ChannelState.SUBSCRIBING)) { - recoverSubscriptionsOfChannel(channelNumber); - } + var pinId = pinIdToChannelHolder.getKey(); + var holder = pinIdToChannelHolder.getValue(); + recoverSubscriptionsOfChannel(pinId, channel, holder); } } } @@ -265,31 +267,41 @@ private void addShutdownListenerToChannel(Channel channel, Boolean withRecovery) }); } - private @Nullable Map.Entry getChannelHolderByChannelNumber(int channelNumber) { + private @Nullable Map.Entry getChannelHolderByChannel(Channel channel) { return channelsByPin .entrySet() .stream() - .filter(entry -> Objects.nonNull(entry.getValue().channel) && channelNumber == entry.getValue().channel.getChannelNumber()) + .filter(entry -> channel == entry.getValue().channel) .findAny() .orElse(null); } - private void recoverSubscriptionsOfChannel(int channelNumber) { + private void recoverSubscriptionsOfChannel(@NotNull final PinId pinId, Channel channel, @NotNull final ChannelHolder holder) { channelChecker.execute(() -> { try { - var pinIdToChannelHolder = getChannelHolderByChannelNumber(channelNumber); - if (pinIdToChannelHolder != null) { - PinId pinId = pinIdToChannelHolder.getKey(); - ChannelHolder channelHolder = pinIdToChannelHolder.getValue(); + holder.lock.lock(); + SubscriptionCallbacks subscriptionCallbacks = holder.subscriptionCallbacks; + boolean resubscribe = false; + try { + if (holder.channel != channel) { + LOGGER.warn("Channel already recovered"); + return; + } - SubscriptionCallbacks subscriptionCallbacks = channelHolder.subscriptionCallbacks; if (subscriptionCallbacks != null) { channelsByPin.remove(pinId); - if (channelHolder.state == ChannelState.SUBSCRIBED) { - LOGGER.info("Changing channel for holder with pin id: " + pinId.toString()); - basicConsume(pinId.queue, subscriptionCallbacks.deliverCallback, subscriptionCallbacks.cancelCallback); + if (holder.isSubscribed) { + holder.isSubscribed = false; + resubscribe = true; } } + } finally { + holder.lock.unlock(); + } + + if (resubscribe) { + LOGGER.info("Changing channel for holder with pin id: " + pinId); + basicConsume(pinId.queue, subscriptionCallbacks.deliverCallback, subscriptionCallbacks.cancelCallback); } } catch (IOException e) { LOGGER.warn("Failed to recovery channel's subscriptions", e); @@ -676,12 +688,6 @@ public String toString() { } } - private enum ChannelState { - UNSUBSCRIBED, - SUBSCRIBING, - SUBSCRIBED - } - private static class ChannelHolder { private final Lock lock = new ReentrantLock(); private final Supplier supplier; @@ -695,7 +701,7 @@ private static class ChannelHolder { @GuardedBy("lock") private Channel channel; @GuardedBy("lock") - private ChannelState state = ChannelState.UNSUBSCRIBED; + private boolean isSubscribed = false; public ChannelHolder( Supplier supplier, @@ -772,30 +778,33 @@ public void retryingPublishWithLock(ChannelConsumer consumer, ConnectionManagerC } public T retryingConsumeWithLock(ChannelMapper mapper, ConnectionManagerConfiguration configuration) throws InterruptedException, IOException { - lock.lock(); - state = ChannelState.SUBSCRIBING; - try { - Iterator iterator = null; - Channel tempChannel = getChannel(); - while (true) { - try { - var tag = mapper.map(tempChannel); - state = ChannelState.SUBSCRIBED; - return tag; - } catch (IOException e) { - iterator = handleAndSleep(configuration, iterator, "Retrying consume", e); - var reason = tempChannel.getCloseReason(); - if (reason != null) { - if (reason.getMessage().contains("reply-text=RESOURCE_LOCKED")) { - state = ChannelState.UNSUBSCRIBED; - throw e; - } - tempChannel = recreateChannel(); - } + Iterator iterator = null; + IOException exception; + Channel tempChannel = null; + boolean isChannelClosed = false; + while (true) { + lock.lock(); + try { + if (tempChannel == null) { + tempChannel = getChannel(); + } else if (isChannelClosed) { + tempChannel = recreateChannel(); + } + + var tag = mapper.map(tempChannel); + isSubscribed = true; + return tag; + } catch (IOException e) { + var reason = tempChannel.getCloseReason(); + isChannelClosed = reason != null; + if (isChannelClosed && reason.getMessage().contains("reply-text=RESOURCE_LOCKED")) { + throw e; } + exception = e; + } finally { + lock.unlock(); } - } finally { - lock.unlock(); + iterator = handleAndSleep(configuration, iterator, "Retrying consume", exception); } } @@ -803,7 +812,7 @@ public void unsubscribeWithLock(String tag, CancelAction action) throws IOExcept lock.lock(); try { action.execute(channel, tag); - state = ChannelState.UNSUBSCRIBED; + isSubscribed = false; } finally { lock.unlock(); } @@ -905,10 +914,6 @@ private Channel getChannel(boolean waitForRecovery) { reconnectionChecker.accept(channel, waitForRecovery); return channel; } - - public ChannelState state() { - return state; - } } private interface ChannelMapper { From 3fb90369419296932d1209b06183fc36f709d554 Mon Sep 17 00:00:00 2001 From: Oleg Smelov Date: Mon, 13 Nov 2023 11:00:35 +0400 Subject: [PATCH 40/51] test added to TestConnectionManager (multiple subscribers in parallel) --- .../connection/TestConnectionManager.kt | 186 ++++++++++++++++++ 1 file changed, 186 insertions(+) diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt index 65a66afcc..e11d0e1cf 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt @@ -20,6 +20,7 @@ import com.exactpro.th2.common.annotations.IntegrationTest import com.exactpro.th2.common.schema.message.ContainerConstants.DEFAULT_CONFIRMATION_TIMEOUT import com.exactpro.th2.common.schema.message.ContainerConstants.DEFAULT_PREFETCH_COUNT import com.exactpro.th2.common.schema.message.ContainerConstants.RABBITMQ_IMAGE_NAME +import com.exactpro.th2.common.schema.message.DeliveryMetadata import com.exactpro.th2.common.schema.message.ManualAckDeliveryCallback import com.exactpro.th2.common.schema.message.SubscriberMonitor import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.ConnectionManagerConfiguration @@ -38,6 +39,7 @@ import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger import kotlin.concurrent.thread import com.rabbitmq.client.CancelCallback +import com.rabbitmq.client.Delivery import mu.KotlinLogging import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.Assertions @@ -707,6 +709,190 @@ class TestConnectionManager { } } + @Test + fun `connection manager handles ack timeout (multiple subscribers in parallel)`() { + val configFilename = "rabbitmq_it.conf" + + class Counters { + val messages = AtomicInteger(0) + val redeliveredMessages = AtomicInteger(0) + } + + class ConsumerParams( + val unsubscribe: Boolean, + val expectedReceivedMessages: Int, + val expectedRedeliveredMessages: Int + ) + + class TestParams( + val queueName: String, + val subscriberName: String, + val consumers: List, + val messagesToSend: Int, + val expectedChannelsCount: Int, + val expectedLeftMessages: Int + ) + + val testCases = listOf( + TestParams( + queueName = "queue1", + subscriberName = "subscriber1", + consumers = listOf( + ConsumerParams( + unsubscribe = false, + expectedReceivedMessages = 3, + expectedRedeliveredMessages = 1 + ) + ), + expectedChannelsCount = 1, + messagesToSend = 2, + expectedLeftMessages = 0 + ), + + TestParams( + queueName = "queue2", + subscriberName = "subscriber2", + consumers = listOf( + ConsumerParams( + unsubscribe = true, + expectedReceivedMessages = 2, + expectedRedeliveredMessages = 0 + ) + ), + expectedChannelsCount = 0, + messagesToSend = 2, + expectedLeftMessages = 1 + ), + + TestParams( + queueName = "queue3", + subscriberName = "subscriber3", + consumers = listOf( + ConsumerParams( + unsubscribe = true, + expectedReceivedMessages = 2, + expectedRedeliveredMessages = 0 + ), + ConsumerParams( + unsubscribe = true, + expectedReceivedMessages = 2, + expectedRedeliveredMessages = 0 + ) + ), + messagesToSend = 4, + expectedChannelsCount = 0, + expectedLeftMessages = 2 + ) + ) + + RabbitMQContainer(RABBITMQ_IMAGE_NAME) + .withRabbitMQConfig(MountableFile.forClasspathResource(configFilename)) + .apply { testCases.forEach { withQueue(it.queueName) } } + .use { container -> + container.start() + LOGGER.info { "Started with port ${container.amqpPort}" } + + class TestCaseContext( + val connectionManager: ConnectionManager, + val consumersThreads: List, + val consumerCounters: List + ) + + val testCasesContexts: List = testCases.map { params -> + val connectionManager = createConnectionManager( + container, + ConnectionManagerConfiguration( + subscriberName = params.subscriberName, + prefetchCount = DEFAULT_PREFETCH_COUNT, + confirmationTimeout = DEFAULT_CONFIRMATION_TIMEOUT, + minConnectionRecoveryTimeout = 100, + maxConnectionRecoveryTimeout = 200, + maxRecoveryAttempts = 5 + ) + ) + + val consumerCounters: List = List(params.consumers.size) { Counters() } + + class DeliverCallback(private val consumerNumber: Int) : ManualAckDeliveryCallback { + override fun handle( + deliveryMetadata: DeliveryMetadata, + delivery: Delivery, + confirmProcessed: ManualAckDeliveryCallback.Confirmation + ) { + val consumerCounter = consumerCounters[consumerNumber] + + LOGGER.info { "Received ${delivery.body.toString(Charsets.UTF_8)} from \"${delivery.envelope.routingKey}\"" } + if (consumerCounter.messages.getAndIncrement() == 1) { + LOGGER.info { "Left this message unacked" } + } else { + confirmProcessed.confirm() + LOGGER.info { "Confirmed!" } + } + + if (delivery.envelope.isRedeliver) { + consumerCounter.redeliveredMessages.incrementAndGet() + } + } + } + + val consumersThreads = params.consumers.mapIndexed { index, subscriberParams -> + thread { + val subscriberMonitor = connectionManager.basicConsume(params.queueName, DeliverCallback(index)) { + LOGGER.info { "Canceled $it" } + } + + Thread.sleep(1000) + + if (subscriberParams.unsubscribe) { + LOGGER.info { "Unsubscribing..." } + subscriberMonitor.unsubscribe() + } + } + } + + repeat(params.messagesToSend) { + LOGGER.info { "Sending message ${it + 1} to queue ${params.queueName}" } + putMessageInQueue(container, params.queueName) + } + + TestCaseContext(connectionManager, consumersThreads, consumerCounters) + } + + LOGGER.info { "Sleeping..." } + Thread.sleep(63000) + + val queuesListExecResult = getQueuesInfo(container) + LOGGER.info { "queues list: \n $queuesListExecResult" } + + testCases.forEachIndexed { index, params -> + val context = testCasesContexts[index] + assertEquals(params.expectedChannelsCount, getSubscribedChannelsCount(container, params.queueName)) { + "Wrong number of opened channels (subscriber: `${params.subscriberName}`)" + } + + params.consumers.forEachIndexed { consumerIndex, consumerParams -> + val counters = context.consumerCounters[consumerIndex] + assertEquals(consumerParams.expectedReceivedMessages, counters.messages.get()) { + "Wrong number of received messages (subscriber: `${params.subscriberName}`, consumer index: `${consumerIndex}`)" + } + + assertEquals(consumerParams.expectedRedeliveredMessages, counters.redeliveredMessages.get()) { + "Wrong number of redelivered messages (subscriber: `${params.subscriberName}`, consumer index: `${consumerIndex}`)" + } + } + + assertTrue(queuesListExecResult.toString().contains("${params.queueName}\t${params.expectedLeftMessages}")) { + "There should ${params.expectedLeftMessages} message(s) left in the '${params.queueName}' queue" + } + } + + testCasesContexts.forEach { context -> + context.consumersThreads.forEach { it.interrupt() } + context.connectionManager.close() + } + } + } + @Test fun `connection manager handles ack timeout and subscription cancel`() { val configFilename = "rabbitmq_it.conf" From 41c247e2e883fcf8c3e1c73620e63935fe90aaca Mon Sep 17 00:00:00 2001 From: Oleg Smelov Date: Mon, 13 Nov 2023 15:36:25 +0400 Subject: [PATCH 41/51] unnecessary `isSubscribed` flag reset removed --- .../message/impl/rabbitmq/connection/ConnectionManager.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java index f43d99ad8..daea64c47 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java @@ -290,10 +290,7 @@ private void recoverSubscriptionsOfChannel(@NotNull final PinId pinId, Channel c if (subscriptionCallbacks != null) { channelsByPin.remove(pinId); - if (holder.isSubscribed) { - holder.isSubscribed = false; - resubscribe = true; - } + resubscribe = holder.isSubscribed; } } finally { holder.lock.unlock(); From 28b9e4b5662f610394e72d2a56ef32c05a12c472 Mon Sep 17 00:00:00 2001 From: Oleg Smelov Date: Mon, 13 Nov 2023 16:02:16 +0400 Subject: [PATCH 42/51] `recoverSubscriptionsOfChannel` refactored --- .../connection/ConnectionManager.java | 56 ++++++++++--------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java index daea64c47..9b8f635d1 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java @@ -61,6 +61,7 @@ import java.util.Map; import java.util.Objects; import java.util.Collections; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -277,36 +278,20 @@ private void addShutdownListenerToChannel(Channel channel, Boolean withRecovery) } private void recoverSubscriptionsOfChannel(@NotNull final PinId pinId, Channel channel, @NotNull final ChannelHolder holder) { - channelChecker.execute(() -> { - try { - holder.lock.lock(); - SubscriptionCallbacks subscriptionCallbacks = holder.subscriptionCallbacks; - boolean resubscribe = false; + channelChecker.execute(() -> + holder.getCallbacksForRecovery(channel).ifPresent(subscriptionCallbacks -> { + LOGGER.info("Changing channel for holder with pin id: " + pinId); + channelsByPin.remove(pinId); try { - if (holder.channel != channel) { - LOGGER.warn("Channel already recovered"); - return; - } - - if (subscriptionCallbacks != null) { - channelsByPin.remove(pinId); - resubscribe = holder.isSubscribed; - } - } finally { - holder.lock.unlock(); - } - - if (resubscribe) { - LOGGER.info("Changing channel for holder with pin id: " + pinId); basicConsume(pinId.queue, subscriptionCallbacks.deliverCallback, subscriptionCallbacks.cancelCallback); + } catch (IOException e) { + LOGGER.warn("Failed to recovery channel's subscriptions", e); + // this code executed in executor service and exception thrown here will not be handled anywhere + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } - } catch (IOException e) { - LOGGER.warn("Failed to recovery channel's subscriptions", e); - // this code executed in executor service and exception thrown here will not be handled anywhere - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - }); + }) + ); } private void addShutdownListenerToConnection(Connection conn) { @@ -805,6 +790,23 @@ public T retryingConsumeWithLock(ChannelMapper mapper, ConnectionManagerC } } + public Optional getCallbacksForRecovery(Channel channelToRecover) { + lock.lock(); + SubscriptionCallbacks callbacks = null; + try { + if (channel != channelToRecover) { + LOGGER.warn("Channel already recovered"); + } else if (subscriptionCallbacks != null && isSubscribed) { + isSubscribed = false; + callbacks = subscriptionCallbacks; + } + } finally { + lock.unlock(); + } + + return Optional.ofNullable(callbacks); + } + public void unsubscribeWithLock(String tag, CancelAction action) throws IOException { lock.lock(); try { From 672f098671e8c1fdb441f14d4211ad4b7d5ea40c Mon Sep 17 00:00:00 2001 From: Oleg Smelov Date: Mon, 13 Nov 2023 16:59:01 +0400 Subject: [PATCH 43/51] sync with dev-version-5 --- .../schema/grpc/router/impl/DefaultGrpcRouterTest.kt | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/grpc/router/impl/DefaultGrpcRouterTest.kt b/src/test/kotlin/com/exactpro/th2/common/schema/grpc/router/impl/DefaultGrpcRouterTest.kt index 225add65f..943d7aa9b 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/grpc/router/impl/DefaultGrpcRouterTest.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/grpc/router/impl/DefaultGrpcRouterTest.kt @@ -289,13 +289,6 @@ internal class DefaultGrpcRouterTest { ) } -<<<<<<< HEAD - /* FIXME: this test sometimes failing with TimeoutException - Suppressed: java.lang.IllegalStateException: 'Executor' can't be stopped - at shutdownGracefully (DefaultGrpcRouterTest.kt:718) - */ -======= ->>>>>>> dev-version-5 @Test override fun `server terminated intermediate session (retry true)`() { val clientServerBaton = Baton("client-server") From 50e8891111e85b1a65c880db08ce675136ed2c94 Mon Sep 17 00:00:00 2001 From: Oleg Smelov Date: Thu, 16 Nov 2023 19:39:11 +0400 Subject: [PATCH 44/51] changes after review --- README.md | 5 ++++- .../impl/rabbitmq/connection/ConnectionManager.java | 10 ++++++---- .../rabbitmq/configuration/RabbitMQConfiguration.kt | 10 +++++++--- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 586cce1ec..b486f67c9 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,9 @@ The `CommonFactory` reads a RabbitMQ configuration from the rabbitMQ.json file. * maxConnectionRecoveryTimeout - this option defines a maximum interval in milliseconds between reconnect attempts, with its default value set to 60000. Common factory increases the reconnect interval values from minConnectionRecoveryTimeout to maxConnectionRecoveryTimeout. +* retryTimeDeviationPercent - specifies random deviation to delay interval duration. Default value is 10 percents. + E.g. if delay interval is 30 seconds and `retryTimeDeviationPercent` is 10 percents the actual duration of interval + will be random value from 27 to 33 seconds. * prefetchCount - this option is the maximum number of messages that the server will deliver, with its value set to 0 if unlimited, the default value is set to 10. * messageRecursionLimit - an integer number denotes how deep the nested protobuf message might be, set by default 100 @@ -508,7 +511,7 @@ dependencies { + Added InterruptedException to basicConsume method signature. + Added additional logging for RabbitMQ errors. + Fixed connection recovery delay time. -+ Integration tests for these scenarios. ++ Integration tests for RabbitMQ retry scenarios. ### 5.7.1-dev diff --git a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java index 9b8f635d1..438c0748e 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java @@ -343,7 +343,7 @@ public boolean isOpen() { } @Override - public void close() throws IOException { + public void close() { if (connectionIsClosed.getAndSet(true)) { LOGGER.info("Connection manager already closed"); return; @@ -352,14 +352,16 @@ public void close() throws IOException { LOGGER.info("Closing connection manager"); for (ChannelHolder channelHolder: channelsByPin.values()) { - channelHolder.channel.abort(); + try { + channelHolder.channel.abort(); + } catch (IOException e) { + LOGGER.error("Cannot close channel", e); + } } int closeTimeout = configuration.getConnectionCloseTimeout(); if (connection.isOpen()) { try { - // We close the connection and don't close channels - // because when a channel's connection is closed, so is the channel connection.close(closeTimeout); } catch (IOException e) { LOGGER.error("Cannot close connection", e); diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/configuration/RabbitMQConfiguration.kt b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/configuration/RabbitMQConfiguration.kt index e5f960465..6c147e5c0 100644 --- a/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/configuration/RabbitMQConfiguration.kt +++ b/src/main/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/configuration/RabbitMQConfiguration.kt @@ -48,6 +48,7 @@ data class ConnectionManagerConfiguration( check(maxRecoveryAttempts > 0) { "expected 'maxRecoveryAttempts' greater than 0 but was $maxRecoveryAttempts" } check(minConnectionRecoveryTimeout > 0) { "expected 'minConnectionRecoveryTimeout' greater than 0 but was $minConnectionRecoveryTimeout" } check(maxConnectionRecoveryTimeout >= minConnectionRecoveryTimeout) { "expected 'maxConnectionRecoveryTimeout' ($maxConnectionRecoveryTimeout) no less than 'minConnectionRecoveryTimeout' ($minConnectionRecoveryTimeout)" } + check(retryTimeDeviationPercent in 0..100) { "expected 'retryTimeDeviationPercent' no less than 0 and not greater than 100 but was $retryTimeDeviationPercent" } check(workingThreads > 0) { "expected 'workingThreads' greater than 0 but was $workingThreads" } check(!confirmationTimeout.run { isNegative || isZero }) { "expected 'confirmationTimeout' greater than 0 but was $confirmationTimeout" } } @@ -76,7 +77,7 @@ data class RetryingDelay(val tryNumber: Int, val delay: Int) { deviationPercent: Int ): Int { return if (numberOfTries <= maxRecoveryAttempts) { - getRecoveryDelayWithIncrement(numberOfTries, minTime, maxTime, maxRecoveryAttempts) + getRecoveryDelayWithIncrement(numberOfTries, minTime, maxTime, maxRecoveryAttempts, deviationPercent) } else { getRecoveryDelayWithDeviation(maxTime, deviationPercent) } @@ -93,9 +94,12 @@ data class RetryingDelay(val tryNumber: Int, val delay: Int) { numberOfTries: Int, minTime: Int, maxTime: Int, - maxRecoveryAttempts: Int + maxRecoveryAttempts: Int, + deviationPercent: Int ): Int { - return minTime + (maxTime - minTime) / maxRecoveryAttempts * numberOfTries + val delay = minTime + (maxTime - minTime) / maxRecoveryAttempts * numberOfTries + val deviation = maxTime * deviationPercent / 100 + return ThreadLocalRandom.current().nextInt(delay - deviation, delay + deviation + 1) } } } \ No newline at end of file From 04df9ac2ee84d19a5271867a24f9d13d1dba76af Mon Sep 17 00:00:00 2001 From: Oleg Smelov Date: Fri, 17 Nov 2023 04:01:11 +0400 Subject: [PATCH 45/51] locking in `retryingPublishWithLock` filtering out recovering tasks for non subscribed channels --- .../connection/ConnectionManager.java | 59 +++++++++++-------- 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java index 438c0748e..ea63f1b5c 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java @@ -258,9 +258,11 @@ private void addShutdownListenerToChannel(Channel channel, Boolean withRecovery) if (pinIdToChannelHolder == null) return; if (withRecovery && recoverableErrors.stream().anyMatch(errorString::contains)) { - var pinId = pinIdToChannelHolder.getKey(); var holder = pinIdToChannelHolder.getValue(); - recoverSubscriptionsOfChannel(pinId, channel, holder); + if (holder.isSubscribed()) { + var pinId = pinIdToChannelHolder.getKey(); + recoverSubscriptionsOfChannel(pinId, channel, holder); + } } } } @@ -734,30 +736,30 @@ public void withLock(boolean waitForRecovery, ChannelConsumer consumer) throws I } public void retryingPublishWithLock(ChannelConsumer consumer, ConnectionManagerConfiguration configuration) throws InterruptedException { - lock.lock(); Iterator iterator = configuration.createRetryingDelaySequence().iterator(); - try { - Channel tempChannel = getChannel(true); - while (true) { - try { - consumer.consume(tempChannel); - break; - } catch (IOException | ShutdownSignalException e) { - var currentValue = iterator.next(); - int recoveryDelay = currentValue.getDelay(); - LOGGER.warn("Retrying publishing #{}, waiting for {}ms. Reason: {}", currentValue.getTryNumber(), recoveryDelay, e); - TimeUnit.MILLISECONDS.sleep(recoveryDelay); - - // We should not recover the channel if its connection is closed - // If we do that the channel will be also auto recovered by RabbitMQ client - // during connection recovery, and we will get two new channels instead of one closed. - if (!tempChannel.isOpen() && tempChannel.getConnection().isOpen()) { - tempChannel = recreateChannel(); - } + int recoveryDelay; + Channel tempChannel = getChannel(true); + while (true) { + lock.lock(); + try { + consumer.consume(tempChannel); + break; + } catch (IOException | ShutdownSignalException e) { + var currentValue = iterator.next(); + recoveryDelay = currentValue.getDelay(); + LOGGER.warn("Retrying publishing #{}, waiting for {}ms. Reason: {}", currentValue.getTryNumber(), recoveryDelay, e); + + // We should not recover the channel if its connection is closed + // If we do that the channel will be also auto recovered by RabbitMQ client + // during connection recovery, and we will get two new channels instead of one closed. + if (!tempChannel.isOpen() && tempChannel.getConnection().isOpen()) { + tempChannel = recreateChannel(); } + } finally { + lock.unlock(); } - } finally { - lock.unlock(); + + TimeUnit.MILLISECONDS.sleep(recoveryDelay); } } @@ -797,7 +799,7 @@ public Optional getCallbacksForRecovery(Channel channelTo SubscriptionCallbacks callbacks = null; try { if (channel != channelToRecover) { - LOGGER.warn("Channel already recovered"); + LOGGER.error("Channel already recovered"); } else if (subscriptionCallbacks != null && isSubscribed) { isSubscribed = false; callbacks = subscriptionCallbacks; @@ -889,6 +891,15 @@ public void acquireAndSubmitCheck(Supplier> futureSupplier) { } } + public boolean isSubscribed() { + lock.lock(); + try { + return isSubscribed; + } finally { + lock.unlock(); + } + } + public boolean reachedPendingLimit() { lock.lock(); try { From df8589019317db3fd6babc5aa5497acee50a2f0e Mon Sep 17 00:00:00 2001 From: Oleg Smelov Date: Fri, 17 Nov 2023 13:40:16 +0400 Subject: [PATCH 46/51] README.md duplicated test removed addShutdownListenerToChannel refactored --- README.md | 1 + .../connection/ConnectionManager.java | 17 ++-- .../connection/TestConnectionManager.kt | 93 ------------------- 3 files changed, 9 insertions(+), 102 deletions(-) diff --git a/README.md b/README.md index b486f67c9..43728226c 100644 --- a/README.md +++ b/README.md @@ -382,6 +382,7 @@ describes gRPC service structure) This kind of router provides the ability for component to send / receive messages via RabbitMQ. Router has several methods to subscribe and publish RabbitMQ messages steam (th2 use batches of messages or events as transport). +Supports recovery of subscriptions cancelled by RabbitMQ due to following errors: "delivery acknowledgement timed out" and "queue not found". #### Choice pin by attributes diff --git a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java index ea63f1b5c..a2d245b47 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java @@ -253,15 +253,14 @@ private void addShutdownListenerToChannel(Channel channel, Boolean withRecovery) errorBuilder.append(channelCause); String errorString = errorBuilder.toString(); LOGGER.warn(errorString); - - var pinIdToChannelHolder = getChannelHolderByChannel(channel); - if (pinIdToChannelHolder == null) return; - - if (withRecovery && recoverableErrors.stream().anyMatch(errorString::contains)) { - var holder = pinIdToChannelHolder.getValue(); - if (holder.isSubscribed()) { - var pinId = pinIdToChannelHolder.getKey(); - recoverSubscriptionsOfChannel(pinId, channel, holder); + if (withRecovery) { + var pinIdToChannelHolder = getChannelHolderByChannel(channel); + if (pinIdToChannelHolder != null && recoverableErrors.stream().anyMatch(errorString::contains)) { + var holder = pinIdToChannelHolder.getValue(); + if (holder.isSubscribed()) { + var pinId = pinIdToChannelHolder.getKey(); + recoverSubscriptionsOfChannel(pinId, channel, holder); + } } } } diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt index e11d0e1cf..4fa1263c0 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt @@ -762,26 +762,6 @@ class TestConnectionManager { expectedChannelsCount = 0, messagesToSend = 2, expectedLeftMessages = 1 - ), - - TestParams( - queueName = "queue3", - subscriberName = "subscriber3", - consumers = listOf( - ConsumerParams( - unsubscribe = true, - expectedReceivedMessages = 2, - expectedRedeliveredMessages = 0 - ), - ConsumerParams( - unsubscribe = true, - expectedReceivedMessages = 2, - expectedRedeliveredMessages = 0 - ) - ), - messagesToSend = 4, - expectedChannelsCount = 0, - expectedLeftMessages = 2 ) ) @@ -893,79 +873,6 @@ class TestConnectionManager { } } - @Test - fun `connection manager handles ack timeout and subscription cancel`() { - val configFilename = "rabbitmq_it.conf" - val queueName = "queue" - - RabbitMQContainer(RABBITMQ_IMAGE_NAME) - .withRabbitMQConfig(MountableFile.forClasspathResource(configFilename)) - .withQueue(queueName) - .use { - it.start() - LOGGER.info { "Started with port ${it.amqpPort}" } - val counter = AtomicInteger(0) - createConnectionManager( - it, - ConnectionManagerConfiguration( - subscriberName = "test", - prefetchCount = DEFAULT_PREFETCH_COUNT, - confirmationTimeout = DEFAULT_CONFIRMATION_TIMEOUT, - minConnectionRecoveryTimeout = 100, - maxConnectionRecoveryTimeout = 200, - maxRecoveryAttempts = 5 - ), - ).use { connectionManager -> - var thread: Thread? = null - try { - thread = thread { - val subscriberMonitor = connectionManager.basicConsume(queueName, { _, delivery, ack -> - LOGGER.info { "Received 1 ${delivery.body.toString(Charsets.UTF_8)} from \"${delivery.envelope.routingKey}\"" } - if (counter.get() == 0) { - ack.confirm() - LOGGER.info { "Confirmed!" } - } else { - LOGGER.info { "Left this message unacked" } - } - counter.incrementAndGet() - }) { - LOGGER.info { "Canceled $it" } - } - - Thread.sleep(30000) - LOGGER.info { "Unsubscribing..." } - subscriberMonitor.unsubscribe() - } - - LOGGER.info { "Sending first message" } - putMessageInQueue(it, queueName) - - LOGGER.info { "queues list: \n ${getQueuesInfo(it)}" } - - LOGGER.info { "Sending second message" } - putMessageInQueue(it, queueName) - LOGGER.info { "Sleeping..." } - Thread.sleep(63000) - - val queuesListExecResult = getQueuesInfo(it) - LOGGER.info { "queues list: \n $queuesListExecResult" } - - assertEquals(0, getSubscribedChannelsCount(it, queueName)) - assertEquals(2, counter.get()) { "Wrong number of received messages" } - assertTrue(queuesListExecResult.toString().contains("$queueName\t1")) { - "There should a message left in the queue" - } - } finally { - Assertions.assertNotNull(thread) - Assertions.assertDoesNotThrow { - thread!!.interrupt() - } - assertFalse(thread!!.isAlive) - } - } - } - } - private fun CountDownLatch.assertComplete(message: String) { assertTrue( await( From d9f110be3d57d8db52049a10cc98780c44b837dc Mon Sep 17 00:00:00 2001 From: Oleg Smelov Date: Fri, 17 Nov 2023 17:22:02 +0400 Subject: [PATCH 47/51] recoverSubscriptionsOfChannel refactored dependencies update --- build.gradle | 54 +++++++++---------- .../connection/ConnectionManager.java | 45 +++++++++------- .../schema/message/ContainerConstants.kt | 2 +- 3 files changed, 54 insertions(+), 47 deletions(-) diff --git a/build.gradle b/build.gradle index 0a6dcd40f..4cc66004f 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,7 @@ buildscript { } dependencies { - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlin_version}" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -21,15 +21,15 @@ plugins { id 'java-library' id 'java-test-fixtures' id 'maven-publish' - id "io.github.gradle-nexus.publish-plugin" version "1.0.0" + id "io.github.gradle-nexus.publish-plugin" version "1.3.0" id 'signing' - id 'org.jetbrains.kotlin.jvm' version "${kotlin_version}" - id 'org.jetbrains.kotlin.kapt' version "${kotlin_version}" - id "org.owasp.dependencycheck" version "8.3.1" - id "me.champeau.jmh" version "0.6.8" + id 'org.jetbrains.kotlin.jvm' version "$kotlin_version" + id 'org.jetbrains.kotlin.kapt' version "$kotlin_version" + id "org.owasp.dependencycheck" version "8.4.3" + id "me.champeau.jmh" version "0.7.2" id "com.gorylenko.gradle-git-properties" version "2.4.1" id 'com.github.jk1.dependency-license-report' version '2.5' - id "de.undercouch.download" version "5.4.0" + id "de.undercouch.download" version "5.5.0" id "com.google.protobuf" version "0.9.4" } @@ -41,8 +41,8 @@ ext { protobufVersion = '3.23.2' // The protoc:3.23.3 https://github.com/protocolbuffers/protobuf/issues/13070 serviceGeneratorVersion = '3.5.1' - cradleVersion = '5.1.1-dev' - junitVersion = '5.10.0' + cradleVersion = '5.1.4-dev' + junitVersion = '5.10.1' genBaseDir = file("${buildDir}/generated/source/proto") } @@ -184,7 +184,7 @@ dependencies { api('com.exactpro.th2:grpc-common:4.3.0-dev') { because('protobuf transport is main now, this dependnecy should be moved to grpc, mq protobuf modules after splitting') } - api("com.exactpro.th2:cradle-core:${cradleVersion}") { + api("com.exactpro.th2:cradle-core:$cradleVersion") { because('cradle is included into common library now, this dependnecy should be moved to a cradle module after splitting') } api('io.netty:netty-buffer') { @@ -195,10 +195,10 @@ dependencies { jmh 'org.openjdk.jmh:jmh-generator-annprocess:0.9' implementation 'com.google.protobuf:protobuf-java-util' - implementation "com.exactpro.th2:grpc-service-generator:${serviceGeneratorVersion}" - implementation "com.exactpro.th2:cradle-cassandra:${cradleVersion}" + implementation "com.exactpro.th2:grpc-service-generator:$serviceGeneratorVersion" + implementation "com.exactpro.th2:cradle-cassandra:$cradleVersion" - def autoValueVersion = '1.10.1' + def autoValueVersion = '1.10.4' implementation "com.google.auto.value:auto-value-annotations:$autoValueVersion" kapt("com.google.auto.value:auto-value:$autoValueVersion") { //FIXME: Updated library because it is fat jar @@ -237,7 +237,7 @@ dependencies { implementation "com.fasterxml.jackson.module:jackson-module-kotlin" implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-cbor' - implementation 'com.fasterxml.uuid:java-uuid-generator:4.0.1' + implementation "com.fasterxml.uuid:java-uuid-generator:4.3.0" implementation 'org.apache.logging.log4j:log4j-slf4j2-impl' implementation 'org.apache.logging.log4j:log4j-core' @@ -247,29 +247,29 @@ dependencies { implementation 'io.prometheus:simpleclient_httpserver' implementation 'io.prometheus:simpleclient_log4j2' - implementation('com.squareup.okio:okio:3.5.0') { + implementation("com.squareup.okio:okio:3.6.0") { because('fix vulnerability in transitive dependency ') } - implementation('com.squareup.okhttp3:okhttp:4.11.0') { + implementation("com.squareup.okhttp3:okhttp:4.12.0") { because('fix vulnerability in transitive dependency ') } - implementation('io.fabric8:kubernetes-client:6.8.0') { + implementation("io.fabric8:kubernetes-client:6.9.2") { exclude group: 'com.fasterxml.jackson.dataformat', module: 'jackson-dataformat-yaml' } - implementation 'io.github.microutils:kotlin-logging:3.0.0' // The last version bases on kotlin 1.6.0 + implementation "io.github.microutils:kotlin-logging:3.0.5" testImplementation 'javax.annotation:javax.annotation-api:1.3.2' - testImplementation "org.junit.jupiter:junit-jupiter:${junitVersion}" - testImplementation 'org.mockito.kotlin:mockito-kotlin:4.0.0' + testImplementation "org.junit.jupiter:junit-jupiter:$junitVersion" + testImplementation "org.mockito.kotlin:mockito-kotlin:5.1.0" testImplementation 'org.jetbrains.kotlin:kotlin-test-junit5' - testImplementation "org.testcontainers:testcontainers:1.17.4" - testImplementation "org.testcontainers:rabbitmq:1.17.4" + testImplementation "org.testcontainers:testcontainers:1.19.2" + testImplementation "org.testcontainers:rabbitmq:1.19.2" testImplementation("org.junit-pioneer:junit-pioneer:2.1.0") { because("system property tests") } - testFixturesImplementation group: 'org.jetbrains.kotlin', name: 'kotlin-test-junit5', version: kotlin_version - testFixturesImplementation "org.junit.jupiter:junit-jupiter:${junitVersion}" + testFixturesImplementation "org.jetbrains.kotlin:kotlin-test-junit5:$kotlin_version" + testFixturesImplementation "org.junit.jupiter:junit-jupiter:$junitVersion" } jar { @@ -293,14 +293,14 @@ sourceSets { protobuf { protoc { - artifact = "com.google.protobuf:protoc:${protobufVersion}" + artifact = "com.google.protobuf:protoc:$protobufVersion" } plugins { grpc { - artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" + artifact = "io.grpc:protoc-gen-grpc-java:$grpcVersion" } services { - artifact = "com.exactpro.th2:grpc-service-generator:${serviceGeneratorVersion}:all@jar" + artifact = "com.exactpro.th2:grpc-service-generator:$serviceGeneratorVersion:all@jar" } } generateProtoTasks { diff --git a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java index a2d245b47..bfbd49459 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java @@ -61,7 +61,6 @@ import java.util.Map; import java.util.Objects; import java.util.Collections; -import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -279,20 +278,24 @@ private void addShutdownListenerToChannel(Channel channel, Boolean withRecovery) } private void recoverSubscriptionsOfChannel(@NotNull final PinId pinId, Channel channel, @NotNull final ChannelHolder holder) { - channelChecker.execute(() -> - holder.getCallbacksForRecovery(channel).ifPresent(subscriptionCallbacks -> { - LOGGER.info("Changing channel for holder with pin id: " + pinId); - channelsByPin.remove(pinId); - try { + channelChecker.execute(() -> { + try { + var subscriptionCallbacks = holder.getCallbacksForRecovery(channel); + + if (subscriptionCallbacks != null) { + LOGGER.info("Changing channel for holder with pin id: " + pinId); + channelsByPin.remove(pinId); + if (channel.isOpen()) channel.abort(); basicConsume(pinId.queue, subscriptionCallbacks.deliverCallback, subscriptionCallbacks.cancelCallback); - } catch (IOException e) { - LOGGER.warn("Failed to recovery channel's subscriptions", e); - // this code executed in executor service and exception thrown here will not be handled anywhere - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); } - }) - ); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOGGER.info("Recovering channel's subscriptions interrupted", e); + } catch (Throwable e) { + // this code executed in executor service and exception thrown here will not be handled anywhere + LOGGER.error("Failed to recovery channel's subscriptions", e); + } + }); } private void addShutdownListenerToConnection(Connection conn) { @@ -782,6 +785,10 @@ public T retryingConsumeWithLock(ChannelMapper mapper, ConnectionManagerC } catch (IOException e) { var reason = tempChannel.getCloseReason(); isChannelClosed = reason != null; + + // We should not retry in this case because we never will be able to connect to the queue if we + // receive this error. This error happens if we try to subscribe to an exclusive queue that was + // created by another connection. if (isChannelClosed && reason.getMessage().contains("reply-text=RESOURCE_LOCKED")) { throw e; } @@ -793,21 +800,21 @@ public T retryingConsumeWithLock(ChannelMapper mapper, ConnectionManagerC } } - public Optional getCallbacksForRecovery(Channel channelToRecover) { + public @Nullable SubscriptionCallbacks getCallbacksForRecovery(Channel channelToRecover) { lock.lock(); - SubscriptionCallbacks callbacks = null; + try { if (channel != channelToRecover) { - LOGGER.error("Channel already recovered"); + throw new IllegalStateException("Channel already recovered"); } else if (subscriptionCallbacks != null && isSubscribed) { isSubscribed = false; - callbacks = subscriptionCallbacks; + return subscriptionCallbacks; + } else { + return null; } } finally { lock.unlock(); } - - return Optional.ofNullable(callbacks); } public void unsubscribeWithLock(String tag, CancelAction action) throws IOException { diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/ContainerConstants.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/ContainerConstants.kt index 0ab0fc502..c59aa01d2 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/ContainerConstants.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/ContainerConstants.kt @@ -20,7 +20,7 @@ import org.testcontainers.utility.DockerImageName import java.time.Duration object ContainerConstants { - @JvmField val RABBITMQ_IMAGE_NAME: DockerImageName = DockerImageName.parse("rabbitmq:3.12.7-management-alpine") + @JvmField val RABBITMQ_IMAGE_NAME: DockerImageName = DockerImageName.parse("rabbitmq:3.12.8-management-alpine") const val ROUTING_KEY = "routingKey" const val QUEUE_NAME = "queue" const val EXCHANGE = "test-exchange" From 59498097d5a2e9d4ad1078c61463184f1757c008 Mon Sep 17 00:00:00 2001 From: Oleg Smelov Date: Mon, 20 Nov 2023 11:27:30 +0400 Subject: [PATCH 48/51] locking --- .../connection/ConnectionManager.java | 196 ++++++++++-------- 1 file changed, 106 insertions(+), 90 deletions(-) diff --git a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java index bfbd49459..e209c4b4c 100644 --- a/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java +++ b/src/main/java/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/ConnectionManager.java @@ -196,22 +196,22 @@ private void turnOffReadiness(Throwable exception) { factory.setConnectionRecoveryTriggeringCondition(shutdownSignal -> !connectionIsClosed.get()); factory.setRecoveryDelayHandler(recoveryAttempts -> { - int minTime = connectionManagerConfiguration.getMinConnectionRecoveryTimeout(); - int maxTime = connectionManagerConfiguration.getMaxConnectionRecoveryTimeout(); - int maxRecoveryAttempts = connectionManagerConfiguration.getMaxRecoveryAttempts(); - int deviationPercent = connectionManagerConfiguration.getRetryTimeDeviationPercent(); - - LOGGER.debug("Try to recovery connection to RabbitMQ. Count tries = {}", recoveryAttempts); - int recoveryDelay = RetryingDelay.getRecoveryDelay(recoveryAttempts, minTime, maxTime, maxRecoveryAttempts, deviationPercent); - if (recoveryAttempts >= maxRecoveryAttempts && metrics.getLivenessMonitor().isEnabled()) { - LOGGER.info("Set RabbitMQ liveness to false. Can't recover connection"); - metrics.getLivenessMonitor().disable(); - } + int minTime = connectionManagerConfiguration.getMinConnectionRecoveryTimeout(); + int maxTime = connectionManagerConfiguration.getMaxConnectionRecoveryTimeout(); + int maxRecoveryAttempts = connectionManagerConfiguration.getMaxRecoveryAttempts(); + int deviationPercent = connectionManagerConfiguration.getRetryTimeDeviationPercent(); + + LOGGER.debug("Try to recovery connection to RabbitMQ. Count tries = {}", recoveryAttempts); + int recoveryDelay = RetryingDelay.getRecoveryDelay(recoveryAttempts, minTime, maxTime, maxRecoveryAttempts, deviationPercent); + if (recoveryAttempts >= maxRecoveryAttempts && metrics.getLivenessMonitor().isEnabled()) { + LOGGER.info("Set RabbitMQ liveness to false. Can't recover connection"); + metrics.getLivenessMonitor().disable(); + } + + LOGGER.info("Recovery delay for '{}' try = {}", recoveryAttempts, recoveryDelay); + return recoveryDelay; + }); - LOGGER.info("Recovery delay for '{}' try = {}", recoveryAttempts, recoveryDelay); - return recoveryDelay; - } - ); sharedExecutor = Executors.newFixedThreadPool(configuration.getWorkingThreads(), new ThreadFactoryBuilder() .setNameFormat("rabbitmq-shared-pool-%d") .build()); @@ -256,7 +256,7 @@ private void addShutdownListenerToChannel(Channel channel, Boolean withRecovery) var pinIdToChannelHolder = getChannelHolderByChannel(channel); if (pinIdToChannelHolder != null && recoverableErrors.stream().anyMatch(errorString::contains)) { var holder = pinIdToChannelHolder.getValue(); - if (holder.isSubscribed()) { + if (holder.isChannelSubscribed(channel)) { var pinId = pinIdToChannelHolder.getKey(); recoverSubscriptionsOfChannel(pinId, channel, holder); } @@ -278,14 +278,17 @@ private void addShutdownListenerToChannel(Channel channel, Boolean withRecovery) } private void recoverSubscriptionsOfChannel(@NotNull final PinId pinId, Channel channel, @NotNull final ChannelHolder holder) { - channelChecker.execute(() -> { + channelChecker.execute(() -> holder.withLock(() -> { try { var subscriptionCallbacks = holder.getCallbacksForRecovery(channel); if (subscriptionCallbacks != null) { + LOGGER.info("Changing channel for holder with pin id: " + pinId); - channelsByPin.remove(pinId); - if (channel.isOpen()) channel.abort(); + + var removedHolder = channelsByPin.remove(pinId); + if (removedHolder != holder) throw new IllegalStateException("Channel holder has been replaced"); + basicConsume(pinId.queue, subscriptionCallbacks.deliverCallback, subscriptionCallbacks.cancelCallback); } } catch (InterruptedException e) { @@ -295,7 +298,7 @@ private void recoverSubscriptionsOfChannel(@NotNull final PinId pinId, Channel c // this code executed in executor service and exception thrown here will not be handled anywhere LOGGER.error("Failed to recovery channel's subscriptions", e); } - }); + })); } private void addShutdownListenerToConnection(Connection conn) { @@ -686,9 +689,10 @@ private static class ChannelHolder { private int pending; @GuardedBy("lock") private Future check; - @GuardedBy("lock") + @GuardedBy("lock") // or by `subscribingLock` for `basicConsume` channels private Channel channel; - @GuardedBy("lock") + private final Lock subscribingLock = new ReentrantLock(); + @GuardedBy("subscribingLock") private boolean isSubscribed = false; public ChannelHolder( @@ -738,90 +742,102 @@ public void withLock(boolean waitForRecovery, ChannelConsumer consumer) throws I } public void retryingPublishWithLock(ChannelConsumer consumer, ConnectionManagerConfiguration configuration) throws InterruptedException { - Iterator iterator = configuration.createRetryingDelaySequence().iterator(); - int recoveryDelay; - Channel tempChannel = getChannel(true); - while (true) { - lock.lock(); - try { - consumer.consume(tempChannel); - break; - } catch (IOException | ShutdownSignalException e) { - var currentValue = iterator.next(); - recoveryDelay = currentValue.getDelay(); - LOGGER.warn("Retrying publishing #{}, waiting for {}ms. Reason: {}", currentValue.getTryNumber(), recoveryDelay, e); - - // We should not recover the channel if its connection is closed - // If we do that the channel will be also auto recovered by RabbitMQ client - // during connection recovery, and we will get two new channels instead of one closed. - if (!tempChannel.isOpen() && tempChannel.getConnection().isOpen()) { - tempChannel = recreateChannel(); + lock.lock(); + try { + Iterator iterator = configuration.createRetryingDelaySequence().iterator(); + Channel tempChannel = getChannel(true); + while (true) { + try { + consumer.consume(tempChannel); + break; + } catch (IOException | ShutdownSignalException e) { + var currentValue = iterator.next(); + var recoveryDelay = currentValue.getDelay(); + LOGGER.warn("Retrying publishing #{}, waiting for {}ms. Reason: {}", currentValue.getTryNumber(), recoveryDelay, e); + TimeUnit.MILLISECONDS.sleep(recoveryDelay); + + // We should not recover the channel if its connection is closed + // If we do that the channel will be also auto recovered by RabbitMQ client + // during connection recovery, and we will get two new channels instead of one closed. + if (!tempChannel.isOpen() && tempChannel.getConnection().isOpen()) { + tempChannel = recreateChannel(); + } } - } finally { - lock.unlock(); } - - TimeUnit.MILLISECONDS.sleep(recoveryDelay); + } finally { + lock.unlock(); } } public T retryingConsumeWithLock(ChannelMapper mapper, ConnectionManagerConfiguration configuration) throws InterruptedException, IOException { - Iterator iterator = null; - IOException exception; - Channel tempChannel = null; - boolean isChannelClosed = false; - while (true) { - lock.lock(); - try { - if (tempChannel == null) { - tempChannel = getChannel(); - } else if (isChannelClosed) { - tempChannel = recreateChannel(); - } + lock.lock(); + try { + Iterator iterator = null; + IOException exception; + Channel tempChannel = null; + boolean isChannelClosed = false; + while (true) { + subscribingLock.lock(); + try { + if (tempChannel == null) { + tempChannel = getChannel(); + } else if (isChannelClosed) { + tempChannel = recreateChannel(); + } - var tag = mapper.map(tempChannel); - isSubscribed = true; - return tag; - } catch (IOException e) { - var reason = tempChannel.getCloseReason(); - isChannelClosed = reason != null; - - // We should not retry in this case because we never will be able to connect to the queue if we - // receive this error. This error happens if we try to subscribe to an exclusive queue that was - // created by another connection. - if (isChannelClosed && reason.getMessage().contains("reply-text=RESOURCE_LOCKED")) { - throw e; + var tag = mapper.map(tempChannel); + isSubscribed = true; + return tag; + } catch (IOException e) { + var reason = tempChannel.getCloseReason(); + isChannelClosed = reason != null; + + // We should not retry in this case because we never will be able to connect to the queue if we + // receive this error. This error happens if we try to subscribe to an exclusive queue that was + // created by another connection. + if (isChannelClosed && reason.getMessage().contains("reply-text=RESOURCE_LOCKED")) { + throw e; + } + exception = e; + } finally { + subscribingLock.unlock(); } - exception = e; - } finally { - lock.unlock(); + iterator = handleAndSleep(configuration, iterator, "Retrying consume", exception); } - iterator = handleAndSleep(configuration, iterator, "Retrying consume", exception); + } finally { + lock.unlock(); } } public @Nullable SubscriptionCallbacks getCallbacksForRecovery(Channel channelToRecover) { - lock.lock(); + if (!isSubscribed) { + // if unsubscribe() method was invoked after channel failure + LOGGER.warn("Channel's consumer was unsubscribed."); + return null; + } - try { - if (channel != channelToRecover) { - throw new IllegalStateException("Channel already recovered"); - } else if (subscriptionCallbacks != null && isSubscribed) { - isSubscribed = false; - return subscriptionCallbacks; - } else { - return null; - } - } finally { - lock.unlock(); + if (channel != channelToRecover) { + // this can happens if basicConsume() method was invoked by client code after channel failure + LOGGER.warn("Channel already recreated."); + return null; } + + // recovery should not be called for `basicPublish` channels + if (subscriptionCallbacks == null) throw new IllegalStateException("Channel has no consumer"); + + return subscriptionCallbacks; } public void unsubscribeWithLock(String tag, CancelAction action) throws IOException { lock.lock(); try { - action.execute(channel, tag); - isSubscribed = false; + subscribingLock.lock(); + try { + action.execute(channel, tag); + isSubscribed = false; + } finally { + subscribingLock.unlock(); + } } finally { lock.unlock(); } @@ -897,12 +913,12 @@ public void acquireAndSubmitCheck(Supplier> futureSupplier) { } } - public boolean isSubscribed() { - lock.lock(); + public boolean isChannelSubscribed(Channel channel) { + subscribingLock.lock(); try { - return isSubscribed; + return isSubscribed && channel == this.channel; } finally { - lock.unlock(); + subscribingLock.unlock(); } } From eb2ef351596c5e28cee169b7d47bbb57aaaf7421 Mon Sep 17 00:00:00 2001 From: Oleg Smelov Date: Mon, 4 Mar 2024 20:26:01 +0100 Subject: [PATCH 49/51] deps updated version updated --- build.gradle | 22 +++++++++---------- gradle.properties | 2 +- .../schema/message/ContainerConstants.kt | 4 ++-- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/build.gradle b/build.gradle index 4cc66004f..a89a8034c 100644 --- a/build.gradle +++ b/build.gradle @@ -25,11 +25,11 @@ plugins { id 'signing' id 'org.jetbrains.kotlin.jvm' version "$kotlin_version" id 'org.jetbrains.kotlin.kapt' version "$kotlin_version" - id "org.owasp.dependencycheck" version "8.4.3" + id "org.owasp.dependencycheck" version "9.0.9" id "me.champeau.jmh" version "0.7.2" id "com.gorylenko.gradle-git-properties" version "2.4.1" id 'com.github.jk1.dependency-license-report' version '2.5' - id "de.undercouch.download" version "5.5.0" + id "de.undercouch.download" version "5.6.0" id "com.google.protobuf" version "0.9.4" } @@ -42,7 +42,7 @@ ext { serviceGeneratorVersion = '3.5.1' cradleVersion = '5.1.4-dev' - junitVersion = '5.10.1' + junitVersion = '5.10.2' genBaseDir = file("${buildDir}/generated/source/proto") } @@ -181,7 +181,7 @@ tasks.register('integrationTest', Test) { dependencies { api platform("com.exactpro.th2:bom:4.5.0") - api('com.exactpro.th2:grpc-common:4.3.0-dev') { + api('com.exactpro.th2:grpc-common:4.4.0-dev') { because('protobuf transport is main now, this dependnecy should be moved to grpc, mq protobuf modules after splitting') } api("com.exactpro.th2:cradle-core:$cradleVersion") { @@ -237,7 +237,7 @@ dependencies { implementation "com.fasterxml.jackson.module:jackson-module-kotlin" implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-cbor' - implementation "com.fasterxml.uuid:java-uuid-generator:4.3.0" + implementation "com.fasterxml.uuid:java-uuid-generator:5.0.0" implementation 'org.apache.logging.log4j:log4j-slf4j2-impl' implementation 'org.apache.logging.log4j:log4j-core' @@ -247,13 +247,13 @@ dependencies { implementation 'io.prometheus:simpleclient_httpserver' implementation 'io.prometheus:simpleclient_log4j2' - implementation("com.squareup.okio:okio:3.6.0") { + implementation("com.squareup.okio:okio:3.8.0") { because('fix vulnerability in transitive dependency ') } implementation("com.squareup.okhttp3:okhttp:4.12.0") { because('fix vulnerability in transitive dependency ') } - implementation("io.fabric8:kubernetes-client:6.9.2") { + implementation("io.fabric8:kubernetes-client:6.10.0") { exclude group: 'com.fasterxml.jackson.dataformat', module: 'jackson-dataformat-yaml' } @@ -261,11 +261,11 @@ dependencies { testImplementation 'javax.annotation:javax.annotation-api:1.3.2' testImplementation "org.junit.jupiter:junit-jupiter:$junitVersion" - testImplementation "org.mockito.kotlin:mockito-kotlin:5.1.0" + testImplementation "org.mockito.kotlin:mockito-kotlin:5.2.1" testImplementation 'org.jetbrains.kotlin:kotlin-test-junit5' - testImplementation "org.testcontainers:testcontainers:1.19.2" - testImplementation "org.testcontainers:rabbitmq:1.19.2" - testImplementation("org.junit-pioneer:junit-pioneer:2.1.0") { + testImplementation "org.testcontainers:testcontainers:1.19.6" + testImplementation "org.testcontainers:rabbitmq:1.19.6" + testImplementation("org.junit-pioneer:junit-pioneer:2.2.0") { because("system property tests") } testFixturesImplementation "org.jetbrains.kotlin:kotlin-test-junit5:$kotlin_version" diff --git a/gradle.properties b/gradle.properties index 31c566a7c..5f8be7c82 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,7 +14,7 @@ # limitations under the License. # -release_version=5.8.0 +release_version=5.9.0 description='th2 common library (Java)' vcs_url=https://github.com/th2-net/th2-common-j kapt.include.compile.classpath=false \ No newline at end of file diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/ContainerConstants.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/ContainerConstants.kt index c59aa01d2..807838118 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/ContainerConstants.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/ContainerConstants.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023 Exactpro (Exactpro Systems Limited) + * Copyright 2023-2024 Exactpro (Exactpro Systems Limited) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ import org.testcontainers.utility.DockerImageName import java.time.Duration object ContainerConstants { - @JvmField val RABBITMQ_IMAGE_NAME: DockerImageName = DockerImageName.parse("rabbitmq:3.12.8-management-alpine") + @JvmField val RABBITMQ_IMAGE_NAME: DockerImageName = DockerImageName.parse("rabbitmq:3.13.0-management-alpine") const val ROUTING_KEY = "routingKey" const val QUEUE_NAME = "queue" const val EXCHANGE = "test-exchange" From 945d364b5cabb4bc0fb18fa07a365b5891363459 Mon Sep 17 00:00:00 2001 From: Oleg Smelov Date: Mon, 4 Mar 2024 20:48:12 +0100 Subject: [PATCH 50/51] test fixed --- .../message/impl/rabbitmq/connection/TestConnectionManager.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt index 4fa1263c0..ca32d41b8 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt @@ -681,7 +681,7 @@ class TestConnectionManager { LOGGER.info { "Canceled $it" } } - Thread.sleep(3500) + Thread.sleep(2500) LOGGER.info { "Unsubscribing..." } monitor!!.unsubscribe() } From 4835df2c587fd86a5727725b98c703c85e02e58e Mon Sep 17 00:00:00 2001 From: Oleg Smelov Date: Mon, 4 Mar 2024 20:52:57 +0100 Subject: [PATCH 51/51] test fixed --- .../message/impl/rabbitmq/connection/TestConnectionManager.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt index ca32d41b8..342b58d9a 100644 --- a/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt +++ b/src/test/kotlin/com/exactpro/th2/common/schema/message/impl/rabbitmq/connection/TestConnectionManager.kt @@ -687,7 +687,6 @@ class TestConnectionManager { } for (i in 1..5) { putMessageInQueue(it, queueName) - Thread.sleep(1000) }