diff --git a/r-binder/.gitignore b/r-binder/.gitignore new file mode 100644 index 000000000..0387e4d42 --- /dev/null +++ b/r-binder/.gitignore @@ -0,0 +1,25 @@ +/application.yml +/application.properties +asciidoctor.css +*~ +.#* +*# +target/ +build/ +bin/ +_site/ +.classpath +.project +.settings +.springBeans +.DS_Store +*.sw* +*.iml +*.ipr +*.iws +.idea/* +.factorypath +dump.rdb +.apt_generated +artifacts +.sts4-cache diff --git a/r-binder/.mvn/jvm.config b/r-binder/.mvn/jvm.config new file mode 100644 index 000000000..0e7dabeff --- /dev/null +++ b/r-binder/.mvn/jvm.config @@ -0,0 +1 @@ +-Xmx1024m -XX:CICompilerCount=1 -XX:TieredStopAtLevel=1 -Djava.security.egd=file:/dev/./urandom \ No newline at end of file diff --git a/r-binder/.mvn/maven.config b/r-binder/.mvn/maven.config new file mode 100644 index 000000000..3b8cf46e1 --- /dev/null +++ b/r-binder/.mvn/maven.config @@ -0,0 +1 @@ +-DaltSnapshotDeploymentRepository=repo.spring.io::default::https://repo.spring.io/libs-snapshot-local -P spring diff --git a/r-binder/.mvn/wrapper/MavenWrapperDownloader.java b/r-binder/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 000000000..b901097f2 --- /dev/null +++ b/r-binder/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,117 @@ +/* + * Copyright 2007-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + private static final String WRAPPER_VERSION = "0.5.6"; + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if(mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if(mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if(!outputFile.getParentFile().exists()) { + if(!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + String username = System.getenv("MVNW_USERNAME"); + char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/r-binder/.mvn/wrapper/maven-wrapper.jar b/r-binder/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 000000000..2cc7d4a55 Binary files /dev/null and b/r-binder/.mvn/wrapper/maven-wrapper.jar differ diff --git a/r-binder/.mvn/wrapper/maven-wrapper.properties b/r-binder/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 000000000..642d572ce --- /dev/null +++ b/r-binder/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/r-binder/.settings.xml b/r-binder/.settings.xml new file mode 100644 index 000000000..3da1ea205 --- /dev/null +++ b/r-binder/.settings.xml @@ -0,0 +1,66 @@ + + + + + repo.spring.io + ${env.CI_DEPLOY_USERNAME} + ${env.CI_DEPLOY_PASSWORD} + + + + + + spring + true + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/libs-snapshot-local + + true + + + + spring-milestones + Spring Milestones + https://repo.spring.io/libs-milestone-local + + false + + + + spring-releases + Spring Releases + https://repo.spring.io/release + + false + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/libs-snapshot-local + + true + + + + spring-milestones + Spring Milestones + https://repo.spring.io/libs-milestone-local + + false + + + + + + diff --git a/r-binder/LICENSE b/r-binder/LICENSE new file mode 100644 index 000000000..9b259bdfc --- /dev/null +++ b/r-binder/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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 + + https://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. diff --git a/r-binder/README.adoc b/r-binder/README.adoc new file mode 100644 index 000000000..8bf85c9d5 --- /dev/null +++ b/r-binder/README.adoc @@ -0,0 +1,1301 @@ +//// +DO NOT EDIT THIS FILE. IT WAS GENERATED. +Manual changes to this file will be lost when it is generated again. +Edit the files in the src/main/asciidoc/ directory instead. +//// + + +:jdkversion: 1.8 +:github-tag: master +:github-repo: spring-cloud/spring-cloud-stream-binder-rabbit + +:github-raw: https://raw.githubusercontent.com/{github-repo}/{github-tag} +:github-code: https://github.com/{github-repo}/tree/{github-tag} + +image::https://circleci.com/gh/spring-cloud/spring-cloud-stream-binder-rabbit.svg?style=svg["CircleCI", link="https://circleci.com/gh/spring-cloud/spring-cloud-stream-binder-rabbit"] +image::https://codecov.io/gh/spring-cloud/spring-cloud-stream-binder-rabbit/branch/{github-tag}/graph/badge.svg["codecov", link="https://codecov.io/gh/spring-cloud/spring-cloud-stream-binder-rabbit"] +image::https://badges.gitter.im/spring-cloud/spring-cloud-stream-binder-rabbit.svg[Gitter, link="https://gitter.im/spring-cloud/spring-cloud-stream-binder-rabbit?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge"] + +// ====================================================================================== + +//= Overview +[partintro] +-- +This guide describes the RabbitMQ implementation of the Spring Cloud Stream Binder. +It contains information about its design, usage and configuration options, as well as information on how the Stream Cloud Stream concepts map into RabbitMQ specific constructs. +-- + +== Usage + +To use the RabbitMQ binder, you can add it to your Spring Cloud Stream application, by using the following Maven coordinates: + +[source,xml] +---- + + org.springframework.cloud + spring-cloud-stream-binder-rabbit + +---- + +Alternatively, you can use the Spring Cloud Stream RabbitMQ Starter, as follows: + +[source,xml] +---- + + org.springframework.cloud + spring-cloud-starter-stream-rabbit + +---- + +== RabbitMQ Binder Overview + +The following simplified diagram shows how the RabbitMQ binder operates: + +.RabbitMQ Binder +image::{github-raw}/docs/src/main/asciidoc/images/rabbit-binder.png[width=300,scaledwidth="50%"] + +By default, the RabbitMQ Binder implementation maps each destination to a `TopicExchange`. +For each consumer group, a `Queue` is bound to that `TopicExchange`. +Each consumer instance has a corresponding RabbitMQ `Consumer` instance for its group's `Queue`. +For partitioned producers and consumers, the queues are suffixed with the partition index and use the partition index as the routing key. +For anonymous consumers (those with no `group` property), an auto-delete queue (with a randomized unique name) is used. + +By using the optional `autoBindDlq` option, you can configure the binder to create and configure dead-letter queues (DLQs) (and a dead-letter exchange `DLX`, as well as routing infrastructure). +By default, the dead letter queue has the name of the destination, appended with `.dlq`. +If retry is enabled (`maxAttempts > 1`), failed messages are delivered to the DLQ after retries are exhausted. +If retry is disabled (`maxAttempts = 1`), you should set `requeueRejected` to `false` (the default) so that failed messages are routed to the DLQ, instead of being re-queued. +In addition, `republishToDlq` causes the binder to publish a failed message to the DLQ (instead of rejecting it). +This feature lets additional information (such as the stack trace in the `x-exception-stacktrace` header) be added to the message in headers. +See the <> for information about truncated stack traces. +This option does not need retry enabled. +You can republish a failed message after just one attempt. +Starting with version 1.2, you can configure the delivery mode of republished messages. +See the <>. + +If the stream listener throws an `ImmediateAcknowledgeAmqpException`, the DLQ is bypassed and the message simply discarded. +Starting with version 2.1, this is true regardless of the setting of `republishToDlq`; previously it was only the case when `republishToDlq` was `false`. + +IMPORTANT: Setting `requeueRejected` to `true` (with `republishToDlq=false` ) causes the message to be re-queued and redelivered continually, which is likely not what you want unless the reason for the failure is transient. +In general, you should enable retry within the binder by setting `maxAttempts` to greater than one or by setting `republishToDlq` to `true`. + +Starting with version 3.1.2, if the consumer is marked as `transacted`, publishing to the DLQ will participate in the transaction. +This allows the transaction to roll back if the publishing fails for some reason (for example, if the user is not authorized to publish to the dead letter exchange). +In addition, if the connection factory is configured for publisher confirms or returns, the publication to the DLQ will wait for the confirmation and check for a returned message. +If a negative acknowledgment or returned message is received, the binder will throw an `AmqpRejectAndDontRequeueException`, allowing the broker to take care of publishing to the DLQ as if the `republishToDlq` property is `false`. + +See <> for more information about these properties. + +The framework does not provide any standard mechanism to consume dead-letter messages (or to re-route them back to the primary queue). +Some options are described in <>. + +NOTE: When multiple RabbitMQ binders are used in a Spring Cloud Stream application, it is important to disable 'RabbitAutoConfiguration' to avoid the same configuration from `RabbitAutoConfiguration` being applied to the two binders. +You can exclude the class by using the `@SpringBootApplication` annotation. + +Starting with version 2.0, the `RabbitMessageChannelBinder` sets the `RabbitTemplate.userPublisherConnection` property to `true` so that the non-transactional producers avoid deadlocks on consumers, which can happen if cached connections are blocked because of a https://www.rabbitmq.com/memory.html[memory alarm] on the broker. + +NOTE: Currently, a `multiplex` consumer (a single consumer listening to multiple queues) is only supported for message-driven consumers; polled consumers can only retrieve messages from a single queue. + +== Configuration Options + +This section contains settings specific to the RabbitMQ Binder and bound channels. + +For general binding configuration options and properties, see the https://cloud.spring.io/spring-cloud-static/spring-cloud-stream/current/reference/html/spring-cloud-stream.html#_configuration_options[Spring Cloud Stream core documentation]. + +[[rabbit-binder-properties]] +=== RabbitMQ Binder Properties + +By default, the RabbitMQ binder uses Spring Boot's `ConnectionFactory`. +Conseuqently, it supports all Spring Boot configuration options for RabbitMQ. +(For reference, see the https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#common-application-properties[Spring Boot documentation]). +RabbitMQ configuration options use the `spring.rabbitmq` prefix. + +In addition to Spring Boot options, the RabbitMQ binder supports the following properties: + +spring.cloud.stream.rabbit.binder.adminAddresses:: +A comma-separated list of RabbitMQ management plugin URLs. +Only used when `nodes` contains more than one entry. +Each entry in this list must have a corresponding entry in `spring.rabbitmq.addresses`. +Only needed if you use a RabbitMQ cluster and wish to consume from the node that hosts the queue. +See https://docs.spring.io/spring-amqp/reference/html/#queue-affinity[Queue Affinity and the LocalizedQueueConnectionFactory] for more information. ++ +Default: empty. +spring.cloud.stream.rabbit.binder.nodes:: +A comma-separated list of RabbitMQ node names. +When more than one entry, used to locate the server address where a queue is located. +Each entry in this list must have a corresponding entry in `spring.rabbitmq.addresses`. +Only needed if you use a RabbitMQ cluster and wish to consume from the node that hosts the queue. +See https://docs.spring.io/spring-amqp/reference/html/_reference.html#queue-affinity[Queue Affinity and the LocalizedQueueConnectionFactory] for more information. ++ +Default: empty. +spring.cloud.stream.rabbit.binder.compressionLevel:: +The compression level for compressed bindings. +See `java.util.zip.Deflater`. ++ +Default: `1` (BEST_LEVEL). +spring.cloud.stream.binder.connection-name-prefix:: +A connection name prefix used to name the connection(s) created by this binder. +The name is this prefix followed by `#n`, where `n` increments each time a new connection is opened. ++ +Default: none (Spring AMQP default). + +=== RabbitMQ Consumer Properties + +The following properties are available for Rabbit consumers only and must be prefixed with `spring.cloud.stream.rabbit.bindings..consumer.`. + +However if the same set of properties needs to be applied to most bindings, to +avoid repetition, Spring Cloud Stream supports setting values for all channels, +in the format of `spring.cloud.stream.rabbit.default.=`. + +Also, keep in mind that binding specific property will override its equivalent in the default. + +acknowledgeMode:: +The acknowledge mode. ++ +Default: `AUTO`. +anonymousGroupPrefix:: +When the binding has no `group` property, an anonymous, auto-delete queue is bound to the destination exchange. +The default naming stragegy for such queues results in a queue named `anonymous.`. +Set this property to change the prefix to something other than the default. ++ +Default: `anonymous.`. +autoBindDlq:: +Whether to automatically declare the DLQ and bind it to the binder DLX. ++ +Default: `false`. +bindingRoutingKey:: +The routing key with which to bind the queue to the exchange (if `bindQueue` is `true`). +Can be multiple keys - see `bindingRoutingKeyDelimiter`. +For partitioned destinations, `-` is appended to each key. ++ +Default: `#`. +bindingRoutingKeyDelimiter:: +When this is not null, 'bindingRoutingKey' is considered to be a list of keys delimited by this value; often a comma is used. ++ +Default: `null`. +bindQueue:: +Whether to declare the queue and bind it to the destination exchange. +Set it to `false` if you have set up your own infrastructure and have previously created and bound the queue. ++ +Default: `true`. +consumerTagPrefix:: +Used to create the consumer tag(s); will be appended by `#n` where `n` increments for each consumer created. +Example: `${spring.application.name}-${spring.cloud.stream.bindings.input.group}-${spring.cloud.stream.instance-index}`. ++ +Default: none - the broker will generate random consumer tags. +containerType:: +Select the type of listener container to be used. +See https://docs.spring.io/spring-amqp/reference/html/_reference.html#choose-container[Choosing a Container] in the Spring AMQP documentation for more information. ++ +Default: `simple` +deadLetterQueueName:: +The name of the DLQ ++ +Default: `prefix+destination.dlq` +deadLetterExchange:: +A DLX to assign to the queue. +Relevant only if `autoBindDlq` is `true`. ++ +Default: 'prefix+DLX' +deadLetterExchangeType:: +The type of the DLX to assign to the queue. +Relevant only if `autoBindDlq` is `true`. ++ +Default: 'direct' +deadLetterRoutingKey:: +A dead letter routing key to assign to the queue. +Relevant only if `autoBindDlq` is `true`. ++ +Default: `destination` +declareDlx:: +Whether to declare the dead letter exchange for the destination. +Relevant only if `autoBindDlq` is `true`. +Set to `false` if you have a pre-configured DLX. ++ +Default: `true`. +declareExchange:: +Whether to declare the exchange for the destination. ++ +Default: `true`. +delayedExchange:: +Whether to declare the exchange as a `Delayed Message Exchange`. +Requires the delayed message exchange plugin on the broker. +The `x-delayed-type` argument is set to the `exchangeType`. ++ +Default: `false`. +dlqBindingArguments:: +Arguments applied when binding the dlq to the dead letter exchange; used with `headers` `deadLetterExchangeType` to specify headers to match on. +For example `...dlqBindingArguments.x-match=any`, `...dlqBindingArguments.someHeader=someValue`. ++ +Default: empty +dlqDeadLetterExchange:: +If a DLQ is declared, a DLX to assign to that queue. ++ +Default: `none` +dlqDeadLetterRoutingKey:: +If a DLQ is declared, a dead letter routing key to assign to that queue. ++ +Default: `none` +dlqExpires:: +How long before an unused dead letter queue is deleted (in milliseconds). ++ +Default: `no expiration` +dlqLazy:: +Declare the dead letter queue with the `x-queue-mode=lazy` argument. +See https://www.rabbitmq.com/lazy-queues.html["`Lazy Queues`"]. +Consider using a policy instead of this setting, because using a policy allows changing the setting without deleting the queue. ++ +Default: `false`. +dlqMaxLength:: +Maximum number of messages in the dead letter queue. ++ +Default: `no limit` +dlqMaxLengthBytes:: +Maximum number of total bytes in the dead letter queue from all messages. ++ +Default: `no limit` +dlqMaxPriority:: +Maximum priority of messages in the dead letter queue (0-255). ++ +Default: `none` +dlqOverflowBehavior:: +Action to take when `dlqMaxLength` or `dlqMaxLengthBytes` is exceeded; currently `drop-head` or `reject-publish` but refer to the RabbitMQ documentation. ++ +Default: `none` +dlqQuorum.deliveryLimit:: +When `quorum.enabled=true`, set a delivery limit after which the message is dropped or dead-lettered. ++ +Default: none - broker default will apply. +dlqQuorum.enabled:: +When true, create a quorum dead letter queue instead of a classic queue. ++ +Default: false +dlqQuorum.initialQuorumSize:: +When `quorum.enabled=true`, set the initial quorum size. ++ +Default: none - broker default will apply. +dlqSingleActiveConsumer:: +Set to true to set the `x-single-active-consumer` queue property to true. ++ +Default: `false` +dlqTtl:: +Default time to live to apply to the dead letter queue when declared (in milliseconds). ++ +Default: `no limit` +durableSubscription:: +Whether the subscription should be durable. +Only effective if `group` is also set. ++ +Default: `true`. +exchangeAutoDelete:: +If `declareExchange` is true, whether the exchange should be auto-deleted (that is, removed after the last queue is removed). ++ +Default: `true`. +exchangeDurable:: +If `declareExchange` is true, whether the exchange should be durable (that is, it survives broker restart). ++ +Default: `true`. +exchangeType:: +The exchange type: `direct`, `fanout`, `headers` or `topic` for non-partitioned destinations and `direct`, headers or `topic` for partitioned destinations. ++ +Default: `topic`. +exclusive:: +Whether to create an exclusive consumer. +Concurrency should be 1 when this is `true`. +Often used when strict ordering is required but enabling a hot standby instance to take over after a failure. +See `recoveryInterval`, which controls how often a standby instance attempts to consume. +Consider using `singleActiveConsumer` instead when using RabbitMQ 3.8 or later. ++ +Default: `false`. +expires:: +How long before an unused queue is deleted (in milliseconds). ++ +Default: `no expiration` +failedDeclarationRetryInterval:: +The interval (in milliseconds) between attempts to consume from a queue if it is missing. ++ +Default: 5000 +[[spring-cloud-stream-rabbit-frame-max-headroom]] +frameMaxHeadroom:: +The number of bytes to reserve for other headers when adding the stack trace to a DLQ message header. +All headers must fit within the `frame_max` size configured on the broker. +Stack traces can be large; if the size plus this property exceeds `frame_max` then the stack trace will be truncated. +A WARN log will be written; consider increasing the `frame_max` or reducing the stack trace by catching the exception and throwing one with a smaller stack trace. ++ +Default: 20000 +headerPatterns:: +Patterns for headers to be mapped from inbound messages. ++ +Default: `['*']` (all headers). +lazy:: +Declare the queue with the `x-queue-mode=lazy` argument. +See https://www.rabbitmq.com/lazy-queues.html["`Lazy Queues`"]. +Consider using a policy instead of this setting, because using a policy allows changing the setting without deleting the queue. ++ +Default: `false`. +maxConcurrency:: +The maximum number of consumers. +Not supported when the `containerType` is `direct`. ++ +Default: `1`. +maxLength:: +The maximum number of messages in the queue. ++ +Default: `no limit` +maxLengthBytes:: +The maximum number of total bytes in the queue from all messages. ++ +Default: `no limit` +maxPriority:: +The maximum priority of messages in the queue (0-255). ++ +Default: `none` +missingQueuesFatal:: +When the queue cannot be found, whether to treat the condition as fatal and stop the listener container. +Defaults to `false` so that the container keeps trying to consume from the queue -- for example, when using a cluster and the node hosting a non-HA queue is down. ++ +Default: `false` +overflowBehavior:: +Action to take when `maxLength` or `maxLengthBytes` is exceeded; currently `drop-head` or `reject-publish` but refer to the RabbitMQ documentation. ++ +Default: `none` +prefetch:: +Prefetch count. ++ +Default: `1`. +prefix:: +A prefix to be added to the name of the `destination` and queues. ++ +Default: "". +queueBindingArguments:: +Arguments applied when binding the queue to the exchange; used with `headers` `exchangeType` to specify headers to match on. +For example `...queueBindingArguments.x-match=any`, `...queueBindingArguments.someHeader=someValue`. ++ +Default: empty +queueDeclarationRetries:: +The number of times to retry consuming from a queue if it is missing. +Relevant only when `missingQueuesFatal` is `true`. +Otherwise, the container keeps retrying indefinitely. +Not supported when the `containerType` is `direct`. ++ +Default: `3` +queueNameGroupOnly:: +When true, consume from a queue with a name equal to the `group`. +Otherwise the queue name is `destination.group`. +This is useful, for example, when using Spring Cloud Stream to consume from an existing RabbitMQ queue. ++ +Default: false. +quorum.deliveryLimit:: +When `quorum.enabled=true`, set a delivery limit after which the message is dropped or dead-lettered. ++ +Default: none - broker default will apply. +quorum.enabled:: +When true, create a quorum queue instead of a classic queue. ++ +Default: false +quorum.initialQuorumSize:: +When `quorum.enabled=true`, set the initial quorum size. ++ +Default: none - broker default will apply. +recoveryInterval:: +The interval between connection recovery attempts, in milliseconds. ++ +Default: `5000`. +requeueRejected:: +Whether delivery failures should be re-queued when retry is disabled or `republishToDlq` is `false`. ++ +Default: `false`. +[[spring-cloud-stream-rabbit-republish-delivery-mode]] +republishDeliveryMode:: +When `republishToDlq` is `true`, specifies the delivery mode of the republished message. ++ +Default: `DeliveryMode.PERSISTENT` +republishToDlq:: +By default, messages that fail after retries are exhausted are rejected. +If a dead-letter queue (DLQ) is configured, RabbitMQ routes the failed message (unchanged) to the DLQ. +If set to `true`, the binder republishs failed messages to the DLQ with additional headers, including the exception message and stack trace from the cause of the final failure. +Also see the <>. ++ +Default: `true` +singleActiveConsumer:: +Set to true to set the `x-single-active-consumer` queue property to true. ++ +Default: `false` +transacted:: +Whether to use transacted channels. ++ +Default: `false`. +ttl:: +Default time to live to apply to the queue when declared (in milliseconds). ++ +Default: `no limit` +txSize:: +The number of deliveries between acks. +Not supported when the `containerType` is `direct`. ++ +Default: `1`. + +=== Advanced Listener Container Configuration + +To set listener container properties that are not exposed as binder or binding properties, add a single bean of type `ListenerContainerCustomizer` to the application context. +The binder and binding properties will be set and then the customizer will be called. +The customizer (`configure()` method) is provided with the queue name as well as the consumer group as arguments. + +=== Advanced Queue/Exchange/Binding Configuration + +From time to time, the RabbitMQ team add new features that are enabled by setting some argument when declaring, for example, a queue. +Generally, such features are enabled in the binder by adding appropriate properties, but this may not be immediately available in a current version. +Starting with version 3.0.1, you can now add `DeclarableCustomizer` bean(s) to the application context to modify a `Declarable` (`Queue`, `Exchange` or `Binding`) just before the declaration is performed. +This allows you to add arguments that are not currently directly supported by the binder. + +[[rabbit-receiving-batch]] +=== Receiving Batched Messages + +With the RabbitMQ binder, there are two types of batches handled by consumer bindings: + +==== Batches Created by Producers + +Normally, if a producer binding has `batch-enabled=true` (see <>), or a message is created by a `BatchingRabbitTemplate`, elements of the batch are returned as individual calls to the listener method. +Starting with version 3.0, any such batch can be presented as a `List` to the listener method if `spring.cloud.stream.bindings..consumer.batch-mode` is set to `true`. + +==== Consumer-side Batching + +Starting with version 3.1, the consumer can be configured to assemble multiple inbound messages into a batch which is presented to the application as a `List` of converted payloads. +The following simple application demonstrates how to use this technique: + +==== +[source, properties] +---- +spring.cloud.stream.bindings.input-in-0.group=someGroup + +spring.cloud.stream.bindings.input-in-0.consumer.batch-mode=true + +spring.cloud.stream.rabbit.bindings.input-in-0.consumer.enable-batching=true +spring.cloud.stream.rabbit.bindings.input-in-0.consumer.batch-size=10 +spring.cloud.stream.rabbit.bindings.input-in-0.consumer.receive-timeout=200 +---- +==== + +==== +[source, java] +---- +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + @Bean + Consumer> input() { + return list -> { + System.out.println("Received " + list.size()); + list.forEach(thing -> { + System.out.println(thing); + + // ... + + }); + }; + } + + @Bean + public ApplicationRunner runner(RabbitTemplate template) { + return args -> { + template.convertAndSend("input-in-0.someGroup", "{\"field\":\"value1\"}"); + template.convertAndSend("input-in-0.someGroup", "{\"field\":\"value2\"}"); + }; + } + + public static class Thing { + + private String field; + + public Thing() { + } + + public Thing(String field) { + this.field = field; + } + + public String getField() { + return this.field; + } + + public void setField(String field) { + this.field = field; + } + + @Override + public String toString() { + return "Thing [field=" + this.field + "]"; + } + + } + +} +---- +==== + +==== +[source] +---- +Received 2 +Thing [field=value1] +Thing [field=value2] +---- +==== + +The number of messages in a batch is specified by the `batch-size` and `receive-timeout` properties; if the `receive-timeout` elapses with no new messages, a "short" batch is delivered. + +IMPORTANT: Consumer-side batching is only supported with `container-type=simple` (the default). + +If you wish to examine headers of consumer-side batched messages, you should consume `Message>`; the headers are a `List>` in a header `AmqpInboundChannelAdapter.CONSOLIDATED_HEADERS`, with the headers for each payload element in the corresponding index. +Again, here is a simple example: + +==== +[source, java] +---- +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + @Bean + Consumer>> input() { + return msg -> { + List things = msg.getPayload(); + System.out.println("Received " + things.size()); + @SuppressWarnings("unchecked") + List> headers = + (List>) msg.getHeaders().get(AmqpInboundChannelAdapter.CONSOLIDATED_HEADERS); + for (int i = 0; i < things.size(); i++) { + System.out.println(things.get(i) + " myHeader=" + headers.get(i).get("myHeader")); + + // ... + + } + }; + } + + @Bean + public ApplicationRunner runner(RabbitTemplate template) { + return args -> { + template.convertAndSend("input-in-0.someGroup", "{\"field\":\"value1\"}", msg -> { + msg.getMessageProperties().setHeader("myHeader", "headerValue1"); + return msg; + }); + template.convertAndSend("input-in-0.someGroup", "{\"field\":\"value2\"}", msg -> { + msg.getMessageProperties().setHeader("myHeader", "headerValue2"); + return msg; + }); + }; + } + + public static class Thing { + + private String field; + + public Thing() { + } + + public Thing(String field) { + this.field = field; + } + + public String getfield() { + return this.field; + } + + public void setfield(String field) { + this.field = field; + } + + @Override + public String toString() { + return "Thing [field=" + this.field + "]"; + } + + } + +} +---- +==== + +==== +[source] +---- +Received 2 +Thing [field=value1] myHeader=headerValue1 +Thing [field=value2] myHeader=headerValue2 +---- +==== + +[[rabbit-prod-props]] +=== Rabbit Producer Properties + +The following properties are available for Rabbit producers only and must be prefixed with `spring.cloud.stream.rabbit.bindings..producer.`. + +However if the same set of properties needs to be applied to most bindings, to +avoid repetition, Spring Cloud Stream supports setting values for all channels, +in the format of `spring.cloud.stream.rabbit.default.=`. + +Also, keep in mind that binding specific property will override its equivalent in the default. + + +autoBindDlq:: +Whether to automatically declare the DLQ and bind it to the binder DLX. ++ +Default: `false`. +batchingEnabled:: +Whether to enable message batching by producers. +Messages are batched into one message according to the following properties (described in the next three entries in this list): 'batchSize', `batchBufferLimit`, and `batchTimeout`. +See https://docs.spring.io/spring-amqp//reference/html/_reference.html#template-batching[Batching] for more information. +Also see <>. ++ +Default: `false`. +batchSize:: +The number of messages to buffer when batching is enabled. ++ +Default: `100`. +batchBufferLimit:: +The maximum buffer size when batching is enabled. ++ +Default: `10000`. +batchTimeout:: +The batch timeout when batching is enabled. ++ +Default: `5000`. +bindingRoutingKey:: +The routing key with which to bind the queue to the exchange (if `bindQueue` is `true`). +Can be multiple keys - see `bindingRoutingKeyDelimiter`. +For partitioned destinations, `-n` is appended to each key. +Only applies if `requiredGroups` are provided and then only to those groups. ++ +Default: `#`. +bindingRoutingKeyDelimiter:: +When this is not null, 'bindingRoutingKey' is considered to be a list of keys delimited by this value; often a comma is used. +Only applies if `requiredGroups` are provided and then only to those groups. ++ +Default: `null`. +bindQueue:: +Whether to declare the queue and bind it to the destination exchange. +Set it to `false` if you have set up your own infrastructure and have previously created and bound the queue. +Only applies if `requiredGroups` are provided and then only to those groups. ++ +Default: `true`. +compress:: +Whether data should be compressed when sent. ++ +Default: `false`. +confirmAckChannel:: +When `errorChannelEnabled` is true, a channel to which to send positive delivery acknowledgments (aka publisher confirms). +If the channel does not exist, a `DirectChannel` is registered with this name. +The connection factory must be configured to enable publisher confirms. +Mutually exclusive with `useConfirmHeader`. ++ +Default: `nullChannel` (acks are discarded). +deadLetterQueueName:: +The name of the DLQ +Only applies if `requiredGroups` are provided and then only to those groups. ++ +Default: `prefix+destination.dlq` +deadLetterExchange:: +A DLX to assign to the queue. +Relevant only when `autoBindDlq` is `true`. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: 'prefix+DLX' +deadLetterExchangeType:: +The type of the DLX to assign to the queue. +Relevant only if `autoBindDlq` is `true`. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: 'direct' +deadLetterRoutingKey:: +A dead letter routing key to assign to the queue. +Relevant only when `autoBindDlq` is `true`. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: `destination` +declareDlx:: +Whether to declare the dead letter exchange for the destination. +Relevant only if `autoBindDlq` is `true`. +Set to `false` if you have a pre-configured DLX. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: `true`. +declareExchange:: +Whether to declare the exchange for the destination. ++ +Default: `true`. +delayExpression:: +A SpEL expression to evaluate the delay to apply to the message (`x-delay` header). +It has no effect if the exchange is not a delayed message exchange. ++ +Default: No `x-delay` header is set. +delayedExchange:: +Whether to declare the exchange as a `Delayed Message Exchange`. +Requires the delayed message exchange plugin on the broker. +The `x-delayed-type` argument is set to the `exchangeType`. ++ +Default: `false`. +deliveryMode:: +The delivery mode. ++ +Default: `PERSISTENT`. +dlqBindingArguments:: +Arguments applied when binding the dlq to the dead letter exchange; used with `headers` `deadLetterExchangeType` to specify headers to match on. +For example `...dlqBindingArguments.x-match=any`, `...dlqBindingArguments.someHeader=someValue`. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: empty +dlqDeadLetterExchange:: +When a DLQ is declared, a DLX to assign to that queue. +Applies only if `requiredGroups` are provided and then only to those groups. ++ +Default: `none` +dlqDeadLetterRoutingKey:: +When a DLQ is declared, a dead letter routing key to assign to that queue. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: `none` +dlqExpires:: +How long (in milliseconds) before an unused dead letter queue is deleted. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: `no expiration` +dlqLazy:: +Declare the dead letter queue with the `x-queue-mode=lazy` argument. +See https://www.rabbitmq.com/lazy-queues.html["`Lazy Queues`"]. +Consider using a policy instead of this setting, because using a policy allows changing the setting without deleting the queue. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +dlqMaxLength:: +Maximum number of messages in the dead letter queue. +Applies only if `requiredGroups` are provided and then only to those groups. ++ +Default: `no limit` +dlqMaxLengthBytes:: +Maximum number of total bytes in the dead letter queue from all messages. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: `no limit` +dlqMaxPriority:: +Maximum priority of messages in the dead letter queue (0-255) +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: `none` +dlqQuorum.deliveryLimit:: +When `quorum.enabled=true`, set a delivery limit after which the message is dropped or dead-lettered. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: none - broker default will apply. +dlqQuorum.enabled:: +When true, create a quorum dead letter queue instead of a classic queue. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: false +dlqQuorum.initialQuorumSize:: +When `quorum.enabled=true`, set the initial quorum size. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: none - broker default will apply. +dlqSingleActiveConsumer:: +Set to true to set the `x-single-active-consumer` queue property to true. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: `false` +dlqTtl:: +Default time (in milliseconds) to live to apply to the dead letter queue when declared. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: `no limit` +exchangeAutoDelete:: +If `declareExchange` is `true`, whether the exchange should be auto-delete (it is removed after the last queue is removed). ++ +Default: `true`. +exchangeDurable:: +If `declareExchange` is `true`, whether the exchange should be durable (survives broker restart). ++ +Default: `true`. +exchangeType:: +The exchange type: `direct`, `fanout`, `headers` or `topic` for non-partitioned destinations and `direct`, `headers` or `topic` for partitioned destinations. ++ +Default: `topic`. +expires:: +How long (in milliseconds) before an unused queue is deleted. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: `no expiration` +headerPatterns:: +Patterns for headers to be mapped to outbound messages. ++ +Default: `['*']` (all headers). +lazy:: +Declare the queue with the `x-queue-mode=lazy` argument. +See https://www.rabbitmq.com/lazy-queues.html["`Lazy Queues`"]. +Consider using a policy instead of this setting, because using a policy allows changing the setting without deleting the queue. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: `false`. +maxLength:: +Maximum number of messages in the queue. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: `no limit` +maxLengthBytes:: +Maximum number of total bytes in the queue from all messages. +Only applies if `requiredGroups` are provided and then only to those groups. ++ +Default: `no limit` +maxPriority:: +Maximum priority of messages in the queue (0-255). +Only applies if `requiredGroups` are provided and then only to those groups. ++ +Default: `none` +prefix:: +A prefix to be added to the name of the `destination` exchange. ++ +Default: "". +queueBindingArguments:: +Arguments applied when binding the queue to the exchange; used with `headers` `exchangeType` to specify headers to match on. +For example `...queueBindingArguments.x-match=any`, `...queueBindingArguments.someHeader=someValue`. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: empty +queueNameGroupOnly:: +When `true`, consume from a queue with a name equal to the `group`. +Otherwise the queue name is `destination.group`. +This is useful, for example, when using Spring Cloud Stream to consume from an existing RabbitMQ queue. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: false. +quorum.deliveryLimit:: +When `quorum.enabled=true`, set a delivery limit after which the message is dropped or dead-lettered. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: none - broker default will apply. +quorum.enabled:: +When true, create a quorum queue instead of a classic queue. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: false +quorum.initialQuorumSize:: +When `quorum.enabled=true`, set the initial quorum size. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: none - broker default will apply. +routingKeyExpression:: +A SpEL expression to determine the routing key to use when publishing messages. +For a fixed routing key, use a literal expression, such as `routingKeyExpression='my.routingKey'` in a properties file or `routingKeyExpression: '''my.routingKey'''` in a YAML file. ++ +Default: `destination` or `destination-` for partitioned destinations. +singleActiveConsumer:: +Set to true to set the `x-single-active-consumer` queue property to true. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: `false` +transacted:: +Whether to use transacted channels. ++ +Default: `false`. +ttl:: +Default time (in milliseconds) to live to apply to the queue when declared. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: `no limit` +useConfirmHeader:: +See <>. +Mutually exclusive with `confirmAckChannel`. ++ + +NOTE: In the case of RabbitMQ, content type headers can be set by external applications. +Spring Cloud Stream supports them as part of an extended internal protocol used for any type of transport -- including transports, such as Kafka (prior to 0.11), that do not natively support headers. + +[[publisher-confirms]] +=== Publisher Confirms + +There are two mechanisms to get the result of publishing a message; in each case, the connection factory must have `publisherConfirmType` set `ConfirmType.CORRELATED`. +The "legacy" mechanism is to set the `confirmAckChannel` to the bean name of a message channel from which you can retrieve the confirmations asynchronously; negative acks are sent to the error channel (if enabled) - see <>. + +The preferred mechanism, added in version 3.1 is to use a correlation data header and wait for the result via its `Future` property. +This is particularly useful with a batch listener because you can send multiple messages before waiting for the result. +To use this technique, set the `useConfirmHeader` property to true +The following simple application is an example of using this technique: + +==== +[source, properties] +---- +spring.cloud.stream.bindings.input-in-0.group=someGroup +spring.cloud.stream.bindings.input-in-0.consumer.batch-mode=true + +spring.cloud.stream.source=output +spring.cloud.stream.bindings.output-out-0.producer.error-channel-enabled=true + +spring.cloud.stream.rabbit.bindings.output-out-0.producer.useConfirmHeader=true +spring.cloud.stream.rabbit.bindings.input-in-0.consumer.auto-bind-dlq=true +spring.cloud.stream.rabbit.bindings.input-in-0.consumer.batch-size=10 + +spring.rabbitmq.publisher-confirm-type=correlated +spring.rabbitmq.publisher-returns=true +---- +==== + +==== +[source, java] +---- +@SpringBootApplication +public class Application { + + private static final Logger log = LoggerFactory.getLogger(Application.class); + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + @Autowired + private StreamBridge bridge; + + @Bean + Consumer> input() { + return list -> { + List results = new ArrayList<>(); + list.forEach(str -> { + log.info("Received: " + str); + MyCorrelationData corr = new MyCorrelationData(UUID.randomUUID().toString(), str); + results.add(corr); + this.bridge.send("output-out-0", MessageBuilder.withPayload(str.toUpperCase()) + .setHeader(AmqpHeaders.PUBLISH_CONFIRM_CORRELATION, corr) + .build()); + }); + results.forEach(correlation -> { + try { + Confirm confirm = correlation.getFuture().get(10, TimeUnit.SECONDS); + log.info(confirm + " for " + correlation.getPayload()); + if (correlation.getReturnedMessage() != null) { + log.error("Message for " + correlation.getPayload() + " was returned "); + + // try to re-publish, send a DLQ, etc + + } + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + e.printStackTrace(); + } + catch (ExecutionException | TimeoutException e) { + e.printStackTrace(); + } + }); + }; + } + + + @Bean + public ApplicationRunner runner(BatchingRabbitTemplate template) { + return args -> IntStream.range(0, 10).forEach(i -> + template.convertAndSend("input-in-0", "input-in-0.rbgh303", "foo" + i)); + } + + @Bean + public BatchingRabbitTemplate template(CachingConnectionFactory cf, TaskScheduler taskScheduler) { + BatchingStrategy batchingStrategy = new SimpleBatchingStrategy(10, 1000000, 1000); + return new BatchingRabbitTemplate(cf, batchingStrategy, taskScheduler); + } + +} + +class MyCorrelationData extends CorrelationData { + + private final String payload; + + MyCorrelationData(String id, String payload) { + super(id); + this.payload = payload; + } + + public String getPayload() { + return this.payload; + } + +} +---- +==== + +As you can see, we send each message and then await for the publication results. +If the messages can't be routed, then correlation data is populated with the returned message before the future is completed. + +IMPORTANT: The correlation data must be provided with a unique `id` so that the framework can perform the correlation. + +You cannot set both `useConfirmHeader` and `confirmAckChannel` but you can still receive returned messages in the error channel when `useConfirmHeader` is true, but using the correlation header is more convenient. + +== Using Existing Queues/Exchanges + +By default, the binder will automatically provision a topic exchange with the name being derived from the value of the destination binding property ``. +The destination defaults to the binding name, if not provided. +When binding a consumer, a queue will automatically be provisioned with the name `.` (if a `group` binding property is specified), or an anonymous, auto-delete queue when there is no `group`. +The queue will be bound to the exchange with the "match-all" wildcard routing key (`#`) for a non-partitioned binding or `-` for a partitioned binding. +The prefix is an empty `String` by default. +If an output binding is specified with `requiredGroups`, a queue/binding will be provisioned for each group. + +There are a number of rabbit-specific binding properties that allow you to modify this default behavior. + +If you have an existing exchange/queue that you wish to use, you can completely disable automatic provisioning as follows, assuming the exchange is named `myExchange` and the queue is named `myQueue`: + +* `spring.cloud.stream.bindings..destination=myExhange` +* `spring.cloud.stream.bindings..group=myQueue` +* `spring.cloud.stream.rabbit.bindings..consumer.bindQueue=false` +* `spring.cloud.stream.rabbit.bindings..consumer.declareExchange=false` +* `spring.cloud.stream.rabbit.bindings..consumer.queueNameGroupOnly=true` + +If you want the binder to provision the queue/exchange, but you want to do it using something other than the defaults discussed here, use the following properties. +Refer to the property documentation above for more information. + +* `spring.cloud.stream.rabbit.bindings..consumer.bindingRoutingKey=myRoutingKey` +* `spring.cloud.stream.rabbit.bindings..consumer.exchangeType=` + +* `spring.cloud.stream.rabbit.bindings..producer.routingKeyExpression='myRoutingKey'` + +There are similar properties used when declaring a dead-letter exchange/queue, when `autoBindDlq` is `true`. + +== Retry With the RabbitMQ Binder + +When retry is enabled within the binder, the listener container thread is suspended for any back off periods that are configured. +This might be important when strict ordering is required with a single consumer. However, for other use cases, it prevents other messages from being processed on that thread. +An alternative to using binder retry is to set up dead lettering with time to live on the dead-letter queue (DLQ) as well as dead-letter configuration on the DLQ itself. +See "`<>`" for more information about the properties discussed here. +You can use the following example configuration to enable this feature: + +* Set `autoBindDlq` to `true`. +The binder create a DLQ. +Optionally, you can specify a name in `deadLetterQueueName`. +* Set `dlqTtl` to the back off time you want to wait between redeliveries. +* Set the `dlqDeadLetterExchange` to the default exchange. +Expired messages from the DLQ are routed to the original queue, because the default `deadLetterRoutingKey` is the queue name (`destination.group`). +Setting to the default exchange is achieved by setting the property with no value, as shown in the next example. + +To force a message to be dead-lettered, either throw an `AmqpRejectAndDontRequeueException` or set `requeueRejected` to `false` (the default) and throw any exception. + +The loop continue without end, which is fine for transient problems, but you may want to give up after some number of attempts. +Fortunately, RabbitMQ provides the `x-death` header, which lets you determine how many cycles have occurred. + +To acknowledge a message after giving up, throw an `ImmediateAcknowledgeAmqpException`. + +=== Putting it All Together + +The following configuration creates an exchange `myDestination` with queue `myDestination.consumerGroup` bound to a topic exchange with a wildcard routing key `#`: + +[source] +--- +spring.cloud.stream.bindings.input.destination=myDestination +spring.cloud.stream.bindings.input.group=consumerGroup +#disable binder retries +spring.cloud.stream.bindings.input.consumer.max-attempts=1 +#dlx/dlq setup +spring.cloud.stream.rabbit.bindings.input.consumer.auto-bind-dlq=true +spring.cloud.stream.rabbit.bindings.input.consumer.dlq-ttl=5000 +spring.cloud.stream.rabbit.bindings.input.consumer.dlq-dead-letter-exchange= +--- + +This configuration creates a DLQ bound to a direct exchange (`DLX`) with a routing key of `myDestination.consumerGroup`. +When messages are rejected, they are routed to the DLQ. +After 5 seconds, the message expires and is routed to the original queue by using the queue name as the routing key, as shown in the following example: + +.Spring Boot application +[source, java] +---- +@SpringBootApplication +@EnableBinding(Sink.class) +public class XDeathApplication { + + public static void main(String[] args) { + SpringApplication.run(XDeathApplication.class, args); + } + + @StreamListener(Sink.INPUT) + public void listen(String in, @Header(name = "x-death", required = false) Map death) { + if (death != null && death.get("count").equals(3L)) { + // giving up - don't send to DLX + throw new ImmediateAcknowledgeAmqpException("Failed after 4 attempts"); + } + throw new AmqpRejectAndDontRequeueException("failed"); + } + +} +---- + +Notice that the count property in the `x-death` header is a `Long`. + +[[rabbit-error-channels]] +== Error Channels + +Starting with version 1.3, the binder unconditionally sends exceptions to an error channel for each consumer destination and can also be configured to send async producer send failures to an error channel. +See "`<>`" for more information. + +RabbitMQ has two types of send failures: + +* Returned messages, +* Negatively acknowledged https://www.rabbitmq.com/confirms.html[Publisher Confirms]. + +The latter is rare. +According to the RabbitMQ documentation "[A nack] will only be delivered if an internal error occurs in the Erlang process responsible for a queue.". +You can also get a negative acknowledgment if you publish to a bounded queue with `reject-publish` queue overflow behavior. + +As well as enabling producer error channels (as described in "`<>`"), the RabbitMQ binder only sends messages to the channels if the connection factory is appropriately configured, as follows: + +* `ccf.setPublisherConfirms(true);` +* `ccf.setPublisherReturns(true);` + +When using Spring Boot configuration for the connection factory, set the following properties: + +* `spring.rabbitmq.publisher-confirms` +* `spring.rabbitmq.publisher-returns` + +The payload of the `ErrorMessage` for a returned message is a `ReturnedAmqpMessageException` with the following properties: + +* `failedMessage`: The spring-messaging `Message` that failed to be sent. +* `amqpMessage`: The raw spring-amqp `Message`. +* `replyCode`: An integer value indicating the reason for the failure (for example, 312 - No route). +* `replyText`: A text value indicating the reason for the failure (for example, `NO_ROUTE`). +* `exchange`: The exchange to which the message was published. +* `routingKey`: The routing key used when the message was published. + +Also see <> for an alternative mechanism to receive returned messages. + +For negatively acknowledged confirmations, the payload is a `NackedAmqpMessageException` with the following properties: + +* `failedMessage`: The spring-messaging `Message` that failed to be sent. +* `nackReason`: A reason (if available -- you may need to examine the broker logs for more information). + +There is no automatic handling of these exceptions (such as sending to a <>). +You can consume these exceptions with your own Spring Integration flow. + += Appendices +[appendix] +[[building]] +== Building + +:jdkversion: 1.8 + +=== Basic Compile and Test + +Pre-requisites: + +* To compile, JDK {jdkversion} installed. +* To run tests, RabbitMQ server running on `localhost:5672` + + +The build uses the Maven wrapper so you don't have to install a specific +version of Maven. The main build command is + +---- +$ ./mvnw clean install +---- + +NOTE: There are scripts in `./ci-docker-compose` that use https://docs.docker.com/compose//[Docker Compose] to +start/stop a local RabbitMQ server. + +You can also add '-DskipTests' if you like, to avoid running the tests. + +NOTE: You can also install Maven (>=3.3.3) yourself and run the `mvn` command +in place of `./mvnw` in the examples below. If you do that you also +might need to add `-P spring` if your local Maven settings do not +contain repository declarations for spring pre-release artifacts. + +NOTE: Be aware that you might need to increase the amount of memory +available to Maven by setting a `MAVEN_OPTS` environment variable with +a value like `-Xmx512m -XX:MaxPermSize=128m`. We try to cover this in +the `.mvn` configuration, so if you find you have to do it to make a +build succeed, please raise a ticket to get the settings added to +source control. + + +=== Documentation + +There is a "docs" profile that will generate documentation. + +`./mvnw clean package -Pdocs -DskipTests` + +The reference documentation can then be found in `docs/target/contents/reference`. + +=== Working with the code +If you don't have an IDE preference we would recommend that you use +https://www.springsource.com/developer/sts[Spring Tools Suite] or +https://eclipse.org[Eclipse] when working with the code. We use the +https://eclipse.org/m2e/[m2eclipe] eclipse plugin for maven support. Other IDEs and tools +should also work without issue. + +==== Importing into eclipse with m2eclipse +We recommend the https://eclipse.org/m2e/[m2eclipe] eclipse plugin when working with +eclipse. If you don't already have m2eclipse installed it is available from the "eclipse +marketplace". + +Unfortunately m2e does not yet support Maven 3.3, so once the projects +are imported into Eclipse you will also need to tell m2eclipse to use +the `.settings.xml` file for the projects. If you do not do this you +may see many different errors related to the POMs in the +projects. Open your Eclipse preferences, expand the Maven +preferences, and select User Settings. In the User Settings field +click Browse and navigate to the Spring Cloud project you imported +selecting the `.settings.xml` file in that project. Click Apply and +then OK to save the preference changes. + +NOTE: Alternatively you can copy the repository settings from https://github.com/spring-cloud/spring-cloud-build/blob/master/.settings.xml[`.settings.xml`] into your own `~/.m2/settings.xml`. + +==== Importing into eclipse without m2eclipse +If you prefer not to use m2eclipse you can generate eclipse project metadata using the +following command: + +[indent=0] +---- + $ ./mvnw eclipse:eclipse +---- + +The generated eclipse projects can be imported by selecting `import existing projects` +from the `file` menu. +[[contributing]] +== Contributing + +Spring Cloud is released under the non-restrictive Apache 2.0 license, +and follows a very standard Github development process, using Github +tracker for issues and merging pull requests into master. If you want +to contribute even something trivial please do not hesitate, but +follow the guidelines below. + +=== Sign the Contributor License Agreement +Before we accept a non-trivial patch or pull request we will need you to sign the +https://support.springsource.com/spring_committer_signup[contributor's agreement]. +Signing the contributor's agreement does not grant anyone commit rights to the main +repository, but it does mean that we can accept your contributions, and you will get an +author credit if we do. Active contributors might be asked to join the core team, and +given the ability to merge pull requests. + +=== Code Conventions and Housekeeping +None of these is essential for a pull request, but they will all help. They can also be +added after the original pull request but before a merge. + +* Use the Spring Framework code format conventions. If you use Eclipse + you can import formatter settings using the + `eclipse-code-formatter.xml` file from the + https://github.com/spring-cloud/build/tree/master/eclipse-coding-conventions.xml[Spring + Cloud Build] project. If using IntelliJ, you can use the + https://plugins.jetbrains.com/plugin/6546[Eclipse Code Formatter + Plugin] to import the same file. +* Make sure all new `.java` files to have a simple Javadoc class comment with at least an + `@author` tag identifying you, and preferably at least a paragraph on what the class is + for. +* Add the ASF license header comment to all new `.java` files (copy from existing files + in the project) +* Add yourself as an `@author` to the .java files that you modify substantially (more + than cosmetic changes). +* Add some Javadocs and, if you change the namespace, some XSD doc elements. +* A few unit tests would help a lot as well -- someone has to do it. +* If no-one else is using your branch, please rebase it against the current master (or + other target branch in the main project). +* When writing a commit message please follow https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html[these conventions], + if you are fixing an existing issue please add `Fixes gh-XXXX` at the end of the commit + message (where XXXX is the issue number). + +// ====================================================================================== diff --git a/r-binder/README.md b/r-binder/README.md new file mode 100644 index 000000000..ab7f53dc4 --- /dev/null +++ b/r-binder/README.md @@ -0,0 +1,2 @@ +# spring-cloud-stream-binder-rabbit +Spring Cloud Stream Binder implementation for Rabbit diff --git a/r-binder/ci-docker-compose/docker-compose-RABBITMQ-stop.sh b/r-binder/ci-docker-compose/docker-compose-RABBITMQ-stop.sh new file mode 100755 index 000000000..a0f325b26 --- /dev/null +++ b/r-binder/ci-docker-compose/docker-compose-RABBITMQ-stop.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +LOCAL_HOST="${LOCAL_HOST:-localhost}" +RETRIES="${RETRIES:-70}" + +PORT_TO_CHECK=5672 + +WAIT_TIME="${WAIT_TIME:-5}" + +function netcat_port() { + local PASSED_HOST="${2:-$LOCAL_HOST}" + local RABBITMQ_STOPPED=1 + local counter=1 + for i in $( seq 1 "${RETRIES}" ); do + ((counter++)) + if [ "${counter}" -gt 2 ] + then + echo "Rabbitmq is still running. Will try to stop again in [${WAIT_TIME}] seconds" + fi + sleep "${WAIT_TIME}" + nc -v -w 1 ${PASSED_HOST} $1 && continue + echo "Rabbitmq stopped..." + RABBITMQ_STOPPED=0 + break + done + return ${RABBITMQ_STOPPED} +} + +export -f netcat_port + +dockerComposeFile="docker-compose-RABBITMQ.yml" +docker-compose -f $dockerComposeFile kill + +RABBITMQ_STOPPED="no" + +echo "Waiting for RabbitMQ to stop for [$(( WAIT_TIME * RETRIES ))] seconds" +netcat_port $PORT_TO_CHECK && RABBITMQ_STOPPED="yes" + +if [[ "${RABBITMQ_STOPPED}" == "no" ]] ; then + echo "RabbitMQ failed to stop..." + exit 1 +fi \ No newline at end of file diff --git a/r-binder/ci-docker-compose/docker-compose-RABBITMQ.sh b/r-binder/ci-docker-compose/docker-compose-RABBITMQ.sh new file mode 100755 index 000000000..9e6a58d0e --- /dev/null +++ b/r-binder/ci-docker-compose/docker-compose-RABBITMQ.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +LOCAL_HOST="${LOCAL_HOST:-localhost}" +SHOULD_START_RABBIT="${SHOULD_START_RABBIT:-yes}" +PORT_TO_CHECK=5672 + +WAIT_TIME="${WAIT_TIME:-5}" +RETRIES="${RETRIES:-70}" + +function netcat_port() { + local PASSED_HOST="${2:-$LOCAL_HOST}" + local READY_FOR_TESTS=1 + for i in $( seq 1 "${RETRIES}" ); do + sleep "${WAIT_TIME}" + nc -v -w 1 ${PASSED_HOST} $1 && READY_FOR_TESTS=0 && break + echo "Fail #$i/${RETRIES}... will try again in [${WAIT_TIME}] seconds" + done + return ${READY_FOR_TESTS} +} + +export -f netcat_port + +dockerComposeFile="docker-compose-RABBITMQ.yml" +docker-compose -f $dockerComposeFile kill +docker-compose -f $dockerComposeFile build + +if [[ "${SHOULD_START_RABBIT}" == "yes" ]] ; then + echo -e "\n\nBooting up RabbitMQ" + docker-compose -f $dockerComposeFile up -d rabbitmq +fi + +READY_FOR_TESTS="no" + +echo "Waiting for RabbitMQ to boot for [$(( WAIT_TIME * RETRIES ))] seconds" +netcat_port $PORT_TO_CHECK && READY_FOR_TESTS="yes" + +if [[ "${READY_FOR_TESTS}" == "no" ]] ; then + echo "RabbitMQ failed to start..." + exit 1 +fi + diff --git a/r-binder/ci-docker-compose/docker-compose-RABBITMQ.yml b/r-binder/ci-docker-compose/docker-compose-RABBITMQ.yml new file mode 100644 index 000000000..deb4cf8d1 --- /dev/null +++ b/r-binder/ci-docker-compose/docker-compose-RABBITMQ.yml @@ -0,0 +1,5 @@ +rabbitmq: + image: rabbitmq:management + ports: + - 5672:5672 + - 15672:15672 \ No newline at end of file diff --git a/r-binder/docs/pom.xml b/r-binder/docs/pom.xml new file mode 100644 index 000000000..0a3208e05 --- /dev/null +++ b/r-binder/docs/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + spring-cloud-stream-binder-rabbit-docs + + org.springframework.cloud + spring-cloud-stream-binder-rabbit-parent + 3.2.3-SNAPSHOT + + jar + spring-cloud-stream-binder-rabbit-docs + Spring Cloud Stream Rabbit Binder Docs + + spring-cloud-stream-binder-rabbit + ${basedir}/.. + 3.4 + .*stream.* + deploy + + + + ${project.groupId} + spring-cloud-starter-stream-rabbit + ${project.version} + + + + src/main/asciidoc + + + + docs + + + + pl.project13.maven + git-commit-id-plugin + + + maven-dependency-plugin + + + maven-resources-plugin + + + org.codehaus.mojo + exec-maven-plugin + + + org.asciidoctor + asciidoctor-maven-plugin + + + maven-antrun-plugin + + + maven-deploy-plugin + + + + + + diff --git a/r-binder/docs/src/main/asciidoc/.gitignore b/r-binder/docs/src/main/asciidoc/.gitignore new file mode 100644 index 000000000..bbc341117 --- /dev/null +++ b/r-binder/docs/src/main/asciidoc/.gitignore @@ -0,0 +1,2 @@ +*.html +*.css diff --git a/r-binder/docs/src/main/asciidoc/Guardfile b/r-binder/docs/src/main/asciidoc/Guardfile new file mode 100644 index 000000000..bdd4d7298 --- /dev/null +++ b/r-binder/docs/src/main/asciidoc/Guardfile @@ -0,0 +1,20 @@ +require 'asciidoctor' +require 'erb' + +guard 'shell' do + watch(/.*\.adoc$/) {|m| + Asciidoctor.render_file('index.adoc', \ + :in_place => true, \ + :safe => Asciidoctor::SafeMode::UNSAFE, \ + :attributes=> { \ + 'source-highlighter' => 'prettify', \ + 'icons' => 'font', \ + 'linkcss'=> 'true', \ + 'copycss' => 'true', \ + 'doctype' => 'book'}) + } +end + +guard 'livereload' do + watch(%r{^.+\.(css|js|html)$}) +end diff --git a/r-binder/docs/src/main/asciidoc/README.adoc b/r-binder/docs/src/main/asciidoc/README.adoc new file mode 100644 index 000000000..3a6c87de1 --- /dev/null +++ b/r-binder/docs/src/main/asciidoc/README.adoc @@ -0,0 +1,22 @@ +:jdkversion: 1.8 +:github-tag: master +:github-repo: spring-cloud/spring-cloud-stream-binder-rabbit + +:github-raw: https://raw.githubusercontent.com/{github-repo}/{github-tag} +:github-code: https://github.com/{github-repo}/tree/{github-tag} + +image::https://circleci.com/gh/spring-cloud/spring-cloud-stream-binder-rabbit.svg?style=svg["CircleCI", link="https://circleci.com/gh/spring-cloud/spring-cloud-stream-binder-rabbit"] +image::https://codecov.io/gh/spring-cloud/spring-cloud-stream-binder-rabbit/branch/{github-tag}/graph/badge.svg["codecov", link="https://codecov.io/gh/spring-cloud/spring-cloud-stream-binder-rabbit"] +image::https://badges.gitter.im/spring-cloud/spring-cloud-stream-binder-rabbit.svg[Gitter, link="https://gitter.im/spring-cloud/spring-cloud-stream-binder-rabbit?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge"] + +// ====================================================================================== + +//= Overview +include::overview.adoc[] + += Appendices +[appendix] +include::building.adoc[] +include::contributing.adoc[] + +// ====================================================================================== diff --git a/r-binder/docs/src/main/asciidoc/_configprops.adoc b/r-binder/docs/src/main/asciidoc/_configprops.adoc new file mode 100644 index 000000000..88ac8ca3b --- /dev/null +++ b/r-binder/docs/src/main/asciidoc/_configprops.adoc @@ -0,0 +1,38 @@ +|=== +|Name | Default | Description + +|spring.cloud.stream.binders | | Additional per-binder properties (see {@link BinderProperties}) if more then one binder of the same type is used (i.e., connect to multiple instances of RabbitMq). Here you can specify multiple binder configurations, each with different environment settings. For example; spring.cloud.stream.binders.rabbit1.environment. . . , spring.cloud.stream.binders.rabbit2.environment. . . +|spring.cloud.stream.binding-retry-interval | `30` | Retry interval (in seconds) used to schedule binding attempts. Default: 30 sec. +|spring.cloud.stream.bindings | | Additional binding properties (see {@link BinderProperties}) per binding name (e.g., 'input`). For example; This sets the content-type for the 'input' binding of a Sink application: 'spring.cloud.stream.bindings.input.contentType=text/plain' +|spring.cloud.stream.default-binder | | The name of the binder to use by all bindings in the event multiple binders available (e.g., 'rabbit'). +|spring.cloud.stream.dynamic-destination-cache-size | `10` | The maximum size of Least Recently Used (LRU) cache of dynamic destinations. Once this size is reached, new destinations will trigger the removal of old destinations. Default: 10 +|spring.cloud.stream.dynamic-destinations | `[]` | A list of destinations that can be bound dynamically. If set, only listed destinations can be bound. +|spring.cloud.stream.function.batch-mode | `false` | +|spring.cloud.stream.function.bindings | | +|spring.cloud.stream.function.definition | | Definition of functions to bind. If several functions need to be composed into one, use pipes (e.g., 'fooFunc\|barFunc') +|spring.cloud.stream.instance-count | `1` | The number of deployed instances of an application. Default: 1. NOTE: Could also be managed per individual binding "spring.cloud.stream.bindings.foo.consumer.instance-count" where 'foo' is the name of the binding. +|spring.cloud.stream.instance-index | `0` | The instance id of the application: a number from 0 to instanceCount-1. Used for partitioning and with Kafka. NOTE: Could also be managed per individual binding "spring.cloud.stream.bindings.foo.consumer.instance-index" where 'foo' is the name of the binding. +|spring.cloud.stream.instance-index-list | | A list of instance id's from 0 to instanceCount-1. Used for partitioning and with Kafka. NOTE: Could also be managed per individual binding "spring.cloud.stream.bindings.foo.consumer.instance-index-list" where 'foo' is the name of the binding. This setting will override the one set in 'spring.cloud.stream.instance-index' +|spring.cloud.stream.integration.message-handler-not-propagated-headers | | Message header names that will NOT be copied from the inbound message. +|spring.cloud.stream.metrics.export-properties | | List of properties that are going to be appended to each message. This gets populate by onApplicationEvent, once the context refreshes to avoid overhead of doing per message basis. +|spring.cloud.stream.metrics.key | | The name of the metric being emitted. Should be an unique value per application. Defaults to: ${spring.application.name:${vcap.application.name:${spring.config.name:application}}}. +|spring.cloud.stream.metrics.meter-filter | | Pattern to control the 'meters' one wants to capture. By default all 'meters' will be captured. For example, 'spring.integration.*' will only capture metric information for meters whose name starts with 'spring.integration'. +|spring.cloud.stream.metrics.properties | | Application properties that should be added to the metrics payload For example: `spring.application**`. +|spring.cloud.stream.metrics.schedule-interval | `60s` | Interval expressed as Duration for scheduling metrics snapshots publishing. Defaults to 60 seconds +|spring.cloud.stream.override-cloud-connectors | `false` | This property is only applicable when the cloud profile is active and Spring Cloud Connectors are provided with the application. If the property is false (the default), the binder detects a suitable bound service (for example, a RabbitMQ service bound in Cloud Foundry for the RabbitMQ binder) and uses it for creating connections (usually through Spring Cloud Connectors). When set to true, this property instructs binders to completely ignore the bound services and rely on Spring Boot properties (for example, relying on the spring.rabbitmq.* properties provided in the environment for the RabbitMQ binder). The typical usage of this property is to be nested in a customized environment when connecting to multiple systems. +|spring.cloud.stream.pollable-source | `none` | A semi-colon delimited list of binding names of pollable sources. Binding names follow the same naming convention as functions. For example, name '...pollable-source=foobar' will be accessible as 'foobar-iin-0'' binding +|spring.cloud.stream.poller.cron | | Cron expression value for the Cron Trigger. +|spring.cloud.stream.poller.fixed-delay | `1000` | Fixed delay for default poller. +|spring.cloud.stream.poller.initial-delay | `0` | Initial delay for periodic triggers. +|spring.cloud.stream.poller.max-messages-per-poll | `1` | Maximum messages per poll for the default poller. +|spring.cloud.stream.poller.time-unit | | The TimeUnit to apply to delay values. +|spring.cloud.stream.rabbit.binder.admin-addresses | `[]` | Urls for management plugins; only needed for queue affinity. +|spring.cloud.stream.rabbit.binder.admin-adresses | | +|spring.cloud.stream.rabbit.binder.compression-level | `0` | Compression level for compressed bindings; see 'java.util.zip.Deflator'. +|spring.cloud.stream.rabbit.binder.connection-name-prefix | | Prefix for connection names from this binder. +|spring.cloud.stream.rabbit.binder.nodes | `[]` | Cluster member node names; only needed for queue affinity. +|spring.cloud.stream.rabbit.bindings | | +|spring.cloud.stream.sendto.destination | `none` | The name of the header used to determine the name of the output destination +|spring.cloud.stream.source | | A colon delimited string representing the names of the sources based on which source bindings will be created. This is primarily to support cases where source binding may be required without providing a corresponding Supplier. (e.g., for cases where the actual source of data is outside of scope of spring-cloud-stream - HTTP -> Stream) + +|=== \ No newline at end of file diff --git a/r-binder/docs/src/main/asciidoc/appendix.adoc b/r-binder/docs/src/main/asciidoc/appendix.adoc new file mode 100644 index 000000000..70b828fdd --- /dev/null +++ b/r-binder/docs/src/main/asciidoc/appendix.adoc @@ -0,0 +1,5 @@ +[[appendix]] += Appendices + + + diff --git a/r-binder/docs/src/main/asciidoc/building.adoc b/r-binder/docs/src/main/asciidoc/building.adoc new file mode 100644 index 000000000..24e075711 --- /dev/null +++ b/r-binder/docs/src/main/asciidoc/building.adoc @@ -0,0 +1,83 @@ +[[building]] +== Building + +:jdkversion: 1.8 + +=== Basic Compile and Test + +To build the source you will need to install JDK {jdkversion}. + +The build uses the Maven wrapper so you don't have to install a specific +version of Maven. To enable the tests, you should have RabbitMQ server running +on localhost and the default port (5672) +before building. + +The main build command is + +---- +$ ./mvnw clean install +---- + +You can also add '-DskipTests' if you like, to avoid running the tests. + +NOTE: You can also install Maven (>=3.3.3) yourself and run the `mvn` command +in place of `./mvnw` in the examples below. If you do that you also +might need to add `-P spring` if your local Maven settings do not +contain repository declarations for spring pre-release artifacts. + +NOTE: Be aware that you might need to increase the amount of memory +available to Maven by setting a `MAVEN_OPTS` environment variable with +a value like `-Xmx512m -XX:MaxPermSize=128m`. We try to cover this in +the `.mvn` configuration, so if you find you have to do it to make a +build succeed, please raise a ticket to get the settings added to +source control. + + +The projects that require middleware generally include a +`docker-compose.yml`, so consider using +https://compose.docker.io/[Docker Compose] to run the middeware servers +in Docker containers. + +=== Documentation + +There is a "docs" profile that will generate documentation. + +`./mvnw clean package -Pdocs -DskipTests` + +The reference documentation can then be found in `docs/target/contents/reference`. + +=== Working with the code +If you don't have an IDE preference we would recommend that you use +https://www.springsource.com/developer/sts[Spring Tools Suite] or +https://eclipse.org[Eclipse] when working with the code. We use the +https://eclipse.org/m2e/[m2eclipe] eclipse plugin for maven support. Other IDEs and tools +should also work without issue. + +==== Importing into eclipse with m2eclipse +We recommend the https://eclipse.org/m2e/[m2eclipe] eclipse plugin when working with +eclipse. If you don't already have m2eclipse installed it is available from the "eclipse +marketplace". + +Unfortunately m2e does not yet support Maven 3.3, so once the projects +are imported into Eclipse you will also need to tell m2eclipse to use +the `.settings.xml` file for the projects. If you do not do this you +may see many different errors related to the POMs in the +projects. Open your Eclipse preferences, expand the Maven +preferences, and select User Settings. In the User Settings field +click Browse and navigate to the Spring Cloud project you imported +selecting the `.settings.xml` file in that project. Click Apply and +then OK to save the preference changes. + +NOTE: Alternatively you can copy the repository settings from https://github.com/spring-cloud/spring-cloud-build/blob/master/.settings.xml[`.settings.xml`] into your own `~/.m2/settings.xml`. + +==== Importing into eclipse without m2eclipse +If you prefer not to use m2eclipse you can generate eclipse project metadata using the +following command: + +[indent=0] +---- + $ ./mvnw eclipse:eclipse +---- + +The generated eclipse projects can be imported by selecting `import existing projects` +from the `file` menu. diff --git a/r-binder/docs/src/main/asciidoc/contributing.adoc b/r-binder/docs/src/main/asciidoc/contributing.adoc new file mode 100644 index 000000000..bcdf6c007 --- /dev/null +++ b/r-binder/docs/src/main/asciidoc/contributing.adoc @@ -0,0 +1,42 @@ +[[contributing]] +== Contributing + +Spring Cloud is released under the non-restrictive Apache 2.0 license, +and follows a very standard Github development process, using Github +tracker for issues and merging pull requests into master. If you want +to contribute even something trivial please do not hesitate, but +follow the guidelines below. + +=== Sign the Contributor License Agreement +Before we accept a non-trivial patch or pull request we will need you to sign the +https://support.springsource.com/spring_committer_signup[contributor's agreement]. +Signing the contributor's agreement does not grant anyone commit rights to the main +repository, but it does mean that we can accept your contributions, and you will get an +author credit if we do. Active contributors might be asked to join the core team, and +given the ability to merge pull requests. + +=== Code Conventions and Housekeeping +None of these is essential for a pull request, but they will all help. They can also be +added after the original pull request but before a merge. + +* Use the Spring Framework code format conventions. If you use Eclipse + you can import formatter settings using the + `eclipse-code-formatter.xml` file from the + https://github.com/spring-cloud/build/tree/master/eclipse-coding-conventions.xml[Spring + Cloud Build] project. If using IntelliJ, you can use the + https://plugins.jetbrains.com/plugin/6546[Eclipse Code Formatter + Plugin] to import the same file. +* Make sure all new `.java` files to have a simple Javadoc class comment with at least an + `@author` tag identifying you, and preferably at least a paragraph on what the class is + for. +* Add the ASF license header comment to all new `.java` files (copy from existing files + in the project) +* Add yourself as an `@author` to the .java files that you modify substantially (more + than cosmetic changes). +* Add some Javadocs and, if you change the namespace, some XSD doc elements. +* A few unit tests would help a lot as well -- someone has to do it. +* If no-one else is using your branch, please rebase it against the current master (or + other target branch in the main project). +* When writing a commit message please follow https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html[these conventions], + if you are fixing an existing issue please add `Fixes gh-XXXX` at the end of the commit + message (where XXXX is the issue number). diff --git a/r-binder/docs/src/main/asciidoc/dlq.adoc b/r-binder/docs/src/main/asciidoc/dlq.adoc new file mode 100644 index 000000000..64172d513 --- /dev/null +++ b/r-binder/docs/src/main/asciidoc/dlq.adoc @@ -0,0 +1,243 @@ +[[rabbit-dlq-processing]] +== Dead-Letter Queue Processing + +Because you cannot anticipate how users would want to dispose of dead-lettered messages, the framework does not provide any standard mechanism to handle them. +If the reason for the dead-lettering is transient, you may wish to route the messages back to the original queue. +However, if the problem is a permanent issue, that could cause an infinite loop. +The following Spring Boot application shows an example of how to route those messages back to the original queue but moves them to a third "`parking lot`" queue after three attempts. +The second example uses the https://www.rabbitmq.com/blog/2015/04/16/scheduling-messages-with-rabbitmq/[RabbitMQ Delayed Message Exchange] to introduce a delay to the re-queued message. +In this example, the delay increases for each attempt. +These examples use a `@RabbitListener` to receive messages from the DLQ. +You could also use `RabbitTemplate.receive()` in a batch process. + +The examples assume the original destination is `so8400in` and the consumer group is `so8400`. + +=== Non-Partitioned Destinations + +The first two examples are for when the destination is *not* partitioned: + +[source, java] +---- +@SpringBootApplication +public class ReRouteDlqApplication { + + private static final String ORIGINAL_QUEUE = "so8400in.so8400"; + + private static final String DLQ = ORIGINAL_QUEUE + ".dlq"; + + private static final String PARKING_LOT = ORIGINAL_QUEUE + ".parkingLot"; + + private static final String X_RETRIES_HEADER = "x-retries"; + + public static void main(String[] args) throws Exception { + ConfigurableApplicationContext context = SpringApplication.run(ReRouteDlqApplication.class, args); + System.out.println("Press enter to exit"); + System.in.read(); + context.close(); + } + + @Autowired + private RabbitTemplate rabbitTemplate; + + @RabbitListener(queues = DLQ) + public void rePublish(Message failedMessage) { + Integer retriesHeader = (Integer) failedMessage.getMessageProperties().getHeaders().get(X_RETRIES_HEADER); + if (retriesHeader == null) { + retriesHeader = Integer.valueOf(0); + } + if (retriesHeader < 3) { + failedMessage.getMessageProperties().getHeaders().put(X_RETRIES_HEADER, retriesHeader + 1); + this.rabbitTemplate.send(ORIGINAL_QUEUE, failedMessage); + } + else { + this.rabbitTemplate.send(PARKING_LOT, failedMessage); + } + } + + @Bean + public Queue parkingLot() { + return new Queue(PARKING_LOT); + } + +} +---- + +[source, java] +---- +@SpringBootApplication +public class ReRouteDlqApplication { + + private static final String ORIGINAL_QUEUE = "so8400in.so8400"; + + private static final String DLQ = ORIGINAL_QUEUE + ".dlq"; + + private static final String PARKING_LOT = ORIGINAL_QUEUE + ".parkingLot"; + + private static final String X_RETRIES_HEADER = "x-retries"; + + private static final String DELAY_EXCHANGE = "dlqReRouter"; + + public static void main(String[] args) throws Exception { + ConfigurableApplicationContext context = SpringApplication.run(ReRouteDlqApplication.class, args); + System.out.println("Press enter to exit"); + System.in.read(); + context.close(); + } + + @Autowired + private RabbitTemplate rabbitTemplate; + + @RabbitListener(queues = DLQ) + public void rePublish(Message failedMessage) { + Map headers = failedMessage.getMessageProperties().getHeaders(); + Integer retriesHeader = (Integer) headers.get(X_RETRIES_HEADER); + if (retriesHeader == null) { + retriesHeader = Integer.valueOf(0); + } + if (retriesHeader < 3) { + headers.put(X_RETRIES_HEADER, retriesHeader + 1); + headers.put("x-delay", 5000 * retriesHeader); + this.rabbitTemplate.send(DELAY_EXCHANGE, ORIGINAL_QUEUE, failedMessage); + } + else { + this.rabbitTemplate.send(PARKING_LOT, failedMessage); + } + } + + @Bean + public DirectExchange delayExchange() { + DirectExchange exchange = new DirectExchange(DELAY_EXCHANGE); + exchange.setDelayed(true); + return exchange; + } + + @Bean + public Binding bindOriginalToDelay() { + return BindingBuilder.bind(new Queue(ORIGINAL_QUEUE)).to(delayExchange()).with(ORIGINAL_QUEUE); + } + + @Bean + public Queue parkingLot() { + return new Queue(PARKING_LOT); + } + +} +---- + +=== Partitioned Destinations + +With partitioned destinations, there is one DLQ for all partitions. We determine the original queue from the headers. + +==== `republishToDlq=false` + +When `republishToDlq` is `false`, RabbitMQ publishes the message to the DLX/DLQ with an `x-death` header containing information about the original destination, as shown in the following example: + +[source, java] +---- +@SpringBootApplication +public class ReRouteDlqApplication { + + private static final String ORIGINAL_QUEUE = "so8400in.so8400"; + + private static final String DLQ = ORIGINAL_QUEUE + ".dlq"; + + private static final String PARKING_LOT = ORIGINAL_QUEUE + ".parkingLot"; + + private static final String X_DEATH_HEADER = "x-death"; + + private static final String X_RETRIES_HEADER = "x-retries"; + + public static void main(String[] args) throws Exception { + ConfigurableApplicationContext context = SpringApplication.run(ReRouteDlqApplication.class, args); + System.out.println("Press enter to exit"); + System.in.read(); + context.close(); + } + + @Autowired + private RabbitTemplate rabbitTemplate; + + @SuppressWarnings("unchecked") + @RabbitListener(queues = DLQ) + public void rePublish(Message failedMessage) { + Map headers = failedMessage.getMessageProperties().getHeaders(); + Integer retriesHeader = (Integer) headers.get(X_RETRIES_HEADER); + if (retriesHeader == null) { + retriesHeader = Integer.valueOf(0); + } + if (retriesHeader < 3) { + headers.put(X_RETRIES_HEADER, retriesHeader + 1); + List> xDeath = (List>) headers.get(X_DEATH_HEADER); + String exchange = (String) xDeath.get(0).get("exchange"); + List routingKeys = (List) xDeath.get(0).get("routing-keys"); + this.rabbitTemplate.send(exchange, routingKeys.get(0), failedMessage); + } + else { + this.rabbitTemplate.send(PARKING_LOT, failedMessage); + } + } + + @Bean + public Queue parkingLot() { + return new Queue(PARKING_LOT); + } + +} +---- + +==== `republishToDlq=true` + +When `republishToDlq` is `true`, the republishing recoverer adds the original exchange and routing key to headers, as shown in the following example: + +[source, java] +---- +@SpringBootApplication +public class ReRouteDlqApplication { + + private static final String ORIGINAL_QUEUE = "so8400in.so8400"; + + private static final String DLQ = ORIGINAL_QUEUE + ".dlq"; + + private static final String PARKING_LOT = ORIGINAL_QUEUE + ".parkingLot"; + + private static final String X_RETRIES_HEADER = "x-retries"; + + private static final String X_ORIGINAL_EXCHANGE_HEADER = RepublishMessageRecoverer.X_ORIGINAL_EXCHANGE; + + private static final String X_ORIGINAL_ROUTING_KEY_HEADER = RepublishMessageRecoverer.X_ORIGINAL_ROUTING_KEY; + + public static void main(String[] args) throws Exception { + ConfigurableApplicationContext context = SpringApplication.run(ReRouteDlqApplication.class, args); + System.out.println("Press enter to exit"); + System.in.read(); + context.close(); + } + + @Autowired + private RabbitTemplate rabbitTemplate; + + @RabbitListener(queues = DLQ) + public void rePublish(Message failedMessage) { + Map headers = failedMessage.getMessageProperties().getHeaders(); + Integer retriesHeader = (Integer) headers.get(X_RETRIES_HEADER); + if (retriesHeader == null) { + retriesHeader = Integer.valueOf(0); + } + if (retriesHeader < 3) { + headers.put(X_RETRIES_HEADER, retriesHeader + 1); + String exchange = (String) headers.get(X_ORIGINAL_EXCHANGE_HEADER); + String originalRoutingKey = (String) headers.get(X_ORIGINAL_ROUTING_KEY_HEADER); + this.rabbitTemplate.send(exchange, originalRoutingKey, failedMessage); + } + else { + this.rabbitTemplate.send(PARKING_LOT, failedMessage); + } + } + + @Bean + public Queue parkingLot() { + return new Queue(PARKING_LOT); + } + +} +---- diff --git a/r-binder/docs/src/main/asciidoc/ghpages.sh b/r-binder/docs/src/main/asciidoc/ghpages.sh new file mode 100755 index 000000000..375ce6c4a --- /dev/null +++ b/r-binder/docs/src/main/asciidoc/ghpages.sh @@ -0,0 +1,330 @@ +#!/bin/bash -x + +set -e + +# Set default props like MAVEN_PATH, ROOT_FOLDER etc. +function set_default_props() { + # The script should be run from the root folder + ROOT_FOLDER=`pwd` + echo "Current folder is ${ROOT_FOLDER}" + + if [[ ! -e "${ROOT_FOLDER}/.git" ]]; then + echo "You're not in the root folder of the project!" + exit 1 + fi + + # Prop that will let commit the changes + COMMIT_CHANGES="no" + MAVEN_PATH=${MAVEN_PATH:-} + echo "Path to Maven is [${MAVEN_PATH}]" + REPO_NAME=${PWD##*/} + echo "Repo name is [${REPO_NAME}]" + SPRING_CLOUD_STATIC_REPO=${SPRING_CLOUD_STATIC_REPO:-git@github.com:spring-cloud/spring-cloud-static.git} + echo "Spring Cloud Static repo is [${SPRING_CLOUD_STATIC_REPO}" +} + +# Check if gh-pages exists and docs have been built +function check_if_anything_to_sync() { + git remote set-url --push origin `git config remote.origin.url | sed -e 's/^git:/https:/'` + + if ! (git remote set-branches --add origin gh-pages && git fetch -q); then + echo "No gh-pages, so not syncing" + exit 0 + fi + + if ! [ -d docs/target/generated-docs ] && ! [ "${BUILD}" == "yes" ]; then + echo "No gh-pages sources in docs/target/generated-docs, so not syncing" + exit 0 + fi +} + +function retrieve_current_branch() { + # Code getting the name of the current branch. For master we want to publish as we did until now + # https://stackoverflow.com/questions/1593051/how-to-programmatically-determine-the-current-checked-out-git-branch + # If there is a branch already passed will reuse it - otherwise will try to find it + CURRENT_BRANCH=${BRANCH} + if [[ -z "${CURRENT_BRANCH}" ]] ; then + CURRENT_BRANCH=$(git symbolic-ref -q HEAD) + CURRENT_BRANCH=${CURRENT_BRANCH##refs/heads/} + CURRENT_BRANCH=${CURRENT_BRANCH:-HEAD} + fi + echo "Current branch is [${CURRENT_BRANCH}]" + git checkout ${CURRENT_BRANCH} || echo "Failed to check the branch... continuing with the script" +} + +# Switches to the provided value of the release version. We always prefix it with `v` +function switch_to_tag() { + git checkout v${VERSION} +} + +# Build the docs if switch is on +function build_docs_if_applicable() { + if [[ "${BUILD}" == "yes" ]] ; then + ./mvnw clean install -P docs -pl docs -DskipTests + fi +} + +# Get the name of the `docs.main` property +# Get allowed branches - assumes that a `docs` module is available under `docs` profile +function retrieve_doc_properties() { + MAIN_ADOC_VALUE=$("${MAVEN_PATH}"mvn -q \ + -Dexec.executable="echo" \ + -Dexec.args='${docs.main}' \ + --non-recursive \ + org.codehaus.mojo:exec-maven-plugin:1.3.1:exec) + echo "Extracted 'main.adoc' from Maven build [${MAIN_ADOC_VALUE}]" + + + ALLOW_PROPERTY=${ALLOW_PROPERTY:-"docs.allowed.branches"} + ALLOWED_BRANCHES_VALUE=$("${MAVEN_PATH}"mvn -q \ + -Dexec.executable="echo" \ + -Dexec.args="\${${ALLOW_PROPERTY}}" \ + org.codehaus.mojo:exec-maven-plugin:1.3.1:exec \ + -P docs \ + -pl docs) + echo "Extracted '${ALLOW_PROPERTY}' from Maven build [${ALLOWED_BRANCHES_VALUE}]" +} + +# Stash any outstanding changes +function stash_changes() { + git diff-index --quiet HEAD && dirty=$? || (echo "Failed to check if the current repo is dirty. Assuming that it is." && dirty="1") + if [ "$dirty" != "0" ]; then git stash; fi +} + +# Switch to gh-pages branch to sync it with current branch +function add_docs_from_target() { + local DESTINATION_REPO_FOLDER + if [[ -z "${DESTINATION}" && -z "${CLONE}" ]] ; then + DESTINATION_REPO_FOLDER=${ROOT_FOLDER} + elif [[ "${CLONE}" == "yes" ]]; then + mkdir -p ${ROOT_FOLDER}/target + local clonedStatic=${ROOT_FOLDER}/target/spring-cloud-static + if [[ ! -e "${clonedStatic}/.git" ]]; then + echo "Cloning Spring Cloud Static to target" + git clone ${SPRING_CLOUD_STATIC_REPO} ${clonedStatic} && git checkout gh-pages + else + echo "Spring Cloud Static already cloned - will pull changes" + cd ${clonedStatic} && git checkout gh-pages && git pull origin gh-pages + fi + DESTINATION_REPO_FOLDER=${clonedStatic}/${REPO_NAME} + mkdir -p ${DESTINATION_REPO_FOLDER} + else + if [[ ! -e "${DESTINATION}/.git" ]]; then + echo "[${DESTINATION}] is not a git repository" + exit 1 + fi + DESTINATION_REPO_FOLDER=${DESTINATION}/${REPO_NAME} + mkdir -p ${DESTINATION_REPO_FOLDER} + echo "Destination was provided [${DESTINATION}]" + fi + cd ${DESTINATION_REPO_FOLDER} + git checkout gh-pages + git pull origin gh-pages + + # Add git branches + ################################################################### + if [[ -z "${VERSION}" ]] ; then + copy_docs_for_current_version + else + copy_docs_for_provided_version + fi + commit_changes_if_applicable +} + + +# Copies the docs by using the retrieved properties from Maven build +function copy_docs_for_current_version() { + if [[ "${CURRENT_BRANCH}" == "master" ]] ; then + echo -e "Current branch is master - will copy the current docs only to the root folder" + for f in docs/target/generated-docs/*; do + file=${f#docs/target/generated-docs/*} + if ! git ls-files -i -o --exclude-standard --directory | grep -q ^$file$; then + # Not ignored... + cp -rf $f ${ROOT_FOLDER}/ + git add -A ${ROOT_FOLDER}/$file + fi + done + COMMIT_CHANGES="yes" + else + echo -e "Current branch is [${CURRENT_BRANCH}]" + # https://stackoverflow.com/questions/29300806/a-bash-script-to-check-if-a-string-is-present-in-a-comma-separated-list-of-strin + if [[ ",${ALLOWED_BRANCHES_VALUE}," = *",${CURRENT_BRANCH},"* ]] ; then + mkdir -p ${ROOT_FOLDER}/${CURRENT_BRANCH} + echo -e "Branch [${CURRENT_BRANCH}] is allowed! Will copy the current docs to the [${CURRENT_BRANCH}] folder" + for f in docs/target/generated-docs/*; do + file=${f#docs/target/generated-docs/*} + if ! git ls-files -i -o --exclude-standard --directory | grep -q ^$file$; then + # Not ignored... + # We want users to access 2.0.0.BUILD-SNAPSHOT/ instead of 1.0.0.RELEASE/spring-cloud.sleuth.html + if [[ "${file}" == "${MAIN_ADOC_VALUE}.html" ]] ; then + # We don't want to copy the spring-cloud-sleuth.html + # we want it to be converted to index.html + cp -rf $f ${ROOT_FOLDER}/${CURRENT_BRANCH}/index.html + git add -A ${ROOT_FOLDER}/${CURRENT_BRANCH}/index.html + else + cp -rf $f ${ROOT_FOLDER}/${CURRENT_BRANCH} + git add -A ${ROOT_FOLDER}/${CURRENT_BRANCH}/$file + fi + fi + done + COMMIT_CHANGES="yes" + else + echo -e "Branch [${CURRENT_BRANCH}] is not on the allow list! Check out the Maven [${ALLOW_PROPERTY}] property in + [docs] module available under [docs] profile. Won't commit any changes to gh-pages for this branch." + fi + fi +} + +# Copies the docs by using the explicitly provided version +function copy_docs_for_provided_version() { + local FOLDER=${DESTINATION_REPO_FOLDER}/${VERSION} + mkdir -p ${FOLDER} + echo -e "Current tag is [v${VERSION}] Will copy the current docs to the [${FOLDER}] folder" + for f in ${ROOT_FOLDER}/docs/target/generated-docs/*; do + file=${f#${ROOT_FOLDER}/docs/target/generated-docs/*} + copy_docs_for_branch ${file} ${FOLDER} + done + COMMIT_CHANGES="yes" + CURRENT_BRANCH="v${VERSION}" +} + +# Copies the docs from target to the provided destination +# Params: +# $1 - file from target +# $2 - destination to which copy the files +function copy_docs_for_branch() { + local file=$1 + local destination=$2 + if ! git ls-files -i -o --exclude-standard --directory | grep -q ^${file}$; then + # Not ignored... + # We want users to access 2.0.0.BUILD-SNAPSHOT/ instead of 1.0.0.RELEASE/spring-cloud.sleuth.html + if [[ ("${file}" == "${MAIN_ADOC_VALUE}.html") || ("${file}" == "${REPO_NAME}.html") ]] ; then + # We don't want to copy the spring-cloud-sleuth.html + # we want it to be converted to index.html + cp -rf $f ${destination}/index.html + git add -A ${destination}/index.html + else + cp -rf $f ${destination} + git add -A ${destination}/$file + fi + fi +} + +function commit_changes_if_applicable() { + if [[ "${COMMIT_CHANGES}" == "yes" ]] ; then + COMMIT_SUCCESSFUL="no" + git commit -a -m "Sync docs from ${CURRENT_BRANCH} to gh-pages" && COMMIT_SUCCESSFUL="yes" || echo "Failed to commit changes" + + # Uncomment the following push if you want to auto push to + # the gh-pages branch whenever you commit to master locally. + # This is a little extreme. Use with care! + ################################################################### + if [[ "${COMMIT_SUCCESSFUL}" == "yes" ]] ; then + git push origin gh-pages + fi + fi +} + +# Switch back to the previous branch and exit block +function checkout_previous_branch() { + # If -version was provided we need to come back to root project + cd ${ROOT_FOLDER} + git checkout ${CURRENT_BRANCH} || echo "Failed to check the branch... continuing with the script" + if [ "$dirty" != "0" ]; then git stash pop; fi + exit 0 +} + +# Assert if properties have been properly passed +function assert_properties() { +echo "VERSION [${VERSION}], DESTINATION [${DESTINATION}], CLONE [${CLONE}]" +if [[ "${VERSION}" != "" && (-z "${DESTINATION}" && -z "${CLONE}") ]] ; then echo "Version was set but destination / clone was not!"; exit 1;fi +if [[ ("${DESTINATION}" != "" && "${CLONE}" != "") && -z "${VERSION}" ]] ; then echo "Destination / clone was set but version was not!"; exit 1;fi +if [[ "${DESTINATION}" != "" && "${CLONE}" == "yes" ]] ; then echo "Destination and clone was set. Pick one!"; exit 1;fi +} + +# Prints the usage +function print_usage() { +cat </` +- if the destination switch is passed (-d) then the script will check if the provided dir is a git repo and then will + switch to gh-pages of that repo and copy the generated docs to `docs//` + +USAGE: + +You can use the following options: + +-v|--version - the script will apply the whole procedure for a particular library version +-d|--destination - the root of destination folder where the docs should be copied. You have to use the full path. + E.g. point to spring-cloud-static folder. Can't be used with (-c) +-b|--build - will run the standard build process after checking out the branch +-c|--clone - will automatically clone the spring-cloud-static repo instead of providing the destination. + Obviously can't be used with (-d) + +EOF +} + + +# ========================================== +# ____ ____ _____ _____ _____ _______ +# / ____|/ ____| __ \|_ _| __ \__ __| +# | (___ | | | |__) | | | | |__) | | | +# \___ \| | | _ / | | | ___/ | | +# ____) | |____| | \ \ _| |_| | | | +# |_____/ \_____|_| \_\_____|_| |_| +# +# ========================================== + +while [[ $# > 0 ]] +do +key="$1" +case ${key} in + -v|--version) + VERSION="$2" + shift # past argument + ;; + -d|--destination) + DESTINATION="$2" + shift # past argument + ;; + -b|--build) + BUILD="yes" + ;; + -c|--clone) + CLONE="yes" + ;; + -h|--help) + print_usage + exit 0 + ;; + *) + echo "Invalid option: [$1]" + print_usage + exit 1 + ;; +esac +shift # past argument or value +done + +assert_properties +set_default_props +check_if_anything_to_sync +if [[ -z "${VERSION}" ]] ; then + retrieve_current_branch +else + switch_to_tag +fi +build_docs_if_applicable +retrieve_doc_properties +stash_changes +add_docs_from_target +checkout_previous_branch diff --git a/r-binder/docs/src/main/asciidoc/images/part-bindings.png b/r-binder/docs/src/main/asciidoc/images/part-bindings.png new file mode 100644 index 000000000..da8d8612d Binary files /dev/null and b/r-binder/docs/src/main/asciidoc/images/part-bindings.png differ diff --git a/r-binder/docs/src/main/asciidoc/images/part-exchange.png b/r-binder/docs/src/main/asciidoc/images/part-exchange.png new file mode 100644 index 000000000..54eff2690 Binary files /dev/null and b/r-binder/docs/src/main/asciidoc/images/part-exchange.png differ diff --git a/r-binder/docs/src/main/asciidoc/images/part-queues.png b/r-binder/docs/src/main/asciidoc/images/part-queues.png new file mode 100644 index 000000000..0fe7eb700 Binary files /dev/null and b/r-binder/docs/src/main/asciidoc/images/part-queues.png differ diff --git a/r-binder/docs/src/main/asciidoc/images/rabbit-binder.png b/r-binder/docs/src/main/asciidoc/images/rabbit-binder.png new file mode 100755 index 000000000..aabf698b3 Binary files /dev/null and b/r-binder/docs/src/main/asciidoc/images/rabbit-binder.png differ diff --git a/r-binder/docs/src/main/asciidoc/index-docinfo.xml b/r-binder/docs/src/main/asciidoc/index-docinfo.xml new file mode 100644 index 000000000..0db87abde --- /dev/null +++ b/r-binder/docs/src/main/asciidoc/index-docinfo.xml @@ -0,0 +1,14 @@ +Spring Cloud Stream Rabbit Binder +{spring-cloud-stream-binder-Rabbit-version} + + 2013-2016 + Pivotal Software, Inc. + + + + Copies of this document may be made for your own use and for distribution to + others, provided that you do not charge any fee for such copies and further + provided that each copy contains this Copyright Notice, whether distributed in + print or electronically. + + diff --git a/r-binder/docs/src/main/asciidoc/overview.adoc b/r-binder/docs/src/main/asciidoc/overview.adoc new file mode 100644 index 000000000..01d6ad2e2 --- /dev/null +++ b/r-binder/docs/src/main/asciidoc/overview.adoc @@ -0,0 +1,1266 @@ +[partintro] +-- +This guide describes the RabbitMQ implementation of the Spring Cloud Stream Binder. +It contains information about its design, usage and configuration options, as well as information on how the Stream Cloud Stream concepts map into RabbitMQ specific constructs. +-- + +== Usage + +To use the RabbitMQ binder, you can add it to your Spring Cloud Stream application, by using the following Maven coordinates: + +[source,xml] +---- + + org.springframework.cloud + spring-cloud-stream-binder-rabbit + +---- + +Alternatively, you can use the Spring Cloud Stream RabbitMQ Starter, as follows: + +[source,xml] +---- + + org.springframework.cloud + spring-cloud-starter-stream-rabbit + +---- + +== RabbitMQ Binder Overview + +The following simplified diagram shows how the RabbitMQ binder operates: + +.RabbitMQ Binder +image::{github-raw}/docs/src/main/asciidoc/images/rabbit-binder.png[width=300,scaledwidth="50%"] + +By default, the RabbitMQ Binder implementation maps each destination to a `TopicExchange`. +For each consumer group, a `Queue` is bound to that `TopicExchange`. +Each consumer instance has a corresponding RabbitMQ `Consumer` instance for its group's `Queue`. +For partitioned producers and consumers, the queues are suffixed with the partition index and use the partition index as the routing key. +For anonymous consumers (those with no `group` property), an auto-delete queue (with a randomized unique name) is used. + +By using the optional `autoBindDlq` option, you can configure the binder to create and configure dead-letter queues (DLQs) (and a dead-letter exchange `DLX`, as well as routing infrastructure). +By default, the dead letter queue has the name of the destination, appended with `.dlq`. +If retry is enabled (`maxAttempts > 1`), failed messages are delivered to the DLQ after retries are exhausted. +If retry is disabled (`maxAttempts = 1`), you should set `requeueRejected` to `false` (the default) so that failed messages are routed to the DLQ, instead of being re-queued. +In addition, `republishToDlq` causes the binder to publish a failed message to the DLQ (instead of rejecting it). +This feature lets additional information (such as the stack trace in the `x-exception-stacktrace` header) be added to the message in headers. +See the <> for information about truncated stack traces. +This option does not need retry enabled. +You can republish a failed message after just one attempt. +Starting with version 1.2, you can configure the delivery mode of republished messages. +See the <>. + +If the stream listener throws an `ImmediateAcknowledgeAmqpException`, the DLQ is bypassed and the message simply discarded. +Starting with version 2.1, this is true regardless of the setting of `republishToDlq`; previously it was only the case when `republishToDlq` was `false`. + +IMPORTANT: Setting `requeueRejected` to `true` (with `republishToDlq=false` ) causes the message to be re-queued and redelivered continually, which is likely not what you want unless the reason for the failure is transient. +In general, you should enable retry within the binder by setting `maxAttempts` to greater than one or by setting `republishToDlq` to `true`. + +Starting with version 3.1.2, if the consumer is marked as `transacted`, publishing to the DLQ will participate in the transaction. +This allows the transaction to roll back if the publishing fails for some reason (for example, if the user is not authorized to publish to the dead letter exchange). +In addition, if the connection factory is configured for publisher confirms or returns, the publication to the DLQ will wait for the confirmation and check for a returned message. +If a negative acknowledgment or returned message is received, the binder will throw an `AmqpRejectAndDontRequeueException`, allowing the broker to take care of publishing to the DLQ as if the `republishToDlq` property is `false`. + +See <> for more information about these properties. + +The framework does not provide any standard mechanism to consume dead-letter messages (or to re-route them back to the primary queue). +Some options are described in <>. + +NOTE: When multiple RabbitMQ binders are used in a Spring Cloud Stream application, it is important to disable 'RabbitAutoConfiguration' to avoid the same configuration from `RabbitAutoConfiguration` being applied to the two binders. +You can exclude the class by using the `@SpringBootApplication` annotation. + +Starting with version 2.0, the `RabbitMessageChannelBinder` sets the `RabbitTemplate.userPublisherConnection` property to `true` so that the non-transactional producers avoid deadlocks on consumers, which can happen if cached connections are blocked because of a https://www.rabbitmq.com/memory.html[memory alarm] on the broker. + +NOTE: Currently, a `multiplex` consumer (a single consumer listening to multiple queues) is only supported for message-driven consumers; polled consumers can only retrieve messages from a single queue. + +== Configuration Options + +This section contains settings specific to the RabbitMQ Binder and bound channels. + +For general binding configuration options and properties, see the https://cloud.spring.io/spring-cloud-static/spring-cloud-stream/current/reference/html/spring-cloud-stream.html#_configuration_options[Spring Cloud Stream core documentation]. + +[[rabbit-binder-properties]] +=== RabbitMQ Binder Properties + +By default, the RabbitMQ binder uses Spring Boot's `ConnectionFactory`. +Conseuqently, it supports all Spring Boot configuration options for RabbitMQ. +(For reference, see the https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#common-application-properties[Spring Boot documentation]). +RabbitMQ configuration options use the `spring.rabbitmq` prefix. + +In addition to Spring Boot options, the RabbitMQ binder supports the following properties: + +spring.cloud.stream.rabbit.binder.adminAddresses:: +A comma-separated list of RabbitMQ management plugin URLs. +Only used when `nodes` contains more than one entry. +Each entry in this list must have a corresponding entry in `spring.rabbitmq.addresses`. +Only needed if you use a RabbitMQ cluster and wish to consume from the node that hosts the queue. +See https://docs.spring.io/spring-amqp/reference/html/_reference.html#queue-affinity[Queue Affinity and the LocalizedQueueConnectionFactory] for more information. ++ +Default: empty. +spring.cloud.stream.rabbit.binder.nodes:: +A comma-separated list of RabbitMQ node names. +When more than one entry, used to locate the server address where a queue is located. +Each entry in this list must have a corresponding entry in `spring.rabbitmq.addresses`. +Only needed if you use a RabbitMQ cluster and wish to consume from the node that hosts the queue. +See https://docs.spring.io/spring-amqp/reference/html/_reference.html#queue-affinity[Queue Affinity and the LocalizedQueueConnectionFactory] for more information. ++ +Default: empty. +spring.cloud.stream.rabbit.binder.compressionLevel:: +The compression level for compressed bindings. +See `java.util.zip.Deflater`. ++ +Default: `1` (BEST_LEVEL). +spring.cloud.stream.binder.connection-name-prefix:: +A connection name prefix used to name the connection(s) created by this binder. +The name is this prefix followed by `#n`, where `n` increments each time a new connection is opened. ++ +Default: none (Spring AMQP default). + +=== RabbitMQ Consumer Properties + +The following properties are available for Rabbit consumers only and must be prefixed with `spring.cloud.stream.rabbit.bindings..consumer.`. + +However if the same set of properties needs to be applied to most bindings, to +avoid repetition, Spring Cloud Stream supports setting values for all channels, +in the format of `spring.cloud.stream.rabbit.default.=`. + +Also, keep in mind that binding specific property will override its equivalent in the default. + +acknowledgeMode:: +The acknowledge mode. ++ +Default: `AUTO`. +anonymousGroupPrefix:: +When the binding has no `group` property, an anonymous, auto-delete queue is bound to the destination exchange. +The default naming stragegy for such queues results in a queue named `anonymous.`. +Set this property to change the prefix to something other than the default. ++ +Default: `anonymous.`. +autoBindDlq:: +Whether to automatically declare the DLQ and bind it to the binder DLX. ++ +Default: `false`. +bindingRoutingKey:: +The routing key with which to bind the queue to the exchange (if `bindQueue` is `true`). +Can be multiple keys - see `bindingRoutingKeyDelimiter`. +For partitioned destinations, `-` is appended to each key. ++ +Default: `#`. +bindingRoutingKeyDelimiter:: +When this is not null, 'bindingRoutingKey' is considered to be a list of keys delimited by this value; often a comma is used. ++ +Default: `null`. +bindQueue:: +Whether to declare the queue and bind it to the destination exchange. +Set it to `false` if you have set up your own infrastructure and have previously created and bound the queue. ++ +Default: `true`. +consumerTagPrefix:: +Used to create the consumer tag(s); will be appended by `#n` where `n` increments for each consumer created. +Example: `${spring.application.name}-${spring.cloud.stream.bindings.input.group}-${spring.cloud.stream.instance-index}`. ++ +Default: none - the broker will generate random consumer tags. +containerType:: +Select the type of listener container to be used. +See https://docs.spring.io/spring-amqp/reference/html/_reference.html#choose-container[Choosing a Container] in the Spring AMQP documentation for more information. +Also see <>. ++ +Default: `simple` +deadLetterQueueName:: +The name of the DLQ ++ +Default: `prefix+destination.dlq` +deadLetterExchange:: +A DLX to assign to the queue. +Relevant only if `autoBindDlq` is `true`. ++ +Default: 'prefix+DLX' +deadLetterExchangeType:: +The type of the DLX to assign to the queue. +Relevant only if `autoBindDlq` is `true`. ++ +Default: 'direct' +deadLetterRoutingKey:: +A dead letter routing key to assign to the queue. +Relevant only if `autoBindDlq` is `true`. ++ +Default: `destination` +declareDlx:: +Whether to declare the dead letter exchange for the destination. +Relevant only if `autoBindDlq` is `true`. +Set to `false` if you have a pre-configured DLX. ++ +Default: `true`. +declareExchange:: +Whether to declare the exchange for the destination. ++ +Default: `true`. +delayedExchange:: +Whether to declare the exchange as a `Delayed Message Exchange`. +Requires the delayed message exchange plugin on the broker. +The `x-delayed-type` argument is set to the `exchangeType`. ++ +Default: `false`. +dlqBindingArguments:: +Arguments applied when binding the dlq to the dead letter exchange; used with `headers` `deadLetterExchangeType` to specify headers to match on. +For example `...dlqBindingArguments.x-match=any`, `...dlqBindingArguments.someHeader=someValue`. ++ +Default: empty +dlqDeadLetterExchange:: +If a DLQ is declared, a DLX to assign to that queue. ++ +Default: `none` +dlqDeadLetterRoutingKey:: +If a DLQ is declared, a dead letter routing key to assign to that queue. ++ +Default: `none` +dlqExpires:: +How long before an unused dead letter queue is deleted (in milliseconds). ++ +Default: `no expiration` +dlqLazy:: +Declare the dead letter queue with the `x-queue-mode=lazy` argument. +See https://www.rabbitmq.com/lazy-queues.html["`Lazy Queues`"]. +Consider using a policy instead of this setting, because using a policy allows changing the setting without deleting the queue. ++ +Default: `false`. +dlqMaxLength:: +Maximum number of messages in the dead letter queue. ++ +Default: `no limit` +dlqMaxLengthBytes:: +Maximum number of total bytes in the dead letter queue from all messages. ++ +Default: `no limit` +dlqMaxPriority:: +Maximum priority of messages in the dead letter queue (0-255). ++ +Default: `none` +dlqOverflowBehavior:: +Action to take when `dlqMaxLength` or `dlqMaxLengthBytes` is exceeded; currently `drop-head` or `reject-publish` but refer to the RabbitMQ documentation. ++ +Default: `none` +dlqQuorum.deliveryLimit:: +When `quorum.enabled=true`, set a delivery limit after which the message is dropped or dead-lettered. ++ +Default: none - broker default will apply. +dlqQuorum.enabled:: +When true, create a quorum dead letter queue instead of a classic queue. ++ +Default: false +dlqQuorum.initialQuorumSize:: +When `quorum.enabled=true`, set the initial quorum size. ++ +Default: none - broker default will apply. +dlqSingleActiveConsumer:: +Set to true to set the `x-single-active-consumer` queue property to true. ++ +Default: `false` +dlqTtl:: +Default time to live to apply to the dead letter queue when declared (in milliseconds). ++ +Default: `no limit` +durableSubscription:: +Whether the subscription should be durable. +Only effective if `group` is also set. ++ +Default: `true`. +exchangeAutoDelete:: +If `declareExchange` is true, whether the exchange should be auto-deleted (that is, removed after the last queue is removed). ++ +Default: `true`. +exchangeDurable:: +If `declareExchange` is true, whether the exchange should be durable (that is, it survives broker restart). ++ +Default: `true`. +exchangeType:: +The exchange type: `direct`, `fanout`, `headers` or `topic` for non-partitioned destinations and `direct`, headers or `topic` for partitioned destinations. ++ +Default: `topic`. +exclusive:: +Whether to create an exclusive consumer. +Concurrency should be 1 when this is `true`. +Often used when strict ordering is required but enabling a hot standby instance to take over after a failure. +See `recoveryInterval`, which controls how often a standby instance attempts to consume. +Consider using `singleActiveConsumer` instead when using RabbitMQ 3.8 or later. ++ +Default: `false`. +expires:: +How long before an unused queue is deleted (in milliseconds). ++ +Default: `no expiration` +failedDeclarationRetryInterval:: +The interval (in milliseconds) between attempts to consume from a queue if it is missing. ++ +Default: 5000 +[[spring-cloud-stream-rabbit-frame-max-headroom]] +frameMaxHeadroom:: +The number of bytes to reserve for other headers when adding the stack trace to a DLQ message header. +All headers must fit within the `frame_max` size configured on the broker. +Stack traces can be large; if the size plus this property exceeds `frame_max` then the stack trace will be truncated. +A WARN log will be written; consider increasing the `frame_max` or reducing the stack trace by catching the exception and throwing one with a smaller stack trace. ++ +Default: 20000 +headerPatterns:: +Patterns for headers to be mapped from inbound messages. ++ +Default: `['*']` (all headers). +lazy:: +Declare the queue with the `x-queue-mode=lazy` argument. +See https://www.rabbitmq.com/lazy-queues.html["`Lazy Queues`"]. +Consider using a policy instead of this setting, because using a policy allows changing the setting without deleting the queue. ++ +Default: `false`. +maxConcurrency:: +The maximum number of consumers. +Not supported when the `containerType` is `direct`. ++ +Default: `1`. +maxLength:: +The maximum number of messages in the queue. ++ +Default: `no limit` +maxLengthBytes:: +The maximum number of total bytes in the queue from all messages. ++ +Default: `no limit` +maxPriority:: +The maximum priority of messages in the queue (0-255). ++ +Default: `none` +missingQueuesFatal:: +When the queue cannot be found, whether to treat the condition as fatal and stop the listener container. +Defaults to `false` so that the container keeps trying to consume from the queue -- for example, when using a cluster and the node hosting a non-HA queue is down. ++ +Default: `false` +overflowBehavior:: +Action to take when `maxLength` or `maxLengthBytes` is exceeded; currently `drop-head` or `reject-publish` but refer to the RabbitMQ documentation. ++ +Default: `none` +prefetch:: +Prefetch count. ++ +Default: `1`. +prefix:: +A prefix to be added to the name of the `destination` and queues. ++ +Default: "". +queueBindingArguments:: +Arguments applied when binding the queue to the exchange; used with `headers` `exchangeType` to specify headers to match on. +For example `...queueBindingArguments.x-match=any`, `...queueBindingArguments.someHeader=someValue`. ++ +Default: empty +queueDeclarationRetries:: +The number of times to retry consuming from a queue if it is missing. +Relevant only when `missingQueuesFatal` is `true`. +Otherwise, the container keeps retrying indefinitely. +Not supported when the `containerType` is `direct`. ++ +Default: `3` +queueNameGroupOnly:: +When true, consume from a queue with a name equal to the `group`. +Otherwise the queue name is `destination.group`. +This is useful, for example, when using Spring Cloud Stream to consume from an existing RabbitMQ queue. ++ +Default: false. +quorum.deliveryLimit:: +When `quorum.enabled=true`, set a delivery limit after which the message is dropped or dead-lettered. ++ +Default: none - broker default will apply. +quorum.enabled:: +When true, create a quorum queue instead of a classic queue. ++ +Default: false +quorum.initialQuorumSize:: +When `quorum.enabled=true`, set the initial quorum size. ++ +Default: none - broker default will apply. +recoveryInterval:: +The interval between connection recovery attempts, in milliseconds. ++ +Default: `5000`. +requeueRejected:: +Whether delivery failures should be re-queued when retry is disabled or `republishToDlq` is `false`. ++ +Default: `false`. +[[spring-cloud-stream-rabbit-republish-delivery-mode]] +republishDeliveryMode:: +When `republishToDlq` is `true`, specifies the delivery mode of the republished message. ++ +Default: `DeliveryMode.PERSISTENT` +republishToDlq:: +By default, messages that fail after retries are exhausted are rejected. +If a dead-letter queue (DLQ) is configured, RabbitMQ routes the failed message (unchanged) to the DLQ. +If set to `true`, the binder republishs failed messages to the DLQ with additional headers, including the exception message and stack trace from the cause of the final failure. +Also see the <>. ++ +Default: `true` +singleActiveConsumer:: +Set to true to set the `x-single-active-consumer` queue property to true. ++ +Default: `false` +transacted:: +Whether to use transacted channels. ++ +Default: `false`. +ttl:: +Default time to live to apply to the queue when declared (in milliseconds). ++ +Default: `no limit` +txSize:: +The number of deliveries between acks. +Not supported when the `containerType` is `direct`. ++ +Default: `1`. + +[[rabbitmq-stream-consumer]] +=== Initial Consumer Support for the RabbitMQ Stream Plugin + +Basic support for the https://rabbitmq.com/stream.html[RabbitMQ Stream Plugin] is now provided. +To enable this feature, you must add the `spring-rabbit-stream` jar to the class path - it must be the same version as `spring-amqp` and `spring-rabbit`. + +IMPORTANT: The consumer properties described above are not supported when you set the `containerType` property to `stream`; `concurrency` is also not supported at this time. +Only a single stream queue can be consumed by each binding. + +To configure the binder to use `containerType=stream`, you must add an `Environment` `@Bean` and, optionally, a customizer to customize the listener container. + +==== +[source, java] +---- +@Bean +Environment streamEnv() { + return Environment.builder() + .build(); +} + +@Bean +ListenerContainerCustomizer customizer() { + return (cont, dest, group) -> { + StreamListenerContainer container = (StreamListenerContainer) cont; + container.setConsumerCustomizer((name, builder) -> { + builder.offset(OffsetSpecification.first()); + }); + // ... + }; +} +---- +==== + +The `name` argument passed to the customizer is `destination + '.' + group + '.container'`. + +The stream `name()` (for the purpose of offset tracking) is set to the binding `destination + '.' + group`. +It can be changed using a `ConsumerCustomizer` shown above. +If you decide to use manual offset tracking, the `Context` is available as a message header: + +==== +[source, java] +---- +int count; + +@Bean +public Consumer> input() { + return msg -> { + System.out.println(msg); + if (++count % 1000 == 0) { + Context context = msg.getHeaders().get("rabbitmq_streamContext", Context.class); + context.consumer().store(context.offset()); + } + }; +} +---- +==== + +Refer to the https://rabbitmq.github.io/rabbitmq-stream-java-client/stable/htmlsingle/[RabbitMQ Stream Java Client documentation] for information about configuring the environment and consumer builder. + +=== Advanced Listener Container Configuration + +To set listener container properties that are not exposed as binder or binding properties, add a single bean of type `ListenerContainerCustomizer` to the application context. +The binder and binding properties will be set and then the customizer will be called. +The customizer (`configure()` method) is provided with the queue name as well as the consumer group as arguments. + +=== Advanced Queue/Exchange/Binding Configuration + +From time to time, the RabbitMQ team add new features that are enabled by setting some argument when declaring, for example, a queue. +Generally, such features are enabled in the binder by adding appropriate properties, but this may not be immediately available in a current version. +Starting with version 3.0.1, you can now add `DeclarableCustomizer` bean(s) to the application context to modify a `Declarable` (`Queue`, `Exchange` or `Binding`) just before the declaration is performed. +This allows you to add arguments that are not currently directly supported by the binder. + +[[rabbit-receiving-batch]] +=== Receiving Batched Messages + +With the RabbitMQ binder, there are two types of batches handled by consumer bindings: + +==== Batches Created by Producers + +Normally, if a producer binding has `batch-enabled=true` (see <>), or a message is created by a `BatchingRabbitTemplate`, elements of the batch are returned as individual calls to the listener method. +Starting with version 3.0, any such batch can be presented as a `List` to the listener method if `spring.cloud.stream.bindings..consumer.batch-mode` is set to `true`. + +==== Consumer-side Batching + +Starting with version 3.1, the consumer can be configured to assemble multiple inbound messages into a batch which is presented to the application as a `List` of converted payloads. +The following simple application demonstrates how to use this technique: + +==== +[source, properties] +---- +spring.cloud.stream.bindings.input-in-0.group=someGroup + +spring.cloud.stream.bindings.input-in-0.consumer.batch-mode=true + +spring.cloud.stream.rabbit.bindings.input-in-0.consumer.enable-batching=true +spring.cloud.stream.rabbit.bindings.input-in-0.consumer.batch-size=10 +spring.cloud.stream.rabbit.bindings.input-in-0.consumer.receive-timeout=200 +---- +==== + +==== +[source, java] +---- +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + @Bean + Consumer> input() { + return list -> { + System.out.println("Received " + list.size()); + list.forEach(thing -> { + System.out.println(thing); + + // ... + + }); + }; + } + + @Bean + public ApplicationRunner runner(RabbitTemplate template) { + return args -> { + template.convertAndSend("input-in-0.someGroup", "{\"field\":\"value1\"}"); + template.convertAndSend("input-in-0.someGroup", "{\"field\":\"value2\"}"); + }; + } + + public static class Thing { + + private String field; + + public Thing() { + } + + public Thing(String field) { + this.field = field; + } + + public String getField() { + return this.field; + } + + public void setField(String field) { + this.field = field; + } + + @Override + public String toString() { + return "Thing [field=" + this.field + "]"; + } + + } + +} +---- +==== + +==== +[source] +---- +Received 2 +Thing [field=value1] +Thing [field=value2] +---- +==== + +The number of messages in a batch is specified by the `batch-size` and `receive-timeout` properties; if the `receive-timeout` elapses with no new messages, a "short" batch is delivered. + +IMPORTANT: Consumer-side batching is only supported with `container-type=simple` (the default). + +If you wish to examine headers of consumer-side batched messages, you should consume `Message>`; the headers are a `List>` in a header `AmqpInboundChannelAdapter.CONSOLIDATED_HEADERS`, with the headers for each payload element in the corresponding index. +Again, here is a simple example: + +==== +[source, java] +---- +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + @Bean + Consumer>> input() { + return msg -> { + List things = msg.getPayload(); + System.out.println("Received " + things.size()); + @SuppressWarnings("unchecked") + List> headers = + (List>) msg.getHeaders().get(AmqpInboundChannelAdapter.CONSOLIDATED_HEADERS); + for (int i = 0; i < things.size(); i++) { + System.out.println(things.get(i) + " myHeader=" + headers.get(i).get("myHeader")); + + // ... + + } + }; + } + + @Bean + public ApplicationRunner runner(RabbitTemplate template) { + return args -> { + template.convertAndSend("input-in-0.someGroup", "{\"field\":\"value1\"}", msg -> { + msg.getMessageProperties().setHeader("myHeader", "headerValue1"); + return msg; + }); + template.convertAndSend("input-in-0.someGroup", "{\"field\":\"value2\"}", msg -> { + msg.getMessageProperties().setHeader("myHeader", "headerValue2"); + return msg; + }); + }; + } + + public static class Thing { + + private String field; + + public Thing() { + } + + public Thing(String field) { + this.field = field; + } + + public String getfield() { + return this.field; + } + + public void setfield(String field) { + this.field = field; + } + + @Override + public String toString() { + return "Thing [field=" + this.field + "]"; + } + + } + +} +---- +==== + +==== +[source] +---- +Received 2 +Thing [field=value1] myHeader=headerValue1 +Thing [field=value2] myHeader=headerValue2 +---- +==== + +[[rabbit-prod-props]] +=== Rabbit Producer Properties + +The following properties are available for Rabbit producers only and must be prefixed with `spring.cloud.stream.rabbit.bindings..producer.`. + +However if the same set of properties needs to be applied to most bindings, to +avoid repetition, Spring Cloud Stream supports setting values for all channels, +in the format of `spring.cloud.stream.rabbit.default.=`. + +Also, keep in mind that binding specific property will override its equivalent in the default. + + +autoBindDlq:: +Whether to automatically declare the DLQ and bind it to the binder DLX. ++ +Default: `false`. +batchingEnabled:: +Whether to enable message batching by producers. +Messages are batched into one message according to the following properties (described in the next three entries in this list): 'batchSize', `batchBufferLimit`, and `batchTimeout`. +See https://docs.spring.io/spring-amqp//reference/html/_reference.html#template-batching[Batching] for more information. +Also see <>. ++ +Default: `false`. +batchSize:: +The number of messages to buffer when batching is enabled. ++ +Default: `100`. +batchBufferLimit:: +The maximum buffer size when batching is enabled. ++ +Default: `10000`. +batchTimeout:: +The batch timeout when batching is enabled. ++ +Default: `5000`. +bindingRoutingKey:: +The routing key with which to bind the queue to the exchange (if `bindQueue` is `true`). +Can be multiple keys - see `bindingRoutingKeyDelimiter`. +For partitioned destinations, `-n` is appended to each key. +Only applies if `requiredGroups` are provided and then only to those groups. ++ +Default: `#`. +bindingRoutingKeyDelimiter:: +When this is not null, 'bindingRoutingKey' is considered to be a list of keys delimited by this value; often a comma is used. +Only applies if `requiredGroups` are provided and then only to those groups. ++ +Default: `null`. +bindQueue:: +Whether to declare the queue and bind it to the destination exchange. +Set it to `false` if you have set up your own infrastructure and have previously created and bound the queue. +Only applies if `requiredGroups` are provided and then only to those groups. ++ +Default: `true`. +compress:: +Whether data should be compressed when sent. ++ +Default: `false`. +confirmAckChannel:: +When `errorChannelEnabled` is true, a channel to which to send positive delivery acknowledgments (aka publisher confirms). +If the channel does not exist, a `DirectChannel` is registered with this name. +The connection factory must be configured to enable publisher confirms. +Mutually exclusive with `useConfirmHeader`. ++ +Default: `nullChannel` (acks are discarded). +deadLetterQueueName:: +The name of the DLQ +Only applies if `requiredGroups` are provided and then only to those groups. ++ +Default: `prefix+destination.dlq` +deadLetterExchange:: +A DLX to assign to the queue. +Relevant only when `autoBindDlq` is `true`. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: 'prefix+DLX' +deadLetterExchangeType:: +The type of the DLX to assign to the queue. +Relevant only if `autoBindDlq` is `true`. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: 'direct' +deadLetterRoutingKey:: +A dead letter routing key to assign to the queue. +Relevant only when `autoBindDlq` is `true`. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: `destination` +declareDlx:: +Whether to declare the dead letter exchange for the destination. +Relevant only if `autoBindDlq` is `true`. +Set to `false` if you have a pre-configured DLX. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: `true`. +declareExchange:: +Whether to declare the exchange for the destination. ++ +Default: `true`. +delayExpression:: +A SpEL expression to evaluate the delay to apply to the message (`x-delay` header). +It has no effect if the exchange is not a delayed message exchange. ++ +Default: No `x-delay` header is set. +delayedExchange:: +Whether to declare the exchange as a `Delayed Message Exchange`. +Requires the delayed message exchange plugin on the broker. +The `x-delayed-type` argument is set to the `exchangeType`. ++ +Default: `false`. +deliveryMode:: +The delivery mode. ++ +Default: `PERSISTENT`. +dlqBindingArguments:: +Arguments applied when binding the dlq to the dead letter exchange; used with `headers` `deadLetterExchangeType` to specify headers to match on. +For example `...dlqBindingArguments.x-match=any`, `...dlqBindingArguments.someHeader=someValue`. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: empty +dlqDeadLetterExchange:: +When a DLQ is declared, a DLX to assign to that queue. +Applies only if `requiredGroups` are provided and then only to those groups. ++ +Default: `none` +dlqDeadLetterRoutingKey:: +When a DLQ is declared, a dead letter routing key to assign to that queue. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: `none` +dlqExpires:: +How long (in milliseconds) before an unused dead letter queue is deleted. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: `no expiration` +dlqLazy:: +Declare the dead letter queue with the `x-queue-mode=lazy` argument. +See https://www.rabbitmq.com/lazy-queues.html["`Lazy Queues`"]. +Consider using a policy instead of this setting, because using a policy allows changing the setting without deleting the queue. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +dlqMaxLength:: +Maximum number of messages in the dead letter queue. +Applies only if `requiredGroups` are provided and then only to those groups. ++ +Default: `no limit` +dlqMaxLengthBytes:: +Maximum number of total bytes in the dead letter queue from all messages. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: `no limit` +dlqMaxPriority:: +Maximum priority of messages in the dead letter queue (0-255) +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: `none` +dlqQuorum.deliveryLimit:: +When `quorum.enabled=true`, set a delivery limit after which the message is dropped or dead-lettered. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: none - broker default will apply. +dlqQuorum.enabled:: +When true, create a quorum dead letter queue instead of a classic queue. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: false +dlqQuorum.initialQuorumSize:: +When `quorum.enabled=true`, set the initial quorum size. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: none - broker default will apply. +dlqSingleActiveConsumer:: +Set to true to set the `x-single-active-consumer` queue property to true. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: `false` +dlqTtl:: +Default time (in milliseconds) to live to apply to the dead letter queue when declared. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: `no limit` +exchangeAutoDelete:: +If `declareExchange` is `true`, whether the exchange should be auto-delete (it is removed after the last queue is removed). ++ +Default: `true`. +exchangeDurable:: +If `declareExchange` is `true`, whether the exchange should be durable (survives broker restart). ++ +Default: `true`. +exchangeType:: +The exchange type: `direct`, `fanout`, `headers` or `topic` for non-partitioned destinations and `direct`, `headers` or `topic` for partitioned destinations. ++ +Default: `topic`. +expires:: +How long (in milliseconds) before an unused queue is deleted. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: `no expiration` +headerPatterns:: +Patterns for headers to be mapped to outbound messages. ++ +Default: `['*']` (all headers). +lazy:: +Declare the queue with the `x-queue-mode=lazy` argument. +See https://www.rabbitmq.com/lazy-queues.html["`Lazy Queues`"]. +Consider using a policy instead of this setting, because using a policy allows changing the setting without deleting the queue. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: `false`. +maxLength:: +Maximum number of messages in the queue. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: `no limit` +maxLengthBytes:: +Maximum number of total bytes in the queue from all messages. +Only applies if `requiredGroups` are provided and then only to those groups. ++ +Default: `no limit` +maxPriority:: +Maximum priority of messages in the queue (0-255). +Only applies if `requiredGroups` are provided and then only to those groups. ++ +Default: `none` +prefix:: +A prefix to be added to the name of the `destination` exchange. ++ +Default: "". +queueBindingArguments:: +Arguments applied when binding the queue to the exchange; used with `headers` `exchangeType` to specify headers to match on. +For example `...queueBindingArguments.x-match=any`, `...queueBindingArguments.someHeader=someValue`. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: empty +queueNameGroupOnly:: +When `true`, consume from a queue with a name equal to the `group`. +Otherwise the queue name is `destination.group`. +This is useful, for example, when using Spring Cloud Stream to consume from an existing RabbitMQ queue. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: false. +quorum.deliveryLimit:: +When `quorum.enabled=true`, set a delivery limit after which the message is dropped or dead-lettered. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: none - broker default will apply. +quorum.enabled:: +When true, create a quorum queue instead of a classic queue. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: false +quorum.initialQuorumSize:: +When `quorum.enabled=true`, set the initial quorum size. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: none - broker default will apply. +routingKeyExpression:: +A SpEL expression to determine the routing key to use when publishing messages. +For a fixed routing key, use a literal expression, such as `routingKeyExpression='my.routingKey'` in a properties file or `routingKeyExpression: '''my.routingKey'''` in a YAML file. ++ +Default: `destination` or `destination-` for partitioned destinations. +singleActiveConsumer:: +Set to true to set the `x-single-active-consumer` queue property to true. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: `false` +transacted:: +Whether to use transacted channels. ++ +Default: `false`. +ttl:: +Default time (in milliseconds) to live to apply to the queue when declared. +Applies only when `requiredGroups` are provided and then only to those groups. ++ +Default: `no limit` +useConfirmHeader:: +See <>. +Mutually exclusive with `confirmAckChannel`. ++ + +NOTE: In the case of RabbitMQ, content type headers can be set by external applications. +Spring Cloud Stream supports them as part of an extended internal protocol used for any type of transport -- including transports, such as Kafka (prior to 0.11), that do not natively support headers. + +[[publisher-confirms]] +=== Publisher Confirms + +There are two mechanisms to get the result of publishing a message; in each case, the connection factory must have `publisherConfirmType` set `ConfirmType.CORRELATED`. +The "legacy" mechanism is to set the `confirmAckChannel` to the bean name of a message channel from which you can retrieve the confirmations asynchronously; negative acks are sent to the error channel (if enabled) - see <>. + +The preferred mechanism, added in version 3.1 is to use a correlation data header and wait for the result via its `Future` property. +This is particularly useful with a batch listener because you can send multiple messages before waiting for the result. +To use this technique, set the `useConfirmHeader` property to true +The following simple application is an example of using this technique: + +==== +[source, properties] +---- +spring.cloud.stream.bindings.input-in-0.group=someGroup +spring.cloud.stream.bindings.input-in-0.consumer.batch-mode=true + +spring.cloud.stream.source=output +spring.cloud.stream.bindings.output-out-0.producer.error-channel-enabled=true + +spring.cloud.stream.rabbit.bindings.output-out-0.producer.useConfirmHeader=true +spring.cloud.stream.rabbit.bindings.input-in-0.consumer.auto-bind-dlq=true +spring.cloud.stream.rabbit.bindings.input-in-0.consumer.batch-size=10 + +spring.rabbitmq.publisher-confirm-type=correlated +spring.rabbitmq.publisher-returns=true +---- +==== + +==== +[source, java] +---- +@SpringBootApplication +public class Application { + + private static final Logger log = LoggerFactory.getLogger(Application.class); + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + @Autowired + private StreamBridge bridge; + + @Bean + Consumer> input() { + return list -> { + List results = new ArrayList<>(); + list.forEach(str -> { + log.info("Received: " + str); + MyCorrelationData corr = new MyCorrelationData(UUID.randomUUID().toString(), str); + results.add(corr); + this.bridge.send("output-out-0", MessageBuilder.withPayload(str.toUpperCase()) + .setHeader(AmqpHeaders.PUBLISH_CONFIRM_CORRELATION, corr) + .build()); + }); + results.forEach(correlation -> { + try { + Confirm confirm = correlation.getFuture().get(10, TimeUnit.SECONDS); + log.info(confirm + " for " + correlation.getPayload()); + if (correlation.getReturnedMessage() != null) { + log.error("Message for " + correlation.getPayload() + " was returned "); + + // throw some exception to invoke binder retry/error handling + + } + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(e); + } + catch (ExecutionException | TimeoutException e) { + throw new IllegalStateException(e); + } + }); + }; + } + + + @Bean + public ApplicationRunner runner(BatchingRabbitTemplate template) { + return args -> IntStream.range(0, 10).forEach(i -> + template.convertAndSend("input-in-0", "input-in-0.rbgh303", "foo" + i)); + } + + @Bean + public BatchingRabbitTemplate template(CachingConnectionFactory cf, TaskScheduler taskScheduler) { + BatchingStrategy batchingStrategy = new SimpleBatchingStrategy(10, 1000000, 1000); + return new BatchingRabbitTemplate(cf, batchingStrategy, taskScheduler); + } + +} + +class MyCorrelationData extends CorrelationData { + + private final String payload; + + MyCorrelationData(String id, String payload) { + super(id); + this.payload = payload; + } + + public String getPayload() { + return this.payload; + } + +} +---- +==== + +As you can see, we send each message and then await for the publication results. +If the messages can't be routed, then correlation data is populated with the returned message before the future is completed. + +IMPORTANT: The correlation data must be provided with a unique `id` so that the framework can perform the correlation. + +You cannot set both `useConfirmHeader` and `confirmAckChannel` but you can still receive returned messages in the error channel when `useConfirmHeader` is true, but using the correlation header is more convenient. + +== Using Existing Queues/Exchanges + +By default, the binder will automatically provision a topic exchange with the name being derived from the value of the destination binding property ``. +The destination defaults to the binding name, if not provided. +When binding a consumer, a queue will automatically be provisioned with the name `.` (if a `group` binding property is specified), or an anonymous, auto-delete queue when there is no `group`. +The queue will be bound to the exchange with the "match-all" wildcard routing key (`#`) for a non-partitioned binding or `-` for a partitioned binding. +The prefix is an empty `String` by default. +If an output binding is specified with `requiredGroups`, a queue/binding will be provisioned for each group. + +There are a number of rabbit-specific binding properties that allow you to modify this default behavior. + +If you have an existing exchange/queue that you wish to use, you can completely disable automatic provisioning as follows, assuming the exchange is named `myExchange` and the queue is named `myQueue`: + +* `spring.cloud.stream.bindings..destination=myExchange` +* `spring.cloud.stream.bindings..group=myQueue` +* `spring.cloud.stream.rabbit.bindings..consumer.bindQueue=false` +* `spring.cloud.stream.rabbit.bindings..consumer.declareExchange=false` +* `spring.cloud.stream.rabbit.bindings..consumer.queueNameGroupOnly=true` + +If you want the binder to provision the queue/exchange, but you want to do it using something other than the defaults discussed here, use the following properties. +Refer to the property documentation above for more information. + +* `spring.cloud.stream.rabbit.bindings..consumer.bindingRoutingKey=myRoutingKey` +* `spring.cloud.stream.rabbit.bindings..consumer.exchangeType=` + +* `spring.cloud.stream.rabbit.bindings..producer.routingKeyExpression='myRoutingKey'` + +There are similar properties used when declaring a dead-letter exchange/queue, when `autoBindDlq` is `true`. + +== Retry With the RabbitMQ Binder + +When retry is enabled within the binder, the listener container thread is suspended for any back off periods that are configured. +This might be important when strict ordering is required with a single consumer. However, for other use cases, it prevents other messages from being processed on that thread. +An alternative to using binder retry is to set up dead lettering with time to live on the dead-letter queue (DLQ) as well as dead-letter configuration on the DLQ itself. +See "`<>`" for more information about the properties discussed here. +You can use the following example configuration to enable this feature: + +* Set `autoBindDlq` to `true`. +The binder create a DLQ. +Optionally, you can specify a name in `deadLetterQueueName`. +* Set `dlqTtl` to the back off time you want to wait between redeliveries. +* Set the `dlqDeadLetterExchange` to the default exchange. +Expired messages from the DLQ are routed to the original queue, because the default `deadLetterRoutingKey` is the queue name (`destination.group`). +Setting to the default exchange is achieved by setting the property with no value, as shown in the next example. + +To force a message to be dead-lettered, either throw an `AmqpRejectAndDontRequeueException` or set `requeueRejected` to `false` (the default) and throw any exception. + +The loop continue without end, which is fine for transient problems, but you may want to give up after some number of attempts. +Fortunately, RabbitMQ provides the `x-death` header, which lets you determine how many cycles have occurred. + +To acknowledge a message after giving up, throw an `ImmediateAcknowledgeAmqpException`. + +=== Putting it All Together + +The following configuration creates an exchange `myDestination` with queue `myDestination.consumerGroup` bound to a topic exchange with a wildcard routing key `#`: + +[source] +--- +spring.cloud.stream.bindings.input.destination=myDestination +spring.cloud.stream.bindings.input.group=consumerGroup +#disable binder retries +spring.cloud.stream.bindings.input.consumer.max-attempts=1 +#dlx/dlq setup +spring.cloud.stream.rabbit.bindings.input.consumer.auto-bind-dlq=true +spring.cloud.stream.rabbit.bindings.input.consumer.dlq-ttl=5000 +spring.cloud.stream.rabbit.bindings.input.consumer.dlq-dead-letter-exchange= +--- + +This configuration creates a DLQ bound to a direct exchange (`DLX`) with a routing key of `myDestination.consumerGroup`. +When messages are rejected, they are routed to the DLQ. +After 5 seconds, the message expires and is routed to the original queue by using the queue name as the routing key, as shown in the following example: + +.Spring Boot application +[source, java] +---- +@SpringBootApplication +@EnableBinding(Sink.class) +public class XDeathApplication { + + public static void main(String[] args) { + SpringApplication.run(XDeathApplication.class, args); + } + + @StreamListener(Sink.INPUT) + public void listen(String in, @Header(name = "x-death", required = false) Map death) { + if (death != null && death.get("count").equals(3L)) { + // giving up - don't send to DLX + throw new ImmediateAcknowledgeAmqpException("Failed after 4 attempts"); + } + throw new AmqpRejectAndDontRequeueException("failed"); + } + +} +---- + +Notice that the count property in the `x-death` header is a `Long`. + +[[rabbit-error-channels]] +== Error Channels + +Starting with version 1.3, the binder unconditionally sends exceptions to an error channel for each consumer destination and can also be configured to send async producer send failures to an error channel. +See "`<>`" for more information. + +RabbitMQ has two types of send failures: + +* Returned messages, +* Negatively acknowledged https://www.rabbitmq.com/confirms.html[Publisher Confirms]. + +The latter is rare. +According to the RabbitMQ documentation "[A nack] will only be delivered if an internal error occurs in the Erlang process responsible for a queue.". +You can also get a negative acknowledgment if you publish to a bounded queue with `reject-publish` queue overflow behavior. + +As well as enabling producer error channels (as described in "`<>`"), the RabbitMQ binder only sends messages to the channels if the connection factory is appropriately configured, as follows: + +* `ccf.setPublisherConfirms(true);` +* `ccf.setPublisherReturns(true);` + +When using Spring Boot configuration for the connection factory, set the following properties: + +* `spring.rabbitmq.publisher-confirms` +* `spring.rabbitmq.publisher-returns` + +The payload of the `ErrorMessage` for a returned message is a `ReturnedAmqpMessageException` with the following properties: + +* `failedMessage`: The spring-messaging `Message` that failed to be sent. +* `amqpMessage`: The raw spring-amqp `Message`. +* `replyCode`: An integer value indicating the reason for the failure (for example, 312 - No route). +* `replyText`: A text value indicating the reason for the failure (for example, `NO_ROUTE`). +* `exchange`: The exchange to which the message was published. +* `routingKey`: The routing key used when the message was published. + +Also see <> for an alternative mechanism to receive returned messages. + +For negatively acknowledged confirmations, the payload is a `NackedAmqpMessageException` with the following properties: + +* `failedMessage`: The spring-messaging `Message` that failed to be sent. +* `nackReason`: A reason (if available -- you may need to examine the broker logs for more information). + +There is no automatic handling of these exceptions (such as sending to a <>). +You can consume these exceptions with your own Spring Integration flow. + +[[rabbitmq-stream-producer]] +=== Initial Producer Support for the RabbitMQ Stream Plugin + +Basic support for the https://rabbitmq.com/stream.html[RabbitMQ Stream Plugin] is now provided. +To enable this feature, you must add the `spring-rabbit-stream` jar to the class path - it must be the same version as `spring-amqp` and `spring-rabbit`. + +IMPORTANT: The producer properties described above are not supported when you set the `producerType` property to `STREAM_SYNC` or `STREAM_ASYNC`. + +To configure the binder to use a stream `ProducerType`, you must add an `Environment` `@Bean` and, optionally, a customizer to customize the message handler. + +==== +[source, java] +---- +@Bean +Environment streamEnv() { + return Environment.builder() + .build(); +} + +@Bean +ProducerMessageHandlerCustomizer handlerCustomizer() { + return (hand, dest) -> { + RabbitStreamMessageHandler handler = (RabbitStreamMessageHandler) hand; + handler.setConfirmTimeout(5000); + ((RabbitStreamTemplate) handler.getStreamOperations()).setProducerCustomizer( + (name, builder) -> { + ... + }); + }; +} +---- +==== + +Refer to the https://rabbitmq.github.io/rabbitmq-stream-java-client/stable/htmlsingle/[RabbitMQ Stream Java Client documentation] for information about configuring the environment and producer builder. +======= +[[rabbit-binder-health-indicator]] +== Rabbit Binder Health Indicator + +The health indicator for Rabbit binder delegates to the one provided from Spring Boot. +For more information on this, see https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#actuator.endpoints.health.auto-configured-health-indicators[this]. + +You can disable this health indicator at the binder level by using the property - `management.health.binders.enabled` and set this to `false`. +In the case of multibinder environements, this has to be set on the binder's environment properties. + +When the health indicator is disabled, you should see something like the below in the health actuator endpoint: + +``` +"rabbit": { + "status": "UNKNOWN" +} +``` + +At the Spring Boot level, if you want to disable the Rabbit health indicator, you need to use the property `management.health.rabbit.enabled` and set to `false`. diff --git a/r-binder/docs/src/main/asciidoc/partitions.adoc b/r-binder/docs/src/main/asciidoc/partitions.adoc new file mode 100644 index 000000000..0be97700f --- /dev/null +++ b/r-binder/docs/src/main/asciidoc/partitions.adoc @@ -0,0 +1,125 @@ +== Partitioning with the RabbitMQ Binder + +RabbitMQ does not support partitioning natively. + +Sometimes, it is advantageous to send data to specific partitions -- for example, when you want to strictly order message processing, all messages for a particular customer should go to the same partition. + +The `RabbitMessageChannelBinder` provides partitioning by binding a queue for each partition to the destination exchange. + +The following Java and YAML examples show how to configure the producer: + +.Producer +[source, java] +---- +@SpringBootApplication +@EnableBinding(Source.class) +public class RabbitPartitionProducerApplication { + + private static final Random RANDOM = new Random(System.currentTimeMillis()); + + private static final String[] data = new String[] { + "abc1", "def1", "qux1", + "abc2", "def2", "qux2", + "abc3", "def3", "qux3", + "abc4", "def4", "qux4", + }; + + public static void main(String[] args) { + new SpringApplicationBuilder(RabbitPartitionProducerApplication.class) + .web(false) + .run(args); + } + + @InboundChannelAdapter(channel = Source.OUTPUT, poller = @Poller(fixedRate = "5000")) + public Message generate() { + String value = data[RANDOM.nextInt(data.length)]; + System.out.println("Sending: " + value); + return MessageBuilder.withPayload(value) + .setHeader("partitionKey", value) + .build(); + } + +} +---- + +.application.yml +[source, yaml] +---- + spring: + cloud: + stream: + bindings: + output: + destination: partitioned.destination + producer: + partitioned: true + partition-key-expression: headers['partitionKey'] + partition-count: 2 + required-groups: + - myGroup +---- + +[NOTE] +==== +The configuration in the prececing example uses the default partitioning (`key.hashCode() % partitionCount`). +This may or may not provide a suitably balanced algorithm, depending on the key values. +You can override this default by using the `partitionSelectorExpression` or `partitionSelectorClass` properties. + +The `required-groups` property is required only if you need the consumer queues to be provisioned when the producer is deployed. +Otherwise, any messages sent to a partition are lost until the corresponding consumer is deployed. +==== + +The following configuration provisions a topic exchange: + +image::part-exchange.png[scaledwidth="50%"] + +The following queues are bound to that exchange: + +image::part-queues.png[scaledwidth="50%"] + +The following bindings associate the queues to the exchange: + +image::part-bindings.png[scaledwidth="50%"] + +The following Java and YAML examples continue the previous examples and show how to configure the consumer: + +.Consumer +[source, java] +---- +@SpringBootApplication +@EnableBinding(Sink.class) +public class RabbitPartitionConsumerApplication { + + public static void main(String[] args) { + new SpringApplicationBuilder(RabbitPartitionConsumerApplication.class) + .web(false) + .run(args); + } + + @StreamListener(Sink.INPUT) + public void listen(@Payload String in, @Header(AmqpHeaders.CONSUMER_QUEUE) String queue) { + System.out.println(in + " received from queue " + queue); + } + +} +---- + +.application.yml +[source, yaml] +---- + spring: + cloud: + stream: + bindings: + input: + destination: partitioned.destination + group: myGroup + consumer: + partitioned: true + instance-index: 0 +---- + +IMPORTANT: The `RabbitMessageChannelBinder` does not support dynamic scaling. +There must be at least one consumer per partition. +The consumer's `instanceIndex` is used to indicate which partition is consumed. +Platforms such as Cloud Foundry can have only one instance with an `instanceIndex`. diff --git a/r-binder/docs/src/main/asciidoc/spring-cloud-stream-binder-rabbit-aggregate.adoc b/r-binder/docs/src/main/asciidoc/spring-cloud-stream-binder-rabbit-aggregate.adoc new file mode 100644 index 000000000..ec1ccd5a9 --- /dev/null +++ b/r-binder/docs/src/main/asciidoc/spring-cloud-stream-binder-rabbit-aggregate.adoc @@ -0,0 +1,3 @@ +include::overview.adoc[leveloffset=+1] +include::dlq.adoc[leveloffset=+1] +include::partitions.adoc[leveloffset=+1] \ No newline at end of file diff --git a/r-binder/docs/src/main/asciidoc/spring-cloud-stream-binder-rabbit.adoc b/r-binder/docs/src/main/asciidoc/spring-cloud-stream-binder-rabbit.adoc new file mode 100644 index 000000000..d5feae6d8 --- /dev/null +++ b/r-binder/docs/src/main/asciidoc/spring-cloud-stream-binder-rabbit.adoc @@ -0,0 +1,53 @@ +:github-tag: master +:github-repo: spring-cloud/spring-cloud-stream-binder-rabbit +:github-raw: https://raw.githubusercontent.com/{github-repo}/{github-tag} +:github-code: https://github.com/{github-repo}/tree/{github-tag} +:toc: left +:toclevels: 8 +:nofooter: +:sectlinks: true + +[[spring-cloud-stream-binder-rabbit-reference]] += Spring Cloud Stream RabbitMQ Binder Reference Guide +Sabby Anandan, Marius Bogoevici, Eric Bottard, Mark Fisher, Ilayaperumal Gopinathan, Gunnar Hillert, Mark Pollack, Patrick Peralta, Glenn Renfro, Thomas Risberg, Dave Syer, David Turanski, Janne Valkealahti, Benjamin Klein, Gary Russell, Jay Bryant +:doctype: book +:toc: +:toclevels: 4 +:source-highlighter: prettify +:numbered: +:icons: font +:hide-uri-scheme: +:spring-cloud-stream-binder-rabbit-repo: snapshot +:github-tag: master +:spring-cloud-stream-binder-rabbit-docs-version: current +:spring-cloud-stream-binder-rabbit-docs: https://docs.spring.io/spring-cloud-stream-binder-rabbit/docs/{spring-cloud-stream-binder-rabbit-docs-version}/reference +:spring-cloud-stream-binder-rabbit-docs-current: https://docs.spring.io/spring-cloud-stream-binder-rabbit/docs/current-SNAPSHOT/reference/html/ +:github-repo: spring-cloud/spring-cloud-stream-binder-rabbit +:github-raw: https://raw.github.com/{github-repo}/{github-tag} +:github-code: https://github.com/{github-repo}/tree/{github-tag} +:github-wiki: https://github.com/{github-repo}/wiki +:github-master-code: https://github.com/{github-repo}/tree/master +:sc-ext: java + +// ====================================================================================== + +*{project-version}* + + += Reference Guide + + +include::overview.adoc[] + +include::dlq.adoc[] + +include::partitions.adoc[] + += Appendices +[appendix] +include::building.adoc[] + +[appendix] +include::contributing.adoc[] + +// ====================================================================================== diff --git a/r-binder/docs/src/main/ruby/generate_readme.sh b/r-binder/docs/src/main/ruby/generate_readme.sh new file mode 100755 index 000000000..6d0ce9dc5 --- /dev/null +++ b/r-binder/docs/src/main/ruby/generate_readme.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env ruby + +base_dir = File.join(File.dirname(__FILE__),'../../..') +src_dir = File.join(base_dir, "/src/main/asciidoc") +require 'asciidoctor' +require 'optparse' + +options = {} +file = "#{src_dir}/README.adoc" + +OptionParser.new do |o| + o.on('-o OUTPUT_FILE', 'Output file (default is stdout)') { |file| options[:to_file] = file unless file=='-' } + o.on('-h', '--help') { puts o; exit } + o.parse! +end + +file = ARGV[0] if ARGV.length>0 + +# Copied from https://github.com/asciidoctor/asciidoctor-extensions-lab/blob/master/scripts/asciidoc-coalescer.rb +doc = Asciidoctor.load_file file, safe: :unsafe, header_only: true, attributes: options[:attributes] +header_attr_names = (doc.instance_variable_get :@attributes_modified).to_a +header_attr_names.each {|k| doc.attributes[%(#{k}!)] = '' unless doc.attr? k } +attrs = doc.attributes +attrs['allow-uri-read'] = true +puts attrs + +out = "// Do not edit this file (e.g. go instead to src/main/asciidoc)\n\n" +doc = Asciidoctor.load_file file, safe: :unsafe, parse: false, attributes: attrs +out << doc.reader.read + +unless options[:to_file] + puts out +else + File.open(options[:to_file],'w+') do |file| + file.write(out) + end +end diff --git a/r-binder/mvnw b/r-binder/mvnw new file mode 100755 index 000000000..41c0f0c23 --- /dev/null +++ b/r-binder/mvnw @@ -0,0 +1,310 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/r-binder/mvnw.cmd b/r-binder/mvnw.cmd new file mode 100644 index 000000000..86115719e --- /dev/null +++ b/r-binder/mvnw.cmd @@ -0,0 +1,182 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + +FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/r-binder/pom.xml b/r-binder/pom.xml new file mode 100644 index 000000000..e75d70f1e --- /dev/null +++ b/r-binder/pom.xml @@ -0,0 +1,216 @@ + + + 4.0.0 + spring-cloud-stream-binder-rabbit-parent + 3.2.3-SNAPSHOT + pom + + org.springframework.cloud + spring-cloud-build + 3.1.1 + + + + 3.2.3-SNAPSHOT + 1.8 + 3.2.2 + true + true + + true + + + + spring-cloud-stream-binder-rabbit-core + spring-cloud-stream-binder-rabbit + spring-cloud-starter-stream-rabbit + spring-cloud-stream-binder-rabbit-test-support + docs + + + + + org.springframework.cloud + spring-cloud-stream + ${spring-cloud-stream.version} + + + org.springframework.cloud + spring-cloud-stream-binder-rabbit + ${project.version} + + + org.springframework.cloud + spring-cloud-stream-binder-test + ${spring-cloud-stream.version} + test + + + org.springframework.cloud + spring-cloud-stream-test-support-internal + ${spring-cloud-stream.version} + test + + + org.springframework.cloud + spring-cloud-stream-binder-rabbit-test-support + ${project.version} + test + + + + + + org.junit.vintage + junit-vintage-engine + test + + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 1.7 + + + org.apache.maven.plugins + maven-javadoc-plugin + + true + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + ${java.version} + ${java.version} + -parameters + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + + + + spring + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/libs-snapshot-local + + true + + + false + + + + spring-milestones + Spring Milestones + https://repo.spring.io/libs-milestone-local + + false + + + + spring-releases + Spring Releases + https://repo.spring.io/release + + false + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/libs-snapshot-local + + true + + + false + + + + spring-milestones + Spring Milestones + https://repo.spring.io/libs-milestone-local + + false + + + + spring-releases + Spring Releases + https://repo.spring.io/libs-release-local + + false + + + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/libs-snapshot-local + + + spring-milestones + Spring Milestones + https://repo.spring.io/libs-milestone-local + + + spring-releases + Spring Releases + https://repo.spring.io/release + + + rsocket-snapshots + RSocket Snapshots + https://oss.jfrog.org/oss-snapshot-local + + true + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/libs-snapshot-local + + + spring-milestones + Spring Milestones + https://repo.spring.io/libs-milestone-local + + + spring-releases + Spring Releases + https://repo.spring.io/libs-release-local + + + diff --git a/r-binder/spring-cloud-starter-stream-rabbit/pom.xml b/r-binder/spring-cloud-starter-stream-rabbit/pom.xml new file mode 100644 index 000000000..4c34d527d --- /dev/null +++ b/r-binder/spring-cloud-starter-stream-rabbit/pom.xml @@ -0,0 +1,25 @@ + + + 4.0.0 + + org.springframework.cloud + spring-cloud-stream-binder-rabbit-parent + 3.2.3-SNAPSHOT + + spring-cloud-starter-stream-rabbit + Spring Cloud Starter Stream Rabbit + https://projects.spring.io/spring-cloud + + Pivotal Software, Inc. + https://www.spring.io + + + ${basedir}/../.. + + + + org.springframework.cloud + spring-cloud-stream-binder-rabbit + + + diff --git a/r-binder/spring-cloud-stream-binder-rabbit-core/.jdk8 b/r-binder/spring-cloud-stream-binder-rabbit-core/.jdk8 new file mode 100644 index 000000000..e69de29bb diff --git a/r-binder/spring-cloud-stream-binder-rabbit-core/pom.xml b/r-binder/spring-cloud-stream-binder-rabbit-core/pom.xml new file mode 100644 index 000000000..7d3e7ee64 --- /dev/null +++ b/r-binder/spring-cloud-stream-binder-rabbit-core/pom.xml @@ -0,0 +1,53 @@ + + + 4.0.0 + + spring-cloud-stream-binder-rabbit-core + jar + spring-cloud-stream-binder-rabbit-core + RabbitMQ binder core + + + org.springframework.cloud + spring-cloud-stream-binder-rabbit-parent + 3.2.3-SNAPSHOT + + + + + org.springframework.cloud + spring-cloud-stream + + + org.springframework.boot + spring-boot-starter-amqp + + + org.springframework.integration + spring-integration-amqp + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework + spring-web + + + org.apache.httpcomponents + httpclient + + + com.rabbitmq + http-client + 2.1.0.RELEASE + + + org.springframework.cloud + spring-cloud-stream-binder-rabbit-test-support + test + + + diff --git a/r-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/admin/RabbitAdminException.java b/r-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/admin/RabbitAdminException.java new file mode 100644 index 000000000..fbfb32c19 --- /dev/null +++ b/r-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/admin/RabbitAdminException.java @@ -0,0 +1,36 @@ +/* + * Copyright 2015-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.stream.binder.rabbit.admin; + +/** + * Exceptions thrown while interfacing with the RabbitMQ admin plugin. + * + * @author Gary Russell + * @since 1.2 + */ +@SuppressWarnings("serial") +public class RabbitAdminException extends RuntimeException { + + public RabbitAdminException(String message, Throwable cause) { + super(message, cause); + } + + public RabbitAdminException(String message) { + super(message); + } + +} diff --git a/r-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/admin/RabbitBindingCleaner.java b/r-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/admin/RabbitBindingCleaner.java new file mode 100644 index 000000000..13c239a0b --- /dev/null +++ b/r-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/admin/RabbitBindingCleaner.java @@ -0,0 +1,172 @@ +/* + * Copyright 2015-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.stream.binder.rabbit.admin; + +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; + +import com.rabbitmq.http.client.Client; +import com.rabbitmq.http.client.domain.BindingInfo; +import com.rabbitmq.http.client.domain.ExchangeInfo; +import com.rabbitmq.http.client.domain.QueueInfo; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.cloud.stream.binder.AbstractBinder; +import org.springframework.cloud.stream.binder.BindingCleaner; + +/** + * Implementation of {@link org.springframework.cloud.stream.binder.BindingCleaner} for + * the {@code RabbitBinder}. + * + * @author Gary Russell + * @author David Turanski + * @since 1.2 + */ +public class RabbitBindingCleaner implements BindingCleaner { + + private static final Log logger = LogFactory.getLog(RabbitBindingCleaner.class); + + private static final String PREFIX_DELIMITER = "."; + + /** + * Binder prefix. + */ + public static final String BINDER_PREFIX = "binder" + PREFIX_DELIMITER; + + @Override + public Map> clean(String entity, boolean isJob) { + return clean("http://localhost:15672/api", "guest", "guest", "/", BINDER_PREFIX, + entity, isJob); + } + + public Map> clean(String adminUri, String user, String pw, + String vhost, String binderPrefix, String entity, boolean isJob) { + + try { + Client client = new Client(adminUri, user, pw); + return doClean(client, + vhost == null ? "/" : vhost, + binderPrefix == null ? BINDER_PREFIX : binderPrefix, entity, isJob); + } + catch (MalformedURLException | URISyntaxException e) { + throw new RabbitAdminException("Couldn't create a Client", e); + } + } + + private Map> doClean(Client client, + String vhost, String binderPrefix, String entity, boolean isJob) { + + LinkedList removedQueues = isJob ? null + : findStreamQueues(client, vhost, binderPrefix, entity); + List removedExchanges = findExchanges(client, vhost, binderPrefix, entity); + // Delete the queues in reverse order to enable re-running after a partial + // success. + // The queue search above starts with 0 and terminates on a not found. + if (removedQueues != null) { + removedQueues.descendingIterator().forEachRemaining(q -> { + client.deleteQueue(vhost, q); + if (logger.isDebugEnabled()) { + logger.debug("deleted queue: " + q); + } + }); + } + Map> results = new HashMap<>(); + if (removedQueues.size() > 0) { + results.put("queues", removedQueues); + } + // Fanout exchanges for taps + removedExchanges.forEach(exchange -> { + client.deleteExchange(vhost, exchange); + if (logger.isDebugEnabled()) { + logger.debug("deleted exchange: " + exchange); + } + }); + if (removedExchanges.size() > 0) { + results.put("exchanges", removedExchanges); + } + return results; + } + + private LinkedList findStreamQueues(Client client, String vhost, String binderPrefix, String stream) { + String queueNamePrefix = adjustPrefix(AbstractBinder.applyPrefix(binderPrefix, stream)); + List queues = client.getQueues(vhost); + return queues.stream() + .filter(q -> q.getName().startsWith(queueNamePrefix)) + .map(q -> checkNoConsumers(q)) + .collect(Collectors.toCollection(LinkedList::new)); + } + + private String adjustPrefix(String prefix) { + if (prefix.endsWith("*")) { + return prefix.substring(0, prefix.length() - 1); + } + else { + return prefix + PREFIX_DELIMITER; + } + } + + private String checkNoConsumers(QueueInfo queue) { + if (queue.getConsumerCount() != 0) { + throw new RabbitAdminException("Queue " + queue.getName() + " is in use"); + } + return queue.getName(); + } + + private List findExchanges(Client client, String vhost, String binderPrefix, String entity) { + List exchanges = client.getExchanges(vhost); + String exchangeNamePrefix = adjustPrefix(AbstractBinder.applyPrefix(binderPrefix, entity)); + List exchangesToRemove = exchanges.stream() + .filter(e -> e.getName().startsWith(exchangeNamePrefix)) + .map(e -> { + System.out.println(e.getName()); + List bindingsBySource = client.getBindingsBySource(vhost, e.getName()); + return Collections.singletonMap(e.getName(), bindingsBySource); + }) + .map(bindingsMap -> hasNoForeignBindings(bindingsMap, exchangeNamePrefix)) + .collect(Collectors.toList()); + exchangesToRemove.stream() + .map(exchange -> client.getExchangeBindingsByDestination(vhost, exchange)) + .forEach(bindings -> { + if (bindings.size() > 0) { + throw new RabbitAdminException("Cannot delete exchange " + + bindings.get(0).getDestination() + "; it is a destination: " + bindings); + } + }); + return exchangesToRemove; + } + + private String hasNoForeignBindings(Map> bindings, String exchangeNamePrefix) { + Entry> next = bindings.entrySet().iterator().next(); + for (BindingInfo binding : next.getValue()) { + if (!"queue".equals(binding.getDestinationType()) + || !binding.getDestination().startsWith(exchangeNamePrefix)) { + throw new RabbitAdminException("Cannot delete exchange " + + next.getKey() + "; it has bindings: " + bindings); + } + } + return next.getKey(); + } + +} diff --git a/r-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/properties/RabbitBinderConfigurationProperties.java b/r-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/properties/RabbitBinderConfigurationProperties.java new file mode 100644 index 000000000..29e234fcc --- /dev/null +++ b/r-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/properties/RabbitBinderConfigurationProperties.java @@ -0,0 +1,95 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.stream.binder.rabbit.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author David Turanski + * @author Gary Russell + */ +@ConfigurationProperties(prefix = "spring.cloud.stream.rabbit.binder") +public class RabbitBinderConfigurationProperties { + + /** + * Urls for management plugins; only needed for queue affinity. + */ + private String[] adminAddresses = new String[0]; + + /** + * Cluster member node names; only needed for queue affinity. + */ + private String[] nodes = new String[0]; + + /** + * Compression level for compressed bindings; see 'java.util.zip.Deflator'. + */ + private int compressionLevel; + + /** + * Prefix for connection names from this binder. + */ + private String connectionNamePrefix; + + public String[] getAdminAddresses() { + return adminAddresses; + } + + public void setAdminAddresses(String[] adminAddresses) { + this.adminAddresses = adminAddresses; + } + + /** + * @param adminAddresses A comma-separated list of RabbitMQ management plugin URLs. + * @deprecated in favor of {@link #setAdminAddresses(String[])}. Will be removed in a + * future release. + */ + @Deprecated + public void setAdminAdresses(String[] adminAddresses) { + setAdminAddresses(adminAddresses); + } + + @Deprecated + public String[] getAdminAdresses() { + return this.adminAddresses; + } + + public String[] getNodes() { + return nodes; + } + + public void setNodes(String[] nodes) { + this.nodes = nodes; + } + + public int getCompressionLevel() { + return compressionLevel; + } + + public void setCompressionLevel(int compressionLevel) { + this.compressionLevel = compressionLevel; + } + + public String getConnectionNamePrefix() { + return this.connectionNamePrefix; + } + + public void setConnectionNamePrefix(String connectionNamePrefix) { + this.connectionNamePrefix = connectionNamePrefix; + } + +} diff --git a/r-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/properties/RabbitBindingProperties.java b/r-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/properties/RabbitBindingProperties.java new file mode 100644 index 000000000..b1e141c3d --- /dev/null +++ b/r-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/properties/RabbitBindingProperties.java @@ -0,0 +1,47 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.stream.binder.rabbit.properties; + +import org.springframework.cloud.stream.binder.BinderSpecificPropertiesProvider; + +/** + * @author Marius Bogoevici + * @author Oleg Zhurakousky + */ +public class RabbitBindingProperties implements BinderSpecificPropertiesProvider { + + private RabbitConsumerProperties consumer = new RabbitConsumerProperties(); + + private RabbitProducerProperties producer = new RabbitProducerProperties(); + + public RabbitConsumerProperties getConsumer() { + return consumer; + } + + public void setConsumer(RabbitConsumerProperties consumer) { + this.consumer = consumer; + } + + public RabbitProducerProperties getProducer() { + return producer; + } + + public void setProducer(RabbitProducerProperties producer) { + this.producer = producer; + } + +} diff --git a/r-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/properties/RabbitCommonProperties.java b/r-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/properties/RabbitCommonProperties.java new file mode 100644 index 000000000..b72dcd91d --- /dev/null +++ b/r-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/properties/RabbitCommonProperties.java @@ -0,0 +1,588 @@ +/* + * Copyright 2017-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.stream.binder.rabbit.properties; + +import java.util.HashMap; +import java.util.Map; + +import org.hibernate.validator.constraints.Range; + +import org.springframework.amqp.core.ExchangeTypes; + +/** + * @author Gary Russell + * @author Soby Chacko + * @since 1.2 + * + */ +public abstract class RabbitCommonProperties { + + /** + * DLQ name prefix. + */ + public static final String DEAD_LETTER_EXCHANGE = "DLX"; + + /** + * type of exchange to declare (if necessary, and declareExchange is true). + */ + private String exchangeType = ExchangeTypes.TOPIC; + + /** + * whether to declare the exchange. + */ + private boolean declareExchange = true; + + /** + * whether to declare the exchange as durable. + */ + private boolean exchangeDurable = true; + + /** + * whether to declare the exchange as auto-delete. + */ + private boolean exchangeAutoDelete = false; + + /** + * whether a delayed message exchange should be used. + */ + private boolean delayedExchange = false; + + /** + * set to true to name the queue with only the group; default is destination.group. + */ + private boolean queueNameGroupOnly = false; + + /** + * whether to bind a queue (or queues when partitioned) to the exchange. + */ + private boolean bindQueue = true; + + /** + * routing key to bind (default # for non-partitioned, destination-instanceIndex for + * partitioned). + */ + private String bindingRoutingKey; + + /** + * when not null, treat 'bindingRoutingKey' as a delimited list of keys to bind. + */ + private String bindingRoutingKeyDelimiter; + + /** + * default time to live to apply to the queue when declared (ms). + */ + private Integer ttl; + + /** + * how long before an unused queue is deleted (ms). + */ + private Integer expires; + + /** + * maximum number of messages in the queue. + */ + private Integer maxLength; + + /** + * maximum number of total bytes in the queue from all messages. + */ + private Integer maxLengthBytes; + + /** + * maximum priority of messages in the queue (0-255). + */ + private Integer maxPriority; + + /** + * name of the DLQ - default is prefix+destination.dlq. + */ + private String deadLetterQueueName; + + /** + * a DLX to assign to the queue; if autoBindDlq is true, defaults to 'prefix+DLX'. + */ + private String deadLetterExchange; + + /** + * the type of the DLX, if autoBindDlq is true. + */ + private String deadLetterExchangeType = ExchangeTypes.DIRECT; + + /** + * whether to declare the dead-letter exchange when autoBindDlq is true. + */ + private boolean declareDlx = true; + + /** + * a dead letter routing key to assign to that queue; if autoBindDlq is true, defaults + * to destination. + */ + private String deadLetterRoutingKey; + + /** + * default time to live to apply to the dead letter queue when declared (ms). + */ + private Integer dlqTtl; + + /** + * how long before an unused dead letter queue is deleted (ms). + */ + private Integer dlqExpires; + + /** + * maximum number of messages in the dead letter queue. + */ + private Integer dlqMaxLength; + + /** + * maximum number of total bytes in the dead letter queue from all messages. + */ + private Integer dlqMaxLengthBytes; + + /** + * maximum priority of messages in the dead letter queue (0-255). + */ + private Integer dlqMaxPriority; + + /** + * if a DLQ is declared, a DLX to assign to that queue; default none. + */ + private String dlqDeadLetterExchange; + + /** + * if a DLQ is declared, a dead letter routing key to assign to that queue; default + * none. + */ + private String dlqDeadLetterRoutingKey; + + /** + * true to automatically bind a dead letter queue to a DLX. + */ + private boolean autoBindDlq; + + /** + * prefix for elements declared in RabbitMQ (exchanges, queues). + */ + private String prefix = ""; + + /** + * true if the queue is provisioned as a lazy queue. + */ + private boolean lazy; + + /** + * true if the DLQ is provisioned as a lazy queue. + */ + private boolean dlqLazy; + + /** + * action when maxLength or maxLengthBytes is exceeded. + */ + private String overflowBehavior; + + /** + * action when maxLength or maxLengthBytes is exceeded. + */ + private String dlqOverflowBehavior; + + /** + * A map of binding arguments to apply when binding the queue to the exchange. + * Useful for a headers exchange, for example. + */ + private Map queueBindingArguments = new HashMap<>(); + + /** + * A map of binding arguments to apply when binding the dlq to the exchange. + * Useful for a headers exchange, for example. + */ + private Map dlqBindingArguments = new HashMap<>(); + + /** + * Configure the queue to be type quorum instead of classic. + */ + private QuorumConfig quorum = new QuorumConfig(); + + /** + * Configure the DLQ to be type quorum instead of classic. + */ + private QuorumConfig dlqQuorum = new QuorumConfig(); + + /** + * When true, set the 'x-single-active-consumer' queue argument to true. + */ + private boolean singleActiveConsumer; + + /** + * When true, set the 'x-single-active-consumer' queue argument to true. + */ + private boolean dlqSingleActiveConsumer; + + /** + * The bean name of a stream message converter to convert from a Spring AMQP Message + * to a Stream Message. + * @since 3.2 + */ + private String streamStreamMessageConverterBeanName; + + public String getExchangeType() { + return this.exchangeType; + } + + public void setExchangeType(String exchangeType) { + this.exchangeType = exchangeType; + } + + public boolean isDeclareExchange() { + return this.declareExchange; + } + + public void setDeclareExchange(boolean declareExchange) { + this.declareExchange = declareExchange; + } + + public boolean isExchangeDurable() { + return this.exchangeDurable; + } + + public void setExchangeDurable(boolean exchangeDurable) { + this.exchangeDurable = exchangeDurable; + } + + public boolean isExchangeAutoDelete() { + return this.exchangeAutoDelete; + } + + public void setExchangeAutoDelete(boolean exchangeAutoDelete) { + this.exchangeAutoDelete = exchangeAutoDelete; + } + + public boolean isDelayedExchange() { + return this.delayedExchange; + } + + public void setDelayedExchange(boolean delayedExchange) { + this.delayedExchange = delayedExchange; + } + + public boolean isQueueNameGroupOnly() { + return this.queueNameGroupOnly; + } + + public void setQueueNameGroupOnly(boolean queueNameGroupOnly) { + this.queueNameGroupOnly = queueNameGroupOnly; + } + + public boolean isBindQueue() { + return this.bindQueue; + } + + public void setBindQueue(boolean bindQueue) { + this.bindQueue = bindQueue; + } + + public String getBindingRoutingKey() { + return this.bindingRoutingKey; + } + + public void setBindingRoutingKey(String routingKey) { + this.bindingRoutingKey = routingKey; + } + + public String getBindingRoutingKeyDelimiter() { + return this.bindingRoutingKeyDelimiter; + } + + public void setBindingRoutingKeyDelimiter(String bindingRoutingKeyDelimiter) { + this.bindingRoutingKeyDelimiter = bindingRoutingKeyDelimiter; + } + + public Integer getTtl() { + return this.ttl; + } + + public void setTtl(Integer ttl) { + this.ttl = ttl; + } + + public Integer getExpires() { + return this.expires; + } + + public void setExpires(Integer expires) { + this.expires = expires; + } + + public Integer getMaxLength() { + return this.maxLength; + } + + public void setMaxLength(Integer maxLength) { + this.maxLength = maxLength; + } + + public Integer getMaxLengthBytes() { + return this.maxLengthBytes; + } + + public void setMaxLengthBytes(Integer maxLengthBytes) { + this.maxLengthBytes = maxLengthBytes; + } + + @Range(min = 0, max = 255) + public Integer getMaxPriority() { + return this.maxPriority; + } + + public void setMaxPriority(Integer maxPriority) { + this.maxPriority = maxPriority; + } + + public String getDeadLetterQueueName() { + return this.deadLetterQueueName; + } + + public void setDeadLetterQueueName(String deadLetterQueueName) { + this.deadLetterQueueName = deadLetterQueueName; + } + + public String getDeadLetterExchange() { + return this.deadLetterExchange; + } + + public void setDeadLetterExchange(String deadLetterExchange) { + this.deadLetterExchange = deadLetterExchange; + } + + public String getDeadLetterExchangeType() { + return this.deadLetterExchangeType; + } + + public void setDeadLetterExchangeType(String deadLetterExchangeType) { + this.deadLetterExchangeType = deadLetterExchangeType; + } + + public boolean isDeclareDlx() { + return this.declareDlx; + } + + public void setDeclareDlx(boolean declareDlx) { + this.declareDlx = declareDlx; + } + + public String getDeadLetterRoutingKey() { + return this.deadLetterRoutingKey; + } + + public void setDeadLetterRoutingKey(String deadLetterRoutingKey) { + this.deadLetterRoutingKey = deadLetterRoutingKey; + } + + public Integer getDlqTtl() { + return this.dlqTtl; + } + + public void setDlqTtl(Integer dlqTtl) { + this.dlqTtl = dlqTtl; + } + + public Integer getDlqExpires() { + return this.dlqExpires; + } + + public void setDlqExpires(Integer dlqExpires) { + this.dlqExpires = dlqExpires; + } + + public Integer getDlqMaxLength() { + return this.dlqMaxLength; + } + + public void setDlqMaxLength(Integer dlqMaxLength) { + this.dlqMaxLength = dlqMaxLength; + } + + public Integer getDlqMaxLengthBytes() { + return this.dlqMaxLengthBytes; + } + + public void setDlqMaxLengthBytes(Integer dlqMaxLengthBytes) { + this.dlqMaxLengthBytes = dlqMaxLengthBytes; + } + + public Integer getDlqMaxPriority() { + return this.dlqMaxPriority; + } + + public void setDlqMaxPriority(Integer dlqMaxPriority) { + this.dlqMaxPriority = dlqMaxPriority; + } + + public String getDlqDeadLetterExchange() { + return this.dlqDeadLetterExchange; + } + + public void setDlqDeadLetterExchange(String dlqDeadLetterExchange) { + this.dlqDeadLetterExchange = dlqDeadLetterExchange; + } + + public String getDlqDeadLetterRoutingKey() { + return this.dlqDeadLetterRoutingKey; + } + + public void setDlqDeadLetterRoutingKey(String dlqDeadLetterRoutingKey) { + this.dlqDeadLetterRoutingKey = dlqDeadLetterRoutingKey; + } + + public boolean isAutoBindDlq() { + return autoBindDlq; + } + + public void setAutoBindDlq(boolean autoBindDlq) { + this.autoBindDlq = autoBindDlq; + } + + public String getPrefix() { + return prefix; + } + + public void setPrefix(String prefix) { + this.prefix = prefix; + } + + public boolean isLazy() { + return this.lazy; + } + + public void setLazy(boolean lazy) { + this.lazy = lazy; + } + + public boolean isDlqLazy() { + return this.dlqLazy; + } + + public void setDlqLazy(boolean dlqLazy) { + this.dlqLazy = dlqLazy; + } + + public String getOverflowBehavior() { + return this.overflowBehavior; + } + + public void setOverflowBehavior(String overflowBehavior) { + this.overflowBehavior = overflowBehavior; + } + + public String getDlqOverflowBehavior() { + return this.dlqOverflowBehavior; + } + + public void setDlqOverflowBehavior(String dlqOverflowBehavior) { + this.dlqOverflowBehavior = dlqOverflowBehavior; + } + + public Map getQueueBindingArguments() { + return this.queueBindingArguments; + } + + public void setQueueBindingArguments(Map queueBindingArguments) { + this.queueBindingArguments = queueBindingArguments; + } + + public Map getDlqBindingArguments() { + return this.dlqBindingArguments; + } + + public void setDlqBindingArguments(Map dlqBindingArguments) { + this.dlqBindingArguments = dlqBindingArguments; + } + + public QuorumConfig getQuorum() { + return this.quorum; + } + + public void setQuorum(QuorumConfig quorum) { + this.quorum = quorum; + } + + public QuorumConfig getDlqQuorum() { + return this.dlqQuorum; + } + + public void setDlqQuorum(QuorumConfig dlqQuorum) { + this.dlqQuorum = dlqQuorum; + } + + public boolean isSingleActiveConsumer() { + return this.singleActiveConsumer; + } + + public void setSingleActiveConsumer(boolean singleActiveConsumer) { + this.singleActiveConsumer = singleActiveConsumer; + } + + public boolean isDlqSingleActiveConsumer() { + return this.dlqSingleActiveConsumer; + } + + public void setDlqSingleActiveConsumer(boolean dlqSingleActiveConsumer) { + this.dlqSingleActiveConsumer = dlqSingleActiveConsumer; + } + + public String getStreamStreamMessageConverterBeanName() { + return this.streamStreamMessageConverterBeanName; + } + + public void setStreamStreamMessageConverterBeanName(String streamStreamMessageConverterBeanName) { + this.streamStreamMessageConverterBeanName = streamStreamMessageConverterBeanName; + } + + public static class QuorumConfig { + + private boolean enabled; + + private Integer initialGroupSize; + + private Integer deliveryLimit; + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public Integer getInitialGroupSize() { + return this.initialGroupSize; + } + + public void setInitialGroupSize(Integer initialGroupSize) { + this.initialGroupSize = initialGroupSize; + } + + public Integer getDeliveryLimit() { + return this.deliveryLimit; + } + + public void setDeliveryLimit(Integer deliveryLimit) { + this.deliveryLimit = deliveryLimit; + } + + } + +} diff --git a/r-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/properties/RabbitConsumerProperties.java b/r-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/properties/RabbitConsumerProperties.java new file mode 100644 index 000000000..9d2d0fda5 --- /dev/null +++ b/r-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/properties/RabbitConsumerProperties.java @@ -0,0 +1,376 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.stream.binder.rabbit.properties; + +import javax.validation.constraints.Min; + +import org.springframework.amqp.core.AcknowledgeMode; +import org.springframework.amqp.core.MessageDeliveryMode; +import org.springframework.util.Assert; + +/** + * @author Marius Bogoevici + * @author Gary Russell + */ +public class RabbitConsumerProperties extends RabbitCommonProperties { + + /** + * true to use transacted channels. + */ + private boolean transacted; + + /** + * container acknowledge mode. + */ + private AcknowledgeMode acknowledgeMode = AcknowledgeMode.AUTO; + + /** + * maxumum concurrency of this consumer (threads). + */ + private int maxConcurrency = 1; + + /** + * number of prefetched messages pre consumer thread. + */ + private int prefetch = 1; + + /** + * messages per acknowledgment (and commit when transacted). + */ + private int batchSize = 1; + + /** + * true for a durable subscription. + */ + private boolean durableSubscription = true; + + /** + * republish failures to the DLQ with diagnostic headers. + */ + private boolean republishToDlq = true; + + /** + * when republishing to the DLQ, the delivery mode to use. + */ + private MessageDeliveryMode republishDeliveyMode = MessageDeliveryMode.PERSISTENT; + + /** + * true to requeue rejected messages, false to discard (or route to DLQ). + */ + private boolean requeueRejected = false; + + /** + * patterns to match which headers are mapped (inbound). + */ + private String[] headerPatterns = new String[] { "*" }; + + /** + * interval between reconnection attempts. + */ + private long recoveryInterval = 5000; + + /** + * true if the consumer is exclusive. + */ + private boolean exclusive; + + /** + * when true, stop the container instead of retrying queue declarations. + */ + private boolean missingQueuesFatal = false; + + /** + * how many times to attempt passive queue declaration. + */ + private Integer queueDeclarationRetries; + + /** + * interval between attempts to passively declare missing queues. + */ + private Long failedDeclarationRetryInterval; + + /** + * Used to create the consumer tags; will be appended by '#n' where 'n' increments for + * each consumer created. + */ + private String consumerTagPrefix; + + /** + * Room to leave for other headers after adding the stack trace to a DLQ message. + */ + private int frameMaxHeadroom = 20_000; + + /** + * The container type, SIMPLE or DIRECT. + */ + private ContainerType containerType = ContainerType.SIMPLE; + + /** + * Prefix for anonymous queue names (when no group is provided). + */ + private String anonymousGroupPrefix = "anonymous."; + + /** + * When true, the listener container will assemble a list from multiple messages, + * according to the batchSize and receiveTimeout properties. Only applies with + * {@link ContainerType#SIMPLE}. + */ + private boolean enableBatching; + + /** + * How long to block waiting to receive messages; increasing from the default 1 second + * will make the binding less responsive to stop requests. When enableConsumerBatching + * is true, a short batch may be emitted if this time elapses before the batchSize is + * satisfied. Only applies with {@link ContainerType#SIMPLE}. + */ + private Long receiveTimeout; + + public boolean isTransacted() { + return transacted; + } + + public void setTransacted(boolean transacted) { + this.transacted = transacted; + } + + public AcknowledgeMode getAcknowledgeMode() { + return acknowledgeMode; + } + + public void setAcknowledgeMode(AcknowledgeMode acknowledgeMode) { + Assert.notNull(acknowledgeMode, "Acknowledge mode cannot be null"); + this.acknowledgeMode = acknowledgeMode; + } + + @Min(value = 1, message = "Max Concurrency should be greater than zero.") + public int getMaxConcurrency() { + return maxConcurrency; + } + + public void setMaxConcurrency(int maxConcurrency) { + this.maxConcurrency = maxConcurrency; + } + + @Min(value = 1, message = "Prefetch should be greater than zero.") + public int getPrefetch() { + return prefetch; + } + + public void setPrefetch(int prefetch) { + this.prefetch = prefetch; + } + + /** + * @deprecated - use {@link #getHeaderPatterns()}. + * @return the header patterns. + */ + @Deprecated + public String[] getRequestHeaderPatterns() { + return this.headerPatterns; + } + + /** + * @deprecated - use {@link #setHeaderPatterns(String[])}. + * @param requestHeaderPatterns request header patterns + */ + @Deprecated + public void setRequestHeaderPatterns(String[] requestHeaderPatterns) { + this.headerPatterns = requestHeaderPatterns; + } + + /** + * @deprecated in favor of {@link #getBatchSize()} + * @return the tx size. + */ + @Deprecated + @Min(value = 1, message = "Tx Size should be greater than zero.") + public int getTxSize() { + return getBatchSize(); + } + + /** + * deprecated in favor of {@link #setBatchSize(int)}. + * @param txSize the tx size + */ + public void setTxSize(int txSize) { + setBatchSize(txSize); + } + + @Min(value = 1, message = "Batch Size should be greater than zero.") + public int getBatchSize() { + return batchSize; + } + + public void setBatchSize(int batchSize) { + this.batchSize = batchSize; + } + + public boolean isDurableSubscription() { + return durableSubscription; + } + + public void setDurableSubscription(boolean durableSubscription) { + this.durableSubscription = durableSubscription; + } + + public boolean isRepublishToDlq() { + return republishToDlq; + } + + public void setRepublishToDlq(boolean republishToDlq) { + this.republishToDlq = republishToDlq; + } + + public boolean isRequeueRejected() { + return requeueRejected; + } + + public MessageDeliveryMode getRepublishDeliveyMode() { + return this.republishDeliveyMode; + } + + public void setRepublishDeliveyMode(MessageDeliveryMode republishDeliveyMode) { + this.republishDeliveyMode = republishDeliveyMode; + } + + public void setRequeueRejected(boolean requeueRejected) { + this.requeueRejected = requeueRejected; + } + + public String[] getHeaderPatterns() { + return headerPatterns; + } + + public void setHeaderPatterns(String[] replyHeaderPatterns) { + this.headerPatterns = replyHeaderPatterns; + } + + public long getRecoveryInterval() { + return recoveryInterval; + } + + public void setRecoveryInterval(long recoveryInterval) { + this.recoveryInterval = recoveryInterval; + } + + public boolean isExclusive() { + return this.exclusive; + } + + public void setExclusive(boolean exclusive) { + this.exclusive = exclusive; + } + + public boolean getMissingQueuesFatal() { + return this.missingQueuesFatal; + } + + public void setMissingQueuesFatal(boolean missingQueuesFatal) { + this.missingQueuesFatal = missingQueuesFatal; + } + + public Integer getQueueDeclarationRetries() { + return this.queueDeclarationRetries; + } + + public void setQueueDeclarationRetries(Integer queueDeclarationRetries) { + this.queueDeclarationRetries = queueDeclarationRetries; + } + + public Long getFailedDeclarationRetryInterval() { + return this.failedDeclarationRetryInterval; + } + + public void setFailedDeclarationRetryInterval(Long failedDeclarationRetryInterval) { + this.failedDeclarationRetryInterval = failedDeclarationRetryInterval; + } + + public String getConsumerTagPrefix() { + return this.consumerTagPrefix; + } + + public void setConsumerTagPrefix(String consumerTagPrefix) { + this.consumerTagPrefix = consumerTagPrefix; + } + + public int getFrameMaxHeadroom() { + return this.frameMaxHeadroom; + } + + public void setFrameMaxHeadroom(int frameMaxHeadroom) { + this.frameMaxHeadroom = frameMaxHeadroom; + } + + public ContainerType getContainerType() { + return this.containerType; + } + + public void setContainerType(ContainerType containerType) { + this.containerType = containerType; + } + + public String getAnonymousGroupPrefix() { + return this.anonymousGroupPrefix; + } + + public void setAnonymousGroupPrefix(String anonymousGroupPrefix) { + this.anonymousGroupPrefix = anonymousGroupPrefix; + } + + public boolean isEnableBatching() { + return this.enableBatching; + } + + public void setEnableBatching(boolean enableBatching) { + this.enableBatching = enableBatching; + } + + public Long getReceiveTimeout() { + return this.receiveTimeout; + } + + public void setReceiveTimeout(Long receiveTimeout) { + this.receiveTimeout = receiveTimeout; + } + + /** + * Container type. + * @author Gary Russell + * @since 3.2 + * + */ + public enum ContainerType { + + /** + * Container where the RabbitMQ consumer dispatches messages to an invoker thread. + */ + SIMPLE, + + /** + * Container where the listener is invoked directly on the RabbitMQ consumer + * thread. + */ + DIRECT, + + /** + * Container that uses the RabbitMQ Stream Client. + */ + STREAM + + } + +} diff --git a/r-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/properties/RabbitExtendedBindingProperties.java b/r-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/properties/RabbitExtendedBindingProperties.java new file mode 100644 index 000000000..80d6b6530 --- /dev/null +++ b/r-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/properties/RabbitExtendedBindingProperties.java @@ -0,0 +1,51 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.stream.binder.rabbit.properties; + +import java.util.Map; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.stream.binder.AbstractExtendedBindingProperties; +import org.springframework.cloud.stream.binder.BinderSpecificPropertiesProvider; + +/** + * @author Marius Bogoevici + * @author Gary Russell + * @author Oleg Zhurakousky + * @author Soby Chacko + */ +@ConfigurationProperties("spring.cloud.stream.rabbit") +public class RabbitExtendedBindingProperties extends + AbstractExtendedBindingProperties { + + private static final String DEFAULTS_PREFIX = "spring.cloud.stream.rabbit.default"; + + @Override + public String getDefaultsPrefix() { + return DEFAULTS_PREFIX; + } + + public Map getBindings() { + return this.doGetBindings(); + } + + @Override + public Class getExtendedPropertiesEntryClass() { + return RabbitBindingProperties.class; + } + +} diff --git a/r-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/properties/RabbitProducerProperties.java b/r-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/properties/RabbitProducerProperties.java new file mode 100644 index 000000000..b25a6e228 --- /dev/null +++ b/r-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/properties/RabbitProducerProperties.java @@ -0,0 +1,297 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.stream.binder.rabbit.properties; + +import javax.validation.constraints.Min; + +import org.springframework.amqp.core.MessageDeliveryMode; +import org.springframework.expression.Expression; +import org.springframework.util.Assert; + +/** + * @author Marius Bogoevici + * @author Gary Russell + */ +public class RabbitProducerProperties extends RabbitCommonProperties { + + /** + * Determines the producer type. + * @since 3.2 + */ + public enum ProducerType { + + /** + * RabbitMQ Stream producer - blocks until confirm received. + */ + STREAM_SYNC, + + /** + * RabbitMQ Stream producer - does not block. + */ + STREAM_ASYNC, + + /** + * Classic AMQP producer. + */ + AMQP + } + + /** + * true to compress messages. + */ + private boolean compress; + + /** + * true to batch multiple messages into one. + */ + private boolean batchingEnabled; + + /** + * the number of messages to batch, when enabled. + */ + private int batchSize = 100; + + /** + * the size limit for batched messages. + */ + private int batchBufferLimit = 10000; + + /** + * the time after which an incomplete batch will be sent. + */ + private int batchTimeout = 5000; + + /** + * the bean name of a custom batching strategy to use instead of the + * {@link org.springframework.amqp.rabbit.batch.SimpleBatchingStrategy}. + */ + private String batchingStrategyBeanName; + + /** + * true to use transacted channels. + */ + private boolean transacted; + + /** + * the delivery mode for published messages. + */ + private MessageDeliveryMode deliveryMode = MessageDeliveryMode.PERSISTENT; + + /** + * patterns to match which headers are mapped (inbound). + */ + private String[] headerPatterns = new String[] { "*" }; + + /** + * when using a delayed message exchange, a SpEL expression to determine the delay to + * apply to messages. + */ + private Expression delayExpression; + + /** + * a static routing key when publishing messages; default is the destination name; + * suffixed by "-partition" when partitioned. This is only used if `routingKeyExpression` is null + */ + private String routingKey; + + /** + * a custom routing key when publishing messages; default is the destination name; + * suffixed by "-partition" when partitioned. + */ + private Expression routingKeyExpression; + + /** + * the channel name to which to send publisher confirms (acks) if the connection + * factory is so configured; default 'nullChannel'; requires + * 'errorChannelEnabled=true'. + */ + private String confirmAckChannel; + + /** + * When true, the binding will complete the {@link java.util.concurrent.Future} field + * in a {@link org.springframework.amqp.rabbit.connection.CorrelationData} contained + * in the + * {@link org.springframework.amqp.support.AmqpHeaders#PUBLISH_CONFIRM_CORRELATION} + * header when the confirmation is received. + */ + private boolean useConfirmHeader; + + /** + * When STREAM_SYNC or STREAM_ASYNC, create a RabbitMQ Stream producer instead of an + * AMQP producer. + * @since 3.2 + */ + private ProducerType producerType = ProducerType.AMQP; + + /** + * The bean name of a message converter to convert from spring-messaging Message to + * a Spring AMQP Message. + * @since 3.2 + */ + private String streamMessageConverterBeanName; + + /** + * @deprecated - use {@link #setHeaderPatterns(String[])}. + * @param requestHeaderPatterns the patterns. + */ + @Deprecated + public void setRequestHeaderPatterns(String[] requestHeaderPatterns) { + this.headerPatterns = requestHeaderPatterns; + } + + /** + * @deprecated - use {@link #getHeaderPatterns()}. + * @return the header patterns. + */ + @Deprecated + public String[] getRequestHeaderPatterns() { + return this.headerPatterns; + } + + public void setCompress(boolean compress) { + this.compress = compress; + } + + public boolean isCompress() { + return compress; + } + + public void setDeliveryMode(MessageDeliveryMode deliveryMode) { + this.deliveryMode = deliveryMode; + } + + public MessageDeliveryMode getDeliveryMode() { + return deliveryMode; + } + + public String[] getHeaderPatterns() { + return headerPatterns; + } + + public void setHeaderPatterns(String[] replyHeaderPatterns) { + this.headerPatterns = replyHeaderPatterns; + } + + public boolean isBatchingEnabled() { + return batchingEnabled; + } + + public void setBatchingEnabled(boolean batchingEnabled) { + this.batchingEnabled = batchingEnabled; + } + + @Min(value = 1, message = "Batch Size should be greater than zero.") + public int getBatchSize() { + return batchSize; + } + + public void setBatchSize(int batchSize) { + this.batchSize = batchSize; + } + + @Min(value = 1, message = "Batch Buffer Limit should be greater than zero.") + public int getBatchBufferLimit() { + return batchBufferLimit; + } + + public void setBatchBufferLimit(int batchBufferLimit) { + this.batchBufferLimit = batchBufferLimit; + } + + @Min(value = 1, message = "Batch Timeout should be greater than zero.") + public int getBatchTimeout() { + return batchTimeout; + } + + public void setBatchTimeout(int batchTimeout) { + this.batchTimeout = batchTimeout; + } + + public boolean isTransacted() { + return this.transacted; + } + + public void setTransacted(boolean transacted) { + this.transacted = transacted; + } + + public Expression getDelayExpression() { + return this.delayExpression; + } + + public void setDelayExpression(Expression delayExpression) { + this.delayExpression = delayExpression; + } + + public Expression getRoutingKeyExpression() { + return this.routingKeyExpression; + } + + public void setRoutingKeyExpression(Expression routingKeyExpression) { + this.routingKeyExpression = routingKeyExpression; + } + + public String getRoutingKey() { + return this.routingKey; + } + + public void setRoutingKey(String routingKey) { + this.routingKey = routingKey; + } + + public String getConfirmAckChannel() { + return this.confirmAckChannel; + } + + public void setConfirmAckChannel(String confirmAckChannel) { + this.confirmAckChannel = confirmAckChannel; + } + + public String getBatchingStrategyBeanName() { + return batchingStrategyBeanName; + } + + public void setBatchingStrategyBeanName(String batchingStrategyBeanName) { + this.batchingStrategyBeanName = batchingStrategyBeanName; + } + + public boolean isUseConfirmHeader() { + return this.useConfirmHeader; + } + + public void setUseConfirmHeader(boolean useConfirmHeader) { + this.useConfirmHeader = useConfirmHeader; + } + + public ProducerType getProducerType() { + return this.producerType; + } + + public void setProducerType(ProducerType producerType) { + Assert.notNull(producerType, "'producerType' cannot be null"); + this.producerType = producerType; + } + + public String getStreamMessageConverterBeanName() { + return this.streamMessageConverterBeanName; + } + + public void setStreamMessageConverterBeanName(String streamMessageConverterBeanName) { + this.streamMessageConverterBeanName = streamMessageConverterBeanName; + } + +} diff --git a/r-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/provisioning/RabbitExchangeQueueProvisioner.java b/r-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/provisioning/RabbitExchangeQueueProvisioner.java new file mode 100644 index 000000000..b0a036b95 --- /dev/null +++ b/r-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/provisioning/RabbitExchangeQueueProvisioner.java @@ -0,0 +1,851 @@ +/* + * Copyright 2016-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.stream.binder.rabbit.provisioning; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.amqp.AmqpConnectException; +import org.springframework.amqp.core.AnonymousQueue; +import org.springframework.amqp.core.Base64UrlNamingStrategy; +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.Binding.DestinationType; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.DeclarableCustomizer; +import org.springframework.amqp.core.DirectExchange; +import org.springframework.amqp.core.Exchange; +import org.springframework.amqp.core.ExchangeBuilder; +import org.springframework.amqp.core.FanoutExchange; +import org.springframework.amqp.core.HeadersExchange; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.core.TopicExchange; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.DeclarationExceptionEvent; +import org.springframework.amqp.rabbit.core.RabbitAdmin; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; +import org.springframework.cloud.stream.binder.ExtendedProducerProperties; +import org.springframework.cloud.stream.binder.rabbit.properties.RabbitCommonProperties; +import org.springframework.cloud.stream.binder.rabbit.properties.RabbitCommonProperties.QuorumConfig; +import org.springframework.cloud.stream.binder.rabbit.properties.RabbitConsumerProperties; +import org.springframework.cloud.stream.binder.rabbit.properties.RabbitConsumerProperties.ContainerType; +import org.springframework.cloud.stream.binder.rabbit.properties.RabbitProducerProperties; +import org.springframework.cloud.stream.provisioning.ConsumerDestination; +import org.springframework.cloud.stream.provisioning.ProducerDestination; +import org.springframework.cloud.stream.provisioning.ProvisioningException; +import org.springframework.cloud.stream.provisioning.ProvisioningProvider; +import org.springframework.context.ApplicationListener; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * AMQP implementation for {@link ProvisioningProvider}. + * + * @author Soby Chacko + * @author Gary Russell + * @author Oleg Zhurakousky + * @author Michael Michailidis + */ +// @checkstyle:off +public class RabbitExchangeQueueProvisioner + implements ApplicationListener, + ProvisioningProvider, ExtendedProducerProperties> { + + // @checkstyle:on + + /** + * The delimiter between a group and index when constructing a binder + * consumer/producer. + */ + private static final String GROUP_INDEX_DELIMITER = "."; + + protected final Log logger = LogFactory.getLog(getClass()); + + private final RabbitAdmin rabbitAdmin; + + private boolean notOurAdminException; + + private final GenericApplicationContext autoDeclareContext = new GenericApplicationContext(); + + private final List customizers; + + private final AtomicInteger producerExchangeBeanNameQualifier = new AtomicInteger(); + + public RabbitExchangeQueueProvisioner(ConnectionFactory connectionFactory) { + this(connectionFactory, Collections.emptyList()); + } + + public RabbitExchangeQueueProvisioner(ConnectionFactory connectionFactory, + List customizers) { + + this.rabbitAdmin = new RabbitAdmin(connectionFactory); + this.autoDeclareContext.refresh(); + this.rabbitAdmin.setApplicationContext(this.autoDeclareContext); + this.rabbitAdmin.afterPropertiesSet(); + this.customizers = customizers; + } + + @Override + public ProducerDestination provisionProducerDestination(String name, + ExtendedProducerProperties producerProperties) { + final String exchangeName = applyPrefix( + producerProperties.getExtension().getPrefix(), name); + Exchange exchange = buildExchange(producerProperties.getExtension(), + exchangeName); + String beanNameQualifier = "prod" + this.producerExchangeBeanNameQualifier.incrementAndGet(); + if (producerProperties.getExtension().isDeclareExchange()) { + declareExchange(exchangeName, beanNameQualifier, exchange); + } + Binding binding = null; + for (String requiredGroupName : producerProperties.getRequiredGroups()) { + String baseQueueName = producerProperties.getExtension() + .isQueueNameGroupOnly() ? requiredGroupName + : (exchangeName + "." + requiredGroupName); + if (!producerProperties.isPartitioned()) { + autoBindDLQ(baseQueueName, baseQueueName, requiredGroupName, + producerProperties.getExtension()); + if (producerProperties.getExtension().isBindQueue()) { + Queue queue = new Queue(baseQueueName, true, false, false, queueArgs( + baseQueueName, producerProperties.getExtension(), false)); + declareQueue(baseQueueName, queue); + String[] routingKeys = bindingRoutingKeys(producerProperties.getExtension()); + if (ObjectUtils.isEmpty(routingKeys)) { + binding = notPartitionedBinding(exchange, queue, null, producerProperties.getExtension()); + } + else { + for (String routingKey : routingKeys) { + binding = notPartitionedBinding(exchange, queue, routingKey, + producerProperties.getExtension()); + } + } + } + } + else { + // if the stream is partitioned, create one queue for each target + // partition for the default group + for (int i = 0; i < producerProperties.getPartitionCount(); i++) { + String partitionSuffix = "-" + i; + String partitionQueueName = baseQueueName + partitionSuffix; + autoBindDLQ(baseQueueName, baseQueueName + partitionSuffix, requiredGroupName, + producerProperties.getExtension()); + if (producerProperties.getExtension().isBindQueue()) { + Queue queue = new Queue(partitionQueueName, true, false, false, + queueArgs(partitionQueueName, + producerProperties.getExtension(), false)); + declareQueue(queue.getName(), queue); + String prefix = producerProperties.getExtension().getPrefix(); + String destination = StringUtils.isEmpty(prefix) ? exchangeName + : exchangeName.substring(prefix.length()); + String[] routingKeys = bindingRoutingKeys(producerProperties.getExtension()); + if (ObjectUtils.isEmpty(routingKeys)) { + binding = partitionedBinding(destination, exchange, queue, null, + producerProperties.getExtension(), i); + } + else { + for (String routingKey : routingKeys) { + binding = partitionedBinding(destination, exchange, queue, routingKey, + producerProperties.getExtension(), i); + } + } + } + } + } + } + return new RabbitProducerDestination(exchange, binding, beanNameQualifier); + } + + @Override + public ConsumerDestination provisionConsumerDestination(String name, String group, + ExtendedConsumerProperties properties) { + ConsumerDestination consumerDestination; + if (!properties.isMultiplex()) { + consumerDestination = doProvisionConsumerDestination(name, group, properties); + } + else { + String[] provisionedDestinations = Stream + .of(StringUtils.tokenizeToStringArray(name, ",", true, true)) + .flatMap(destination -> { + if (properties.isPartitioned() && !ObjectUtils.isEmpty(properties.getInstanceIndexList())) { + List consumerDestinationNames = new ArrayList<>(); + + for (Integer index : properties.getInstanceIndexList()) { + ExtendedConsumerProperties temporaryProperties = + new ExtendedConsumerProperties<>(properties.getExtension()); + BeanUtils.copyProperties(properties, temporaryProperties); + temporaryProperties.setInstanceIndex(index); + consumerDestinationNames.add(doProvisionConsumerDestination(destination, group, + temporaryProperties).getName()); + } + + return consumerDestinationNames.stream(); + } + else { + return Stream.of(doProvisionConsumerDestination(destination, group, + properties).getName()); + } + }) + .toArray(String[]::new); + consumerDestination = new RabbitConsumerDestination( + StringUtils.arrayToCommaDelimitedString(provisionedDestinations), + null, group, name); + } + return consumerDestination; + } + + private ConsumerDestination doProvisionConsumerDestination(String name, String group, + ExtendedConsumerProperties properties) { + + boolean anonymous = !StringUtils.hasText(group); + String anonymousGroup = null; + if (anonymous) { + anonymousGroup = new Base64UrlNamingStrategy( + properties.getExtension().getAnonymousGroupPrefix() == null + ? "" + : properties.getExtension().getAnonymousGroupPrefix()).generateName(); + } + String baseQueueName; + if (properties.getExtension().isQueueNameGroupOnly()) { + baseQueueName = anonymous ? anonymousGroup : group; + } + else { + baseQueueName = groupedName(name, anonymous ? anonymousGroup : group); + } + if (this.logger.isInfoEnabled()) { + this.logger.info("declaring queue for inbound: " + baseQueueName + + ", bound to: " + name); + } + String prefix = properties.getExtension().getPrefix(); + final String exchangeName = applyPrefix(prefix, name); + Exchange exchange = buildExchange(properties.getExtension(), exchangeName); + if (properties.getExtension().isDeclareExchange()) { + declareExchange(exchangeName, anonymous ? anonymousGroup : group, exchange); + } + String queueName = applyPrefix(prefix, baseQueueName); + boolean partitioned = !anonymous && properties.isPartitioned(); + boolean durable = !anonymous && properties.getExtension().isDurableSubscription(); + Queue queue; + if (anonymous) { + String anonQueueName = queueName; + queue = new AnonymousQueue((org.springframework.amqp.core.NamingStrategy) () -> anonQueueName, + queueArgs(queueName, properties.getExtension(), false)); + } + else { + if (partitioned) { + String partitionSuffix = "-" + properties.getInstanceIndex(); + queueName += partitionSuffix; + } + if (durable) { + queue = new Queue(queueName, true, false, false, + queueArgs(queueName, properties.getExtension(), false)); + } + else { + queue = new Queue(queueName, false, false, true, + queueArgs(queueName, properties.getExtension(), false)); + } + } + Binding binding = null; + if (properties.getExtension().isBindQueue()) { + if (properties.getExtension().getContainerType().equals(ContainerType.STREAM)) { + queue.getArguments().put("x-queue-type", "stream"); + } + declareQueue(queueName, queue); + String[] routingKeys = bindingRoutingKeys(properties.getExtension()); + if (ObjectUtils.isEmpty(routingKeys)) { + binding = declareConsumerBindings(name, null, properties, exchange, partitioned, queue); + } + else { + for (String routingKey : routingKeys) { + binding = declareConsumerBindings(name, routingKey, properties, exchange, partitioned, queue); + } + } + } + if (durable) { + autoBindDLQ(applyPrefix(properties.getExtension().getPrefix(), baseQueueName), + queueName, group, properties.getExtension()); + } + return new RabbitConsumerDestination(queue.getName(), binding, anonymous ? baseQueueName : group, name); + } + + /** + * Construct a name comprised of the name and group. + * @param name the name. + * @param group the group. + * @return the constructed name. + */ + protected final String groupedName(String name, String group) { + return name + GROUP_INDEX_DELIMITER + + (StringUtils.hasText(group) ? group : "default"); + } + + private Binding declareConsumerBindings(String name, String routingKey, + ExtendedConsumerProperties properties, + Exchange exchange, boolean partitioned, Queue queue) { + + if (partitioned) { + return partitionedBinding(name, exchange, queue, routingKey, properties.getExtension(), + properties.getInstanceIndex()); + } + else { + return notPartitionedBinding(exchange, queue, routingKey, properties.getExtension()); + } + } + + private Binding partitionedBinding(String destination, Exchange exchange, Queue queue, String rk, + RabbitCommonProperties extendedProperties, int index) { + + String bindingKey = rk; + if (bindingKey == null) { + bindingKey = destination; + } + bindingKey += "-" + index; + Map arguments = new HashMap<>(); + arguments.putAll(extendedProperties.getQueueBindingArguments()); + if (exchange instanceof TopicExchange) { + Binding binding = BindingBuilder.bind(queue).to((TopicExchange) exchange) + .with(bindingKey); + declareBinding(queue.getName(), binding); + return binding; + } + else if (exchange instanceof DirectExchange) { + Binding binding = BindingBuilder.bind(queue).to((DirectExchange) exchange) + .with(bindingKey); + declareBinding(queue.getName(), binding); + return binding; + } + else if (exchange instanceof FanoutExchange) { + throw new ProvisioningException( + "A fanout exchange is not appropriate for partitioned apps"); + } + else if (exchange instanceof HeadersExchange) { + Binding binding = new Binding(queue.getName(), DestinationType.QUEUE, exchange.getName(), "", arguments); + declareBinding(queue.getName(), binding); + return binding; + } + else { + throw new ProvisioningException( + "Cannot bind to a " + exchange.getType() + " exchange"); + } + } + + private Binding notPartitionedBinding(Exchange exchange, Queue queue, String rk, + RabbitCommonProperties extendedProperties) { + + String routingKey = rk; + if (routingKey == null) { + routingKey = "#"; + } + Map arguments = new HashMap<>(extendedProperties.getQueueBindingArguments()); + if (exchange instanceof TopicExchange) { + Binding binding = BindingBuilder.bind(queue).to((TopicExchange) exchange) + .with(routingKey); + declareBinding(queue.getName(), binding); + return binding; + } + else if (exchange instanceof DirectExchange) { + Binding binding = BindingBuilder.bind(queue).to((DirectExchange) exchange) + .with(routingKey); + declareBinding(queue.getName(), binding); + return binding; + } + else if (exchange instanceof FanoutExchange) { + Binding binding = BindingBuilder.bind(queue).to((FanoutExchange) exchange); + declareBinding(queue.getName(), binding); + return binding; + } + else if (exchange instanceof HeadersExchange) { + Binding binding = new Binding(queue.getName(), DestinationType.QUEUE, exchange.getName(), "", arguments); + declareBinding(queue.getName(), binding); + return binding; + } + else { + throw new ProvisioningException( + "Cannot bind to a " + exchange.getType() + " exchange"); + } + } + + private String[] bindingRoutingKeys(RabbitCommonProperties extendedProperties) { + /* + * When the delimiter is null, we get a String[1] containing the original. + */ + return StringUtils.delimitedListToStringArray(extendedProperties.getBindingRoutingKey(), + extendedProperties.getBindingRoutingKeyDelimiter()); + } + + /** + * If so requested, declare the DLX/DLQ and bind it. The DLQ is bound to the DLX with + * a routing key of the original queue name because we use default exchange routing by + * queue name for the original message. + * @param baseQueueName The base name for the queue (including the binder prefix, if + * any). + * @param routingKey The routing key for the queue. + * @param group The consumer group. + * @param properties the properties. + */ + private void autoBindDLQ(final String baseQueueName, String routingKey, String group, + RabbitCommonProperties properties) { + boolean autoBindDlq = properties.isAutoBindDlq(); + if (this.logger.isDebugEnabled()) { + this.logger.debug("autoBindDLQ=" + autoBindDlq + " for: " + baseQueueName); + } + if (autoBindDlq) { + String dlqName; + if (properties.getDeadLetterQueueName() == null) { + dlqName = constructDLQName(baseQueueName); + } + else { + dlqName = properties.getDeadLetterQueueName(); + } + Queue dlq = new Queue(dlqName, true, false, false, + queueArgs(dlqName, properties, true)); + declareQueue(dlqName, dlq); + String dlxName = deadLetterExchangeName(properties); + if (properties.isDeclareDlx()) { + declareExchange(dlxName, group, + new ExchangeBuilder(dlxName, + properties.getDeadLetterExchangeType()).durable(true) + .build()); + } + Map arguments = new HashMap<>(properties.getDlqBindingArguments()); + Binding dlqBinding = new Binding(dlq.getName(), DestinationType.QUEUE, + dlxName, properties.getDeadLetterRoutingKey() == null ? routingKey + : properties.getDeadLetterRoutingKey(), + arguments); + declareBinding(dlqName, dlqBinding); + if (properties instanceof RabbitConsumerProperties + && ((RabbitConsumerProperties) properties).isRepublishToDlq()) { + /* + * Also bind with the base queue name when republishToDlq is used, which + * does not know about partitioning + */ + declareBinding(dlqName + ".2", new Binding(dlq.getName(), DestinationType.QUEUE, + dlxName, baseQueueName, arguments)); + } + } + } + + /** + * For binder implementations that support dead lettering, construct the name of the + * dead letter entity for the underlying pipe name. + * @param name the name. + * @return constructDLQName + */ + public static String constructDLQName(String name) { + return name + ".dlq"; + } + + private String deadLetterExchangeName(RabbitCommonProperties properties) { + if (properties.getDeadLetterExchange() == null) { + return properties.getPrefix() + RabbitCommonProperties.DEAD_LETTER_EXCHANGE; + } + else { + return properties.getDeadLetterExchange(); + } + } + + private void declareQueue(String beanName, Queue queueArg) { + Queue queue = queueArg; + for (DeclarableCustomizer customizer : this.customizers) { + queue = (Queue) customizer.apply(queue); + } + try { + this.rabbitAdmin.declareQueue(queue); + } + catch (AmqpConnectException e) { + if (this.logger.isDebugEnabled()) { + this.logger.debug("Declaration of queue: " + queue.getName() + + " deferred - connection not available"); + } + } + catch (RuntimeException e) { + if (this.notOurAdminException) { + this.notOurAdminException = false; + throw e; + } + if (this.logger.isDebugEnabled()) { + this.logger.debug( + "Declaration of queue: " + queue.getName() + " deferred", e); + } + } + addToAutoDeclareContext(beanName, queue); + } + + private Map queueArgs(String queueName, + RabbitCommonProperties properties, boolean isDlq) { + Map args = new HashMap<>(); + if (!isDlq) { + if (properties.isAutoBindDlq()) { + String dlx; + if (properties.getDeadLetterExchange() != null) { + dlx = properties.getDeadLetterExchange(); + } + else { + dlx = applyPrefix(properties.getPrefix(), "DLX"); + } + args.put("x-dead-letter-exchange", dlx); + String dlRk; + if (properties.getDeadLetterRoutingKey() != null) { + dlRk = properties.getDeadLetterRoutingKey(); + } + else { + dlRk = queueName; + } + args.put("x-dead-letter-routing-key", dlRk); + } + } + else { + if (properties.getDlqDeadLetterExchange() != null) { + args.put("x-dead-letter-exchange", properties.getDlqDeadLetterExchange()); + } + if (properties.getDlqDeadLetterRoutingKey() != null) { + args.put("x-dead-letter-routing-key", + properties.getDlqDeadLetterRoutingKey()); + } + } + additionalArgs(args, properties, isDlq); + return args; + } + + private void additionalArgs(Map args, RabbitCommonProperties properties, boolean isDlq) { + Integer expires = isDlq ? properties.getDlqExpires() : properties.getExpires(); + Integer maxLength = isDlq ? properties.getDlqMaxLength() + : properties.getMaxLength(); + Integer maxLengthBytes = isDlq ? properties.getDlqMaxLengthBytes() + : properties.getMaxLengthBytes(); + Integer maxPriority = isDlq ? properties.getDlqMaxPriority() + : properties.getMaxPriority(); + Integer ttl = isDlq ? properties.getDlqTtl() : properties.getTtl(); + boolean lazy = isDlq ? properties.isDlqLazy() : properties.isLazy(); + String overflow = isDlq ? properties.getDlqOverflowBehavior() + : properties.getOverflowBehavior(); + QuorumConfig quorum = isDlq ? properties.getDlqQuorum() : properties.getQuorum(); + boolean singleActive = isDlq ? properties.isDlqSingleActiveConsumer() : properties.isSingleActiveConsumer(); + if (expires != null) { + args.put("x-expires", expires); + } + if (maxLength != null) { + args.put("x-max-length", maxLength); + } + if (maxLengthBytes != null) { + args.put("x-max-length-bytes", maxLengthBytes); + } + if (maxPriority != null) { + args.put("x-max-priority", maxPriority); + } + if (ttl != null) { + args.put("x-message-ttl", ttl); + } + if (lazy) { + args.put("x-queue-mode", "lazy"); + } + if (StringUtils.hasText(overflow)) { + args.put("x-overflow", overflow); + } + if (quorum != null && quorum.isEnabled()) { + args.put("x-queue-type", "quorum"); + if (quorum.getDeliveryLimit() != null) { + args.put("x-delivery-limit", quorum.getDeliveryLimit()); + } + if (quorum.getInitialGroupSize() != null) { + args.put("x-quorum-initial-group-size", quorum.getInitialGroupSize()); + } + } + if (singleActive) { + args.put("x-single-active-consumer", true); + } + } + + public static String applyPrefix(String prefix, String name) { + return prefix + name; + } + + private Exchange buildExchange(RabbitCommonProperties properties, + String exchangeName) { + try { + ExchangeBuilder builder = new ExchangeBuilder(exchangeName, + properties.getExchangeType()); + builder.durable(properties.isExchangeDurable()); + if (properties.isExchangeAutoDelete()) { + builder.autoDelete(); + } + if (properties.isDelayedExchange()) { + builder.delayed(); + } + return builder.build(); + } + catch (Exception e) { + throw new ProvisioningException("Failed to create exchange object", e); + } + } + + private void declareExchange(final String rootName, String group, final Exchange exchangeArg) { + Exchange exchange = exchangeArg; + for (DeclarableCustomizer customizer : this.customizers) { + exchange = (Exchange) customizer.apply(exchange); + } + try { + this.rabbitAdmin.declareExchange(exchange); + } + catch (AmqpConnectException e) { + if (this.logger.isDebugEnabled()) { + this.logger.debug("Declaration of exchange: " + exchange.getName() + + " deferred - connection not available"); + } + } + catch (RuntimeException e) { + if (this.notOurAdminException) { + this.notOurAdminException = false; + throw e; + } + if (this.logger.isDebugEnabled()) { + this.logger.debug( + "Declaration of exchange: " + exchange.getName() + " deferred", + e); + } + } + addToAutoDeclareContext(rootName + "." + group + ".exchange", exchange); + } + + private void addToAutoDeclareContext(String name, Object bean) { + synchronized (this.autoDeclareContext) { + if (!this.autoDeclareContext.containsBean(name)) { + this.autoDeclareContext.getBeanFactory().registerSingleton(name, bean); + } + } + } + + private void declareBinding(String rootName, org.springframework.amqp.core.Binding bindingArg) { + Binding binding = bindingArg; + for (DeclarableCustomizer customizer : this.customizers) { + binding = (Binding) customizer.apply(binding); + } + try { + this.rabbitAdmin.declareBinding(binding); + } + catch (AmqpConnectException e) { + if (this.logger.isDebugEnabled()) { + this.logger.debug("Declaration of binding: " + rootName + + ".binding deferred - connection not available"); + } + } + catch (RuntimeException e) { + if (this.notOurAdminException) { + this.notOurAdminException = false; + throw e; + } + if (this.logger.isDebugEnabled()) { + this.logger.debug( + "Declaration of binding: " + rootName + ".binding deferred", e); + } + } + addToAutoDeclareContext(rootName + ".binding", binding); + } + + public void cleanAutoDeclareContext(ConsumerDestination destination, + ExtendedConsumerProperties consumerProperties) { + + synchronized (this.autoDeclareContext) { + Stream.of(StringUtils.tokenizeToStringArray(destination.getName(), ",", true, + true)).forEach(name -> { + String group = null; + String bindingName = null; + if (destination instanceof RabbitConsumerDestination) { + group = ((RabbitConsumerDestination) destination).getGroup(); + bindingName = ((RabbitConsumerDestination) destination).getBindingName(); + } + RabbitConsumerProperties properties = consumerProperties.getExtension(); + String toRemove = properties.isQueueNameGroupOnly() ? bindingName + "." + group : name.trim(); + boolean partitioned = consumerProperties.isPartitioned(); + if (partitioned) { + toRemove = removePartitionPart(toRemove); + } + removeSingleton(toRemove + ".exchange"); + removeQueueAndBindingBeans(properties, name.trim(), "", group, partitioned); + }); + } + } + + public void cleanAutoDeclareContext(ProducerDestination dest, + ExtendedProducerProperties properties) { + + synchronized (this.autoDeclareContext) { + if (dest instanceof RabbitProducerDestination) { + String qual = ((RabbitProducerDestination) dest).getBeanNameQualifier(); + removeSingleton(dest.getName() + "." + qual + ".exchange"); + String[] requiredGroups = properties.getRequiredGroups(); + if (!ObjectUtils.isEmpty(requiredGroups)) { + for (String group : requiredGroups) { + if (properties.isPartitioned()) { + for (int i = 0; i < properties.getPartitionCount(); i++) { + removeQueueAndBindingBeans(properties.getExtension(), + properties.getExtension().isQueueNameGroupOnly() ? "" : dest.getName(), + group + "-" + i, group, true); + } + } + else { + removeQueueAndBindingBeans(properties.getExtension(), dest.getName() + "." + group, "", + group, false); + } + } + } + } + } + } + + private void removeQueueAndBindingBeans(RabbitCommonProperties properties, String name, String suffix, + String group, boolean partitioned) { + + boolean suffixPresent = StringUtils.hasText(suffix); + String withSuffix = name + (suffixPresent ? ("." + suffix) : ""); + String nameDotOptional = name; + if (!StringUtils.hasText(name)) { + withSuffix = suffix; + } + else { + nameDotOptional = name + "."; + } + removeSingleton(withSuffix + ".binding"); + removeSingleton(withSuffix); + String dlq = (suffixPresent ? nameDotOptional + group : withSuffix) + ".dlq"; // only one DLQ when partitioned + if (StringUtils.hasText(properties.getDeadLetterQueueName())) { + dlq = properties.getDeadLetterQueueName(); + } + else if (partitioned) { + String removedPart = removePartitionPart(dlq); + if (!removedPart.endsWith(".dlq")) { + dlq = removedPart + ".dlq"; + } + } + removeSingleton(dlq + ".binding"); + removeSingleton(dlq + ".2.binding"); + removeSingleton(dlq); + removeSingleton(deadLetterExchangeName(properties) + "." + group + ".exchange"); + } + + private String removePartitionPart(String toRemove) { + int finalHyphen = toRemove.lastIndexOf("-"); + if (finalHyphen > 0) { + return toRemove.substring(0, finalHyphen); + } + return toRemove; + } + + private void removeSingleton(String name) { + if (this.autoDeclareContext.containsBean(name)) { + ConfigurableListableBeanFactory beanFactory = this.autoDeclareContext + .getBeanFactory(); + if (beanFactory instanceof DefaultListableBeanFactory) { + ((DefaultListableBeanFactory) beanFactory).destroySingleton(name); + } + } + } + + @Override + public void onApplicationEvent(DeclarationExceptionEvent event) { + this.notOurAdminException = true; // our admin doesn't have an event publisher + } + + private static final class RabbitProducerDestination implements ProducerDestination { + + private final Exchange exchange; + + private final Binding binding; + + private final String beanNameQualifier; + + RabbitProducerDestination(Exchange exchange, Binding binding, String beanNameQualifier) { + Assert.notNull(exchange, "exchange must not be null"); + this.exchange = exchange; + this.binding = binding; + this.beanNameQualifier = beanNameQualifier; + } + + @Override + public String getName() { + return this.exchange.getName(); + } + + @Override + public String getNameForPartition(int partition) { + return this.exchange.getName(); + } + + @Nullable + String getBeanNameQualifier() { + return this.beanNameQualifier; + } + + @Override + public String toString() { + return "RabbitProducerDestination{" + "exchange=" + this.exchange + ", binding=" + + this.binding + '}'; + } + + } + + private static final class RabbitConsumerDestination implements ConsumerDestination { + + private final String queue; + + private final Binding binding; + + private final String group; + + private final String bindingName; + + RabbitConsumerDestination(String queue, Binding binding, String group, String bindingName) { + Assert.notNull(queue, "queue must not be null"); + this.queue = queue; + this.binding = binding; + this.group = group; + this.bindingName = bindingName; + } + + @Override + public String getName() { + return this.queue; + } + + String getGroup() { + return this.group; + } + + String getBindingName() { + return this.bindingName; + } + + @Override + public String toString() { + return "RabbitConsumerDestination{" + "queue=" + this.queue + ", binding=" + + this.binding + ", group=" + this.group + ", bindingName=" + this.bindingName + '}'; + } + + } + +} diff --git a/r-binder/spring-cloud-stream-binder-rabbit-core/src/test/java/org/springframework/cloud/stream/binder/rabbit/provisioning/RabbitExchangeQueueProvisionerTests.java b/r-binder/spring-cloud-stream-binder-rabbit-core/src/test/java/org/springframework/cloud/stream/binder/rabbit/provisioning/RabbitExchangeQueueProvisionerTests.java new file mode 100644 index 000000000..6ac9a26ff --- /dev/null +++ b/r-binder/spring-cloud-stream-binder-rabbit-core/src/test/java/org/springframework/cloud/stream/binder/rabbit/provisioning/RabbitExchangeQueueProvisionerTests.java @@ -0,0 +1,221 @@ +/* + * Copyright 2022-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.stream.binder.rabbit.provisioning; + +import java.io.IOException; +import java.util.Set; + +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.impl.AMQImpl.Queue.DeclareOk; +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.core.Declarable; +import org.springframework.amqp.rabbit.connection.Connection; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.utils.test.TestUtils; +import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; +import org.springframework.cloud.stream.binder.ExtendedProducerProperties; +import org.springframework.cloud.stream.binder.rabbit.properties.RabbitConsumerProperties; +import org.springframework.cloud.stream.binder.rabbit.properties.RabbitProducerProperties; +import org.springframework.cloud.stream.provisioning.ConsumerDestination; +import org.springframework.cloud.stream.provisioning.ProducerDestination; +import org.springframework.context.ApplicationContext; +import org.springframework.expression.common.LiteralExpression; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.mock; + +/** + * @author Gary Russell + * @since 3.2.3 + * + */ +public class RabbitExchangeQueueProvisionerTests { + + @Test + void consumerDeclarationsWithDlq() throws IOException { + ConnectionFactory cf = mock(ConnectionFactory.class); + Connection conn = mock(Connection.class); + given(cf.createConnection()).willReturn(conn); + Channel channel = mock(Channel.class); + willReturn(new DeclareOk("x", 0, 0)) + .given(channel).queueDeclare(any(), eq(Boolean.TRUE), eq(Boolean.FALSE), eq(Boolean.FALSE), any()); + given(conn.createChannel(anyBoolean())).willReturn(channel); + RabbitExchangeQueueProvisioner provisioner = new RabbitExchangeQueueProvisioner(cf); + + RabbitConsumerProperties props = new RabbitConsumerProperties(); + props.setAutoBindDlq(true); + ExtendedConsumerProperties properties = + new ExtendedConsumerProperties(props); + ConsumerDestination dest = provisioner.provisionConsumerDestination("foo", "group", properties); + ApplicationContext ctx = + TestUtils.getPropertyValue(provisioner, "autoDeclareContext", ApplicationContext.class); + Set declarables = ctx.getBeansOfType(Declarable.class).keySet(); + assertThat(declarables).contains("foo.group.exchange", "foo.group", "foo.group.binding", "foo.group.dlq", + "DLX.group.exchange", "foo.group.dlq.binding", "foo.group.dlq.2.binding"); + provisioner.cleanAutoDeclareContext(dest, properties); + assertThat(ctx.getBeansOfType(Declarable.class)).isEmpty(); + } + + @Test + void consumerDeclarationsWithDlqQueueNameIsGroup() throws IOException { + ConnectionFactory cf = mock(ConnectionFactory.class); + Connection conn = mock(Connection.class); + given(cf.createConnection()).willReturn(conn); + Channel channel = mock(Channel.class); + willReturn(new DeclareOk("x", 0, 0)) + .given(channel).queueDeclare(any(), eq(Boolean.TRUE), eq(Boolean.FALSE), eq(Boolean.FALSE), any()); + given(conn.createChannel(anyBoolean())).willReturn(channel); + RabbitExchangeQueueProvisioner provisioner = new RabbitExchangeQueueProvisioner(cf); + + RabbitConsumerProperties props = new RabbitConsumerProperties(); + props.setAutoBindDlq(true); + props.setQueueNameGroupOnly(true); + ExtendedConsumerProperties properties = + new ExtendedConsumerProperties(props); + ConsumerDestination dest = provisioner.provisionConsumerDestination("fiz", "group", properties); + ApplicationContext ctx = + TestUtils.getPropertyValue(provisioner, "autoDeclareContext", ApplicationContext.class); + Set declarables = ctx.getBeansOfType(Declarable.class).keySet(); + assertThat(declarables).contains("fiz.group.exchange", "group", "group.binding", "group.dlq", + "DLX.group.exchange", "group.dlq.binding", "group.dlq.2.binding"); + provisioner.cleanAutoDeclareContext(dest, properties); + assertThat(ctx.getBeansOfType(Declarable.class)).isEmpty(); + } + + @Test + void producerDeclarationsNoGroups() throws IOException { + ConnectionFactory cf = mock(ConnectionFactory.class); + Connection conn = mock(Connection.class); + given(cf.createConnection()).willReturn(conn); + Channel channel = mock(Channel.class); + willReturn(new DeclareOk("x", 0, 0)) + .given(channel).queueDeclare(any(), eq(Boolean.TRUE), eq(Boolean.FALSE), eq(Boolean.FALSE), any()); + given(conn.createChannel(anyBoolean())).willReturn(channel); + RabbitExchangeQueueProvisioner provisioner = new RabbitExchangeQueueProvisioner(cf); + + RabbitProducerProperties props = new RabbitProducerProperties(); + ExtendedProducerProperties properties = + new ExtendedProducerProperties(props); + ProducerDestination dest = provisioner.provisionProducerDestination("bar", properties); + ApplicationContext ctx = + TestUtils.getPropertyValue(provisioner, "autoDeclareContext", ApplicationContext.class); + Set declarables = ctx.getBeansOfType(Declarable.class).keySet(); + String qual = TestUtils.getPropertyValue(dest, "beanNameQualifier", String.class); + assertThat(declarables).contains("bar." + qual + ".exchange"); + provisioner.cleanAutoDeclareContext(dest, properties); + assertThat(ctx.getBeansOfType(Declarable.class)).isEmpty(); + } + + @Test + void producerDeclarationsWithGroupsAndDlq() throws IOException { + ConnectionFactory cf = mock(ConnectionFactory.class); + Connection conn = mock(Connection.class); + given(cf.createConnection()).willReturn(conn); + Channel channel = mock(Channel.class); + willReturn(new DeclareOk("x", 0, 0)) + .given(channel).queueDeclare(any(), eq(Boolean.TRUE), eq(Boolean.FALSE), eq(Boolean.FALSE), any()); + given(conn.createChannel(anyBoolean())).willReturn(channel); + RabbitExchangeQueueProvisioner provisioner = new RabbitExchangeQueueProvisioner(cf); + + RabbitProducerProperties props = new RabbitProducerProperties(); + props.setAutoBindDlq(true); + ExtendedProducerProperties properties = + new ExtendedProducerProperties(props); + properties.setRequiredGroups("group1", "group2"); + ProducerDestination dest = provisioner.provisionProducerDestination("baz", properties); + ApplicationContext ctx = + TestUtils.getPropertyValue(provisioner, "autoDeclareContext", ApplicationContext.class); + Set declarables = ctx.getBeansOfType(Declarable.class).keySet(); + String qual = TestUtils.getPropertyValue(dest, "beanNameQualifier", String.class); + assertThat(declarables).contains("baz." + qual + ".exchange", "baz.group1", "baz.group1.binding", + "baz.group1.dlq", "DLX.group1.exchange", "baz.group1.dlq.binding", "baz.group2", "baz.group2.binding", + "baz.group2.dlq", "DLX.group2.exchange", "baz.group2.dlq.binding"); + provisioner.cleanAutoDeclareContext(dest, properties); + assertThat(ctx.getBeansOfType(Declarable.class)).isEmpty(); + } + + @Test + void producerDeclarationsWithGroupsAndDlqAndPartitions() throws IOException { + ConnectionFactory cf = mock(ConnectionFactory.class); + Connection conn = mock(Connection.class); + given(cf.createConnection()).willReturn(conn); + Channel channel = mock(Channel.class); + willReturn(new DeclareOk("x", 0, 0)) + .given(channel).queueDeclare(any(), eq(Boolean.TRUE), eq(Boolean.FALSE), eq(Boolean.FALSE), any()); + given(conn.createChannel(anyBoolean())).willReturn(channel); + RabbitExchangeQueueProvisioner provisioner = new RabbitExchangeQueueProvisioner(cf); + + RabbitProducerProperties props = new RabbitProducerProperties(); + props.setAutoBindDlq(true); + ExtendedProducerProperties properties = + new ExtendedProducerProperties(props); + properties.setRequiredGroups("group1", "group2"); + properties.setPartitionKeyExpression(new LiteralExpression("foo")); + properties.setPartitionCount(2); + ProducerDestination dest = provisioner.provisionProducerDestination("qux", properties); + ApplicationContext ctx = + TestUtils.getPropertyValue(provisioner, "autoDeclareContext", ApplicationContext.class); + Set declarables = ctx.getBeansOfType(Declarable.class).keySet(); + String qual = TestUtils.getPropertyValue(dest, "beanNameQualifier", String.class); + assertThat(declarables).contains("qux." + qual + ".exchange", "qux.group1-0", "qux.group1-0.binding", + "qux.group1-1", "qux.group1-1.binding", "qux.group1.dlq", "DLX.group1.exchange", + "qux.group1.dlq.binding", "qux.group2-0", + "qux.group2-0.binding", "qux.group2-1", "qux.group2-1.binding", "qux.group2.dlq", "DLX.group2.exchange", + "qux.group2.dlq.binding"); + provisioner.cleanAutoDeclareContext(dest, properties); + assertThat(ctx.getBeansOfType(Declarable.class)).isEmpty(); + } + + @Test + void producerDeclarationsWithGroupsAndDlqAndPartitionsQueueNameIsGroup() throws IOException { + ConnectionFactory cf = mock(ConnectionFactory.class); + Connection conn = mock(Connection.class); + given(cf.createConnection()).willReturn(conn); + Channel channel = mock(Channel.class); + willReturn(new DeclareOk("x", 0, 0)) + .given(channel).queueDeclare(any(), eq(Boolean.TRUE), eq(Boolean.FALSE), eq(Boolean.FALSE), any()); + given(conn.createChannel(anyBoolean())).willReturn(channel); + RabbitExchangeQueueProvisioner provisioner = new RabbitExchangeQueueProvisioner(cf); + + RabbitProducerProperties props = new RabbitProducerProperties(); + props.setAutoBindDlq(true); + props.setQueueNameGroupOnly(true); + ExtendedProducerProperties properties = + new ExtendedProducerProperties(props); + properties.setRequiredGroups("group1", "group2"); + properties.setPartitionKeyExpression(new LiteralExpression("foo")); + properties.setPartitionCount(2); + ProducerDestination dest = provisioner.provisionProducerDestination("qux", properties); + ApplicationContext ctx = + TestUtils.getPropertyValue(provisioner, "autoDeclareContext", ApplicationContext.class); + Set declarables = ctx.getBeansOfType(Declarable.class).keySet(); + String qual = TestUtils.getPropertyValue(dest, "beanNameQualifier", String.class); + assertThat(declarables).contains("qux." + qual + ".exchange", "group1-0", "group1-0.binding", "group1-1", + "group1-1.binding", "group1.dlq", "DLX.group1.exchange", "group1.dlq.binding", "group2-0", + "group2-0.binding", "group2-1", "group2-1.binding", "group2.dlq", "DLX.group2.exchange", + "group2.dlq.binding"); + provisioner.cleanAutoDeclareContext(dest, properties); + assertThat(ctx.getBeansOfType(Declarable.class)).isEmpty(); + } + +} diff --git a/r-binder/spring-cloud-stream-binder-rabbit-test-support/.jdk8 b/r-binder/spring-cloud-stream-binder-rabbit-test-support/.jdk8 new file mode 100644 index 000000000..e69de29bb diff --git a/r-binder/spring-cloud-stream-binder-rabbit-test-support/pom.xml b/r-binder/spring-cloud-stream-binder-rabbit-test-support/pom.xml new file mode 100644 index 000000000..3096141ea --- /dev/null +++ b/r-binder/spring-cloud-stream-binder-rabbit-test-support/pom.xml @@ -0,0 +1,27 @@ + + + 4.0.0 + + org.springframework.cloud + spring-cloud-stream-binder-rabbit-parent + 3.2.3-SNAPSHOT + + spring-cloud-stream-binder-rabbit-test-support + Rabbit related test classes + + + org.springframework.cloud + spring-cloud-stream-test-support-internal + compile + + + org.springframework.boot + spring-boot-starter-logging + + + org.springframework.boot + spring-boot-starter-amqp + true + + + diff --git a/r-binder/spring-cloud-stream-binder-rabbit-test-support/src/main/java/org/springframework/cloud/stream/binder/test/junit/rabbit/RabbitTestSupport.java b/r-binder/spring-cloud-stream-binder-rabbit-test-support/src/main/java/org/springframework/cloud/stream/binder/test/junit/rabbit/RabbitTestSupport.java new file mode 100644 index 000000000..e44cadf8a --- /dev/null +++ b/r-binder/spring-cloud-stream-binder-rabbit-test-support/src/main/java/org/springframework/cloud/stream/binder/test/junit/rabbit/RabbitTestSupport.java @@ -0,0 +1,180 @@ +/* + * Copyright 2015-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.stream.binder.test.junit.rabbit; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import javax.net.ServerSocketFactory; +import javax.net.SocketFactory; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.cloud.stream.test.junit.AbstractExternalResourceTestSupport; + +/** + * JUnit {@link org.junit.Rule} that detects the fact that RabbitMQ is available on + * localhost. + * + * @author Mark Fisher + * @author Gary Russell + * @author Eric Bottard + */ +public class RabbitTestSupport + extends AbstractExternalResourceTestSupport { + + private final boolean management; + + public RabbitTestSupport() { + this(false); + } + + public RabbitTestSupport(boolean management) { + super("RABBIT"); + this.management = management; + } + + @Override + protected void obtainResource() throws Exception { + resource = new CachingConnectionFactory("localhost"); + resource.createConnection().close(); + if (management) { + Socket socket = SocketFactory.getDefault().createSocket("localhost", 15672); + socket.close(); + } + } + + @Override + protected void cleanupResource() throws Exception { + resource.destroy(); + } + + /** + * Test class to allow testing deferred entity declarations when RabbitMQ is down. + */ + public static class RabbitProxy { + + private static final Log LOGGER = LogFactory.getLog(RabbitProxy.class); + + private final int port; + + private final ExecutorService serverExec = Executors.newSingleThreadExecutor(); + + private final ExecutorService socketExec = Executors.newCachedThreadPool(); + + private volatile ServerSocket serverSocket; + + public RabbitProxy() throws IOException { + ServerSocket serverSocket = ServerSocketFactory.getDefault() + .createServerSocket(0); + this.port = serverSocket.getLocalPort(); + serverSocket.close(); + } + + public int getPort() { + return this.port; + } + + public void start() throws IOException { + this.serverSocket = ServerSocketFactory.getDefault() + .createServerSocket(this.port, 10); + LOGGER.info("Proxy started"); + this.serverExec.execute(new Runnable() { + + @Override + public void run() { + try { + while (true) { + final Socket socket = serverSocket.accept(); + LOGGER.info("Accepted Connection"); + socketExec.execute(new Runnable() { + + @Override + public void run() { + try { + final Socket rabbitSocket = SocketFactory + .getDefault() + .createSocket("localhost", 5672); + socketExec.execute(new Runnable() { + + @Override + public void run() { + LOGGER.info("Running: " + rabbitSocket.getLocalPort()); + try { + InputStream is = rabbitSocket + .getInputStream(); + OutputStream os = socket + .getOutputStream(); + int c; + while ((c = is.read()) >= 0) { + os.write(c); + } + } + catch (IOException e) { + try { + socket.close(); + rabbitSocket.close(); + } + catch (IOException e1) { + } + } + } + }); + InputStream is = socket.getInputStream(); + OutputStream os = rabbitSocket.getOutputStream(); + int c; + while ((c = is.read()) >= 0) { + os.write(c); + } + } + catch (IOException e) { + try { + socket.close(); + } + catch (IOException e1) { + } + } + } + + }); + } + } + catch (IOException e) { + try { + serverSocket.close(); + } + catch (IOException e1) { + } + } + } + }); + } + + public void stop() throws IOException { + this.serverSocket.close(); + } + + } + +} diff --git a/r-binder/spring-cloud-stream-binder-rabbit/.jdk8 b/r-binder/spring-cloud-stream-binder-rabbit/.jdk8 new file mode 100644 index 000000000..e69de29bb diff --git a/r-binder/spring-cloud-stream-binder-rabbit/pom.xml b/r-binder/spring-cloud-stream-binder-rabbit/pom.xml new file mode 100644 index 000000000..ea0c5c47b --- /dev/null +++ b/r-binder/spring-cloud-stream-binder-rabbit/pom.xml @@ -0,0 +1,116 @@ + + + 4.0.0 + + spring-cloud-stream-binder-rabbit + jar + spring-cloud-stream-binder-rabbit + RabbitMQ binder implementation + + + org.springframework.cloud + spring-cloud-stream-binder-rabbit-parent + 3.2.3-SNAPSHOT + + + + + org.springframework.cloud + spring-cloud-function-deployer + ${spring-cloud-function.version} + true + + + org.springframework.cloud + spring-cloud-stream-binder-rabbit-core + ${project.version} + + + org.springframework.boot + spring-boot-starter-actuator + true + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework.cloud + spring-cloud-stream + + + org.springframework.boot + spring-boot-autoconfigure + true + + + org.springframework.cloud + spring-cloud-connectors-core + true + 2.0.7.RELEASE + + + org.springframework.cloud + spring-cloud-spring-service-connector + true + provided + 2.0.7.RELEASE + + + org.springframework.boot + spring-boot-starter-amqp + + + org.springframework.amqp + spring-rabbit-stream + true + + + org.springframework.integration + spring-integration-jmx + + + org.springframework.integration + spring-integration-test + test + + + org.springframework.cloud + spring-cloud-stream-binder-test + test + + + org.springframework.cloud + spring-cloud-stream-test-support-internal + test + + + org.springframework.cloud + spring-cloud-stream-binder-rabbit-test-support + test + + + org.testcontainers + rabbitmq + 1.15.3 + test + + + + org.apache.httpcomponents + httpclient + + + + + + org.springframework.boot + spring-boot-maven-plugin + + exec + + + + + diff --git a/r-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/RabbitExpressionEvaluatingInterceptor.java b/r-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/RabbitExpressionEvaluatingInterceptor.java new file mode 100644 index 000000000..109038cca --- /dev/null +++ b/r-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/RabbitExpressionEvaluatingInterceptor.java @@ -0,0 +1,101 @@ +/* + * Copyright 2017-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.stream.binder.rabbit; + +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.util.Assert; + +/** + * Interceptor to evaluate expressions for outbound messages before serialization. + * + * @author Gary Russell + * @author Oleg Zhurakousky + * @since 2.0 + * + */ +public class RabbitExpressionEvaluatingInterceptor implements ChannelInterceptor { + + /** + * Instance of ExpressionParser. + */ + public static final ExpressionParser PARSER = new SpelExpressionParser(); + + /** + * Default name for routing key header. + */ + public static final String ROUTING_KEY_HEADER = "scst_routingKey"; + + /** + * Default name for delay header. + */ + public static final String DELAY_HEADER = "scst_delay"; + + private final Expression routingKeyExpression; + + private final Expression delayExpression; + + private final EvaluationContext evaluationContext; + + /** + * Construct an instance with the provided expressions and evaluation context. At + * least one expression muse be non-null. + * @param routingKeyExpression the routing key expresssion. + * @param delayExpression the delay expression. + * @param evaluationContext the evaluation context. + */ + public RabbitExpressionEvaluatingInterceptor(Expression routingKeyExpression, + Expression delayExpression, EvaluationContext evaluationContext) { + Assert.isTrue(routingKeyExpression != null || delayExpression != null, + "At least one expression is required"); + Assert.notNull(evaluationContext, "the 'evaluationContext' cannot be null"); + if (routingKeyExpression != null) { + this.routingKeyExpression = routingKeyExpression; + } + else { + this.routingKeyExpression = null; + } + if (delayExpression != null) { + this.delayExpression = delayExpression; + } + else { + this.delayExpression = null; + } + this.evaluationContext = evaluationContext; + } + + @Override + public Message preSend(Message message, MessageChannel channel) { + MessageBuilder builder = MessageBuilder.fromMessage(message); + if (this.routingKeyExpression != null) { + builder.setHeader(ROUTING_KEY_HEADER, + this.routingKeyExpression.getValue(this.evaluationContext, message)); + } + if (this.delayExpression != null) { + builder.setHeader(DELAY_HEADER, + this.delayExpression.getValue(this.evaluationContext, message)); + } + return builder.build(); + } + +} diff --git a/r-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/RabbitMessageChannelBinder.java b/r-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/RabbitMessageChannelBinder.java new file mode 100644 index 000000000..6b4fd1633 --- /dev/null +++ b/r-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/RabbitMessageChannelBinder.java @@ -0,0 +1,1064 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.stream.binder.rabbit; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.regex.Pattern; + +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Envelope; + +import org.springframework.amqp.AmqpRejectAndDontRequeueException; +import org.springframework.amqp.ImmediateAcknowledgeAmqpException; +import org.springframework.amqp.core.AcknowledgeMode; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessagePostProcessor; +import org.springframework.amqp.core.MessageProperties; +import org.springframework.amqp.rabbit.batch.BatchingStrategy; +import org.springframework.amqp.rabbit.batch.SimpleBatchingStrategy; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory.ConfirmType; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.connection.CorrelationData; +import org.springframework.amqp.rabbit.connection.CorrelationData.Confirm; +import org.springframework.amqp.rabbit.connection.LocalizedQueueConnectionFactory; +import org.springframework.amqp.rabbit.connection.RabbitUtils; +import org.springframework.amqp.rabbit.core.BatchingRabbitTemplate; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer; +import org.springframework.amqp.rabbit.listener.DirectMessageListenerContainer; +import org.springframework.amqp.rabbit.listener.MessageListenerContainer; +import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; +import org.springframework.amqp.rabbit.retry.RejectAndDontRequeueRecoverer; +import org.springframework.amqp.rabbit.retry.RepublishMessageRecoverer; +import org.springframework.amqp.rabbit.support.DefaultMessagePropertiesConverter; +import org.springframework.amqp.rabbit.support.ListenerExecutionFailedException; +import org.springframework.amqp.rabbit.support.MessagePropertiesConverter; +import org.springframework.amqp.support.AmqpHeaders; +import org.springframework.amqp.support.converter.AbstractMessageConverter; +import org.springframework.amqp.support.converter.MessageConversionException; +import org.springframework.amqp.support.converter.SimpleMessageConverter; +import org.springframework.amqp.support.postprocessor.DelegatingDecompressingPostProcessor; +import org.springframework.amqp.support.postprocessor.GZipPostProcessor; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.boot.autoconfigure.amqp.RabbitProperties; +import org.springframework.boot.autoconfigure.amqp.RabbitProperties.Retry; +import org.springframework.cloud.stream.binder.AbstractMessageChannelBinder; +import org.springframework.cloud.stream.binder.BinderHeaders; +import org.springframework.cloud.stream.binder.BinderSpecificPropertiesProvider; +import org.springframework.cloud.stream.binder.DefaultPollableMessageSource; +import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; +import org.springframework.cloud.stream.binder.ExtendedProducerProperties; +import org.springframework.cloud.stream.binder.ExtendedPropertiesBinder; +import org.springframework.cloud.stream.binder.HeaderMode; +import org.springframework.cloud.stream.binder.rabbit.properties.RabbitCommonProperties; +import org.springframework.cloud.stream.binder.rabbit.properties.RabbitConsumerProperties; +import org.springframework.cloud.stream.binder.rabbit.properties.RabbitConsumerProperties.ContainerType; +import org.springframework.cloud.stream.binder.rabbit.properties.RabbitExtendedBindingProperties; +import org.springframework.cloud.stream.binder.rabbit.properties.RabbitProducerProperties; +import org.springframework.cloud.stream.binder.rabbit.properties.RabbitProducerProperties.ProducerType; +import org.springframework.cloud.stream.binder.rabbit.provisioning.RabbitExchangeQueueProvisioner; +import org.springframework.cloud.stream.config.ListenerContainerCustomizer; +import org.springframework.cloud.stream.config.MessageSourceCustomizer; +import org.springframework.cloud.stream.provisioning.ConsumerDestination; +import org.springframework.cloud.stream.provisioning.ProducerDestination; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.expression.Expression; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.integration.IntegrationMessageHeaderAccessor; +import org.springframework.integration.StaticMessageHeaderAccessor; +import org.springframework.integration.acks.AcknowledgmentCallback; +import org.springframework.integration.acks.AcknowledgmentCallback.Status; +import org.springframework.integration.amqp.inbound.AmqpInboundChannelAdapter; +import org.springframework.integration.amqp.inbound.AmqpInboundChannelAdapter.BatchMode; +import org.springframework.integration.amqp.inbound.AmqpMessageSource; +import org.springframework.integration.amqp.outbound.AmqpOutboundEndpoint; +import org.springframework.integration.amqp.support.AmqpHeaderMapper; +import org.springframework.integration.amqp.support.AmqpMessageHeaderErrorMessageStrategy; +import org.springframework.integration.amqp.support.DefaultAmqpHeaderMapper; +import org.springframework.integration.channel.AbstractMessageChannel; +import org.springframework.integration.channel.DirectChannel; +import org.springframework.integration.context.IntegrationContextUtils; +import org.springframework.integration.core.MessageProducer; +import org.springframework.integration.support.DefaultErrorMessageStrategy; +import org.springframework.integration.support.ErrorMessageStrategy; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.MessageHandler; +import org.springframework.messaging.MessagingException; +import org.springframework.messaging.support.ErrorMessage; +import org.springframework.retry.RetryPolicy; +import org.springframework.retry.backoff.ExponentialBackOffPolicy; +import org.springframework.retry.policy.SimpleRetryPolicy; +import org.springframework.retry.support.RetryTemplate; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * A {@link org.springframework.cloud.stream.binder.Binder} implementation backed by + * RabbitMQ. + * + * @author Mark Fisher + * @author Gary Russell + * @author Jennifer Hickey + * @author Gunnar Hillert + * @author Ilayaperumal Gopinathan + * @author David Turanski + * @author Marius Bogoevici + * @author Artem Bilan + * @author Soby Chacko + * @author Oleg Zhurakousky + */ +// @checkstyle:off +public class RabbitMessageChannelBinder extends + AbstractMessageChannelBinder, ExtendedProducerProperties, RabbitExchangeQueueProvisioner> + implements + ExtendedPropertiesBinder, + DisposableBean { + + private static final SimplePassthroughMessageConverter passThoughConverter = new SimplePassthroughMessageConverter(); + + private static final AmqpMessageHeaderErrorMessageStrategy errorMessageStrategy = new AmqpMessageHeaderErrorMessageStrategy(); + + private static final MessagePropertiesConverter inboundMessagePropertiesConverter = new DefaultMessagePropertiesConverter() { + @Override + public MessageProperties toMessageProperties(AMQP.BasicProperties source, + Envelope envelope, String charset) { + MessageProperties properties = super.toMessageProperties(source, envelope, + charset); + properties.setDeliveryMode(null); + return properties; + } + }; + + private static final Pattern interceptorNeededPattern = Pattern.compile("(payload|#root|#this)"); + + // @checkstyle:on + + private final RabbitProperties rabbitProperties; + + private boolean destroyConnectionFactory; + + private ConnectionFactory connectionFactory; + + private MessagePostProcessor decompressingPostProcessor = new DelegatingDecompressingPostProcessor(); + + private MessagePostProcessor compressingPostProcessor = new GZipPostProcessor(); + + private volatile String[] adminAddresses; + + private volatile String[] nodes; + + private volatile boolean clustered; + + private RabbitExtendedBindingProperties extendedBindingProperties = new RabbitExtendedBindingProperties(); + + public RabbitMessageChannelBinder(ConnectionFactory connectionFactory, + RabbitProperties rabbitProperties, + RabbitExchangeQueueProvisioner provisioningProvider) { + + this(connectionFactory, rabbitProperties, provisioningProvider, null, null); + } + + public RabbitMessageChannelBinder(ConnectionFactory connectionFactory, + RabbitProperties rabbitProperties, + RabbitExchangeQueueProvisioner provisioningProvider, + ListenerContainerCustomizer containerCustomizer) { + + this(connectionFactory, rabbitProperties, provisioningProvider, containerCustomizer, null); + } + + public RabbitMessageChannelBinder(ConnectionFactory connectionFactory, + RabbitProperties rabbitProperties, + RabbitExchangeQueueProvisioner provisioningProvider, + ListenerContainerCustomizer containerCustomizer, + MessageSourceCustomizer sourceCustomizer) { + + super(new String[0], provisioningProvider, containerCustomizer, sourceCustomizer); + Assert.notNull(connectionFactory, "connectionFactory must not be null"); + Assert.notNull(rabbitProperties, "rabbitProperties must not be null"); + this.connectionFactory = connectionFactory; + this.rabbitProperties = rabbitProperties; + } + + /** + * Set a {@link MessagePostProcessor} to decompress messages. Defaults to a + * {@link DelegatingDecompressingPostProcessor} with its default delegates. + * @param decompressingPostProcessor the post processor. + */ + public void setDecompressingPostProcessor( + MessagePostProcessor decompressingPostProcessor) { + this.decompressingPostProcessor = decompressingPostProcessor; + } + + /** + * Set a {@link org.springframework.amqp.core.MessagePostProcessor} to compress + * messages. Defaults to a + * {@link org.springframework.amqp.support.postprocessor.GZipPostProcessor}. + * @param compressingPostProcessor the post processor. + */ + public void setCompressingPostProcessor( + MessagePostProcessor compressingPostProcessor) { + this.compressingPostProcessor = compressingPostProcessor; + } + + public void setAdminAddresses(String[] adminAddresses) { + this.adminAddresses = Arrays.copyOf(adminAddresses, adminAddresses.length); + } + + public void setNodes(String[] nodes) { + this.nodes = Arrays.copyOf(nodes, nodes.length); + this.clustered = nodes.length > 1; + } + + public void setExtendedBindingProperties( + RabbitExtendedBindingProperties extendedBindingProperties) { + this.extendedBindingProperties = extendedBindingProperties; + } + + @Override + public void onInit() throws Exception { + super.onInit(); + if (this.clustered) { + String[] addresses = StringUtils.commaDelimitedListToStringArray( + this.rabbitProperties.getAddresses()); + + Assert.state( + addresses.length == this.adminAddresses.length + && addresses.length == this.nodes.length, + "'addresses', 'adminAddresses', and 'nodes' properties must have equal length"); + + this.connectionFactory = new LocalizedQueueConnectionFactory( + this.connectionFactory, addresses, this.adminAddresses, this.nodes, + this.rabbitProperties.getVirtualHost(), + this.rabbitProperties.getUsername(), + this.rabbitProperties.getPassword(), + this.rabbitProperties.getSsl().getEnabled(), + this.rabbitProperties.getSsl().getKeyStore(), + this.rabbitProperties.getSsl().getTrustStore(), + this.rabbitProperties.getSsl().getKeyStorePassword(), + this.rabbitProperties.getSsl().getTrustStorePassword()); + this.destroyConnectionFactory = true; + } + } + + @Override + public void destroy() throws Exception { + if (this.connectionFactory instanceof DisposableBean) { + if (this.destroyConnectionFactory) { + ((DisposableBean) this.connectionFactory).destroy(); + } + } + } + + @Override + public RabbitConsumerProperties getExtendedConsumerProperties(String channelName) { + return this.extendedBindingProperties.getExtendedConsumerProperties(channelName); + } + + @Override + public RabbitProducerProperties getExtendedProducerProperties(String channelName) { + return this.extendedBindingProperties.getExtendedProducerProperties(channelName); + } + + @Override + public String getDefaultsPrefix() { + return this.extendedBindingProperties.getDefaultsPrefix(); + } + + @Override + public Class getExtendedPropertiesEntryClass() { + return this.extendedBindingProperties.getExtendedPropertiesEntryClass(); + } + + @Override + protected MessageHandler createProducerMessageHandler( + final ProducerDestination producerDestination, + ExtendedProducerProperties producerProperties, + MessageChannel errorChannel) { + Assert.state( + !HeaderMode.embeddedHeaders.equals(producerProperties.getHeaderMode()), + "the RabbitMQ binder does not support embedded headers since RabbitMQ supports headers natively"); + String prefix = producerProperties.getExtension().getPrefix(); + String exchangeName = producerDestination.getName(); + String destination = StringUtils.isEmpty(prefix) ? exchangeName + : exchangeName.substring(prefix.length()); + RabbitProducerProperties extendedProperties = producerProperties.getExtension(); + final MessageHandler endpoint; + if (!ProducerType.AMQP.equals(producerProperties.getExtension().getProducerType())) { + endpoint = StreamUtils.createStreamMessageHandler(producerDestination, producerProperties, errorChannel, + destination, extendedProperties, getApplicationContext(), this::configureHeaderMapper); + } + else { + endpoint = amqpHandler(producerDestination, producerProperties, errorChannel, + destination, extendedProperties); + } + return endpoint; + } + + private AmqpOutboundEndpoint amqpHandler(final ProducerDestination producerDestination, + ExtendedProducerProperties producerProperties, MessageChannel errorChannel, + String destination, RabbitProducerProperties extendedProperties) { + final AmqpOutboundEndpoint endpoint = new AmqpOutboundEndpoint( + buildRabbitTemplate(extendedProperties, + errorChannel != null || extendedProperties.isUseConfirmHeader())); + endpoint.setExchangeName(producerDestination.getName()); + boolean expressionInterceptorNeeded = expressionInterceptorNeeded( + extendedProperties); + Expression routingKeyExpression = extendedProperties.getRoutingKeyExpression(); + if (!producerProperties.isPartitioned()) { + if (routingKeyExpression == null) { + endpoint.setRoutingKey(destination); + } + else { + if (expressionInterceptorNeeded) { + endpoint.setRoutingKeyExpressionString("headers['" + + RabbitExpressionEvaluatingInterceptor.ROUTING_KEY_HEADER + + "']"); + } + else { + endpoint.setRoutingKeyExpression(routingKeyExpression); + } + } + } + else { + if (routingKeyExpression == null) { + endpoint.setRoutingKeyExpression( + buildPartitionRoutingExpression(destination, false)); + } + else { + if (expressionInterceptorNeeded) { + endpoint.setRoutingKeyExpression( + buildPartitionRoutingExpression("headers['" + + RabbitExpressionEvaluatingInterceptor.ROUTING_KEY_HEADER + + "']", true)); + } + else { + endpoint.setRoutingKeyExpression(buildPartitionRoutingExpression( + routingKeyExpression.getExpressionString(), true)); + } + } + } + if (extendedProperties.getDelayExpression() != null) { + if (expressionInterceptorNeeded) { + endpoint.setDelayExpressionString("headers['" + + RabbitExpressionEvaluatingInterceptor.DELAY_HEADER + "']"); + } + else { + endpoint.setDelayExpression(extendedProperties.getDelayExpression()); + } + } + endpoint.setHeaderMapper(configureHeaderMapper(extendedProperties)); + endpoint.setDefaultDeliveryMode(extendedProperties.getDeliveryMode()); + endpoint.setBeanFactory(this.getBeanFactory()); + if (errorChannel != null) { + checkConnectionFactoryIsErrorCapable(); + endpoint.setReturnChannel(errorChannel); + if (!extendedProperties.isUseConfirmHeader()) { + endpoint.setConfirmNackChannel(errorChannel); + String ackChannelBeanName = StringUtils + .hasText(extendedProperties.getConfirmAckChannel()) + ? extendedProperties.getConfirmAckChannel() + : IntegrationContextUtils.NULL_CHANNEL_BEAN_NAME; + if (!ackChannelBeanName.equals(IntegrationContextUtils.NULL_CHANNEL_BEAN_NAME) + && !getApplicationContext().containsBean(ackChannelBeanName)) { + GenericApplicationContext context = (GenericApplicationContext) getApplicationContext(); + context.registerBean(ackChannelBeanName, DirectChannel.class, + () -> new DirectChannel()); + } + endpoint.setConfirmAckChannelName(ackChannelBeanName); + endpoint.setConfirmCorrelationExpressionString("#root"); + } + else { + Assert.state(!StringUtils.hasText(extendedProperties.getConfirmAckChannel()), + "You cannot specify a 'confirmAckChannel' when 'useConfirmHeader' is true"); + } + endpoint.setErrorMessageStrategy(new DefaultErrorMessageStrategy()); + } + endpoint.setHeadersMappedLast(true); + return endpoint; + } + + private AmqpHeaderMapper configureHeaderMapper(RabbitProducerProperties extendedProperties) { + DefaultAmqpHeaderMapper mapper = DefaultAmqpHeaderMapper.outboundMapper(); + List headerPatterns = new ArrayList<>(extendedProperties.getHeaderPatterns().length + 3); + headerPatterns.add("!" + BinderHeaders.PARTITION_HEADER); + headerPatterns.add("!" + IntegrationMessageHeaderAccessor.SOURCE_DATA); + headerPatterns.add("!" + IntegrationMessageHeaderAccessor.DELIVERY_ATTEMPT); + headerPatterns.add("!rabbitmq_streamContext"); + headerPatterns.addAll(Arrays.asList(extendedProperties.getHeaderPatterns())); + mapper.setRequestHeaderNames( + headerPatterns.toArray(new String[headerPatterns.size()])); + return mapper; + } + + @Override + protected void postProcessOutputChannel(MessageChannel outputChannel, + ExtendedProducerProperties producerProperties) { + RabbitProducerProperties extendedProperties = producerProperties.getExtension(); + if (expressionInterceptorNeeded(extendedProperties)) { + ((AbstractMessageChannel) outputChannel).addInterceptor(0, + new RabbitExpressionEvaluatingInterceptor( + extendedProperties.getRoutingKeyExpression(), + extendedProperties.getDelayExpression(), + getEvaluationContext())); + } + } + + private boolean expressionInterceptorNeeded(RabbitProducerProperties extendedProperties) { + Expression rkExpression = extendedProperties.getRoutingKeyExpression(); + Expression delayExpression = extendedProperties.getDelayExpression(); + return (rkExpression != null && interceptorNeededPattern.matcher(rkExpression.getExpressionString()).find()) + || (delayExpression != null + && interceptorNeededPattern.matcher(delayExpression.getExpressionString()).find()); + } + + private void checkConnectionFactoryIsErrorCapable() { + if (!(this.connectionFactory instanceof CachingConnectionFactory)) { + logger.warn( + "Unknown connection factory type, cannot determine error capabilities: " + + this.connectionFactory.getClass()); + } + else { + CachingConnectionFactory ccf = (CachingConnectionFactory) this.connectionFactory; + if (!ccf.isPublisherConfirms() && !ccf.isPublisherReturns()) { + logger.warn( + "Producer error channel is enabled, but the connection factory is not configured for " + + "returns or confirms; the error channel will receive no messages"); + } + else if (!ccf.isPublisherConfirms()) { + logger.info( + "Producer error channel is enabled, but the connection factory is only configured to " + + "handle returned messages; negative acks will not be reported"); + } + else if (!ccf.isPublisherReturns()) { + logger.info( + "Producer error channel is enabled, but the connection factory is only configured to " + + "handle negatively acked messages; returned messages will not be reported"); + } + } + } + + private Expression buildPartitionRoutingExpression(String expressionRoot, + boolean rootIsExpression) { + String partitionRoutingExpression = rootIsExpression + ? expressionRoot + " + '-' + headers['" + BinderHeaders.PARTITION_HEADER + + "']" + : "'" + expressionRoot + "-' + headers['" + BinderHeaders.PARTITION_HEADER + + "']"; + return new SpelExpressionParser().parseExpression(partitionRoutingExpression); + } + + @Override + protected MessageProducer createConsumerEndpoint( + ConsumerDestination consumerDestination, String group, + ExtendedConsumerProperties properties) { + Assert.state(!HeaderMode.embeddedHeaders.equals(properties.getHeaderMode()), + "the RabbitMQ binder does not support embedded headers since RabbitMQ supports headers natively"); + String destination = consumerDestination.getName(); + RabbitConsumerProperties extension = properties.getExtension(); + MessageListenerContainer listenerContainer = createAndConfigureContainer(consumerDestination, group, + properties, destination, extension); + String[] queues = StringUtils.tokenizeToStringArray(destination, ",", true, true); + listenerContainer.setQueueNames(queues); + getContainerCustomizer().configure(listenerContainer, + consumerDestination.getName(), group); + listenerContainer.afterPropertiesSet(); + + AmqpInboundChannelAdapter adapter = new AmqpInboundChannelAdapter(listenerContainer); + adapter.setBindSourceMessage(true); + adapter.setBeanFactory(this.getBeanFactory()); + adapter.setBeanName("inbound." + destination); + DefaultAmqpHeaderMapper mapper = DefaultAmqpHeaderMapper.inboundMapper(); + mapper.setRequestHeaderNames(extension.getHeaderPatterns()); + adapter.setHeaderMapper(mapper); + ErrorInfrastructure errorInfrastructure = registerErrorInfrastructure( + consumerDestination, group, properties); + if (properties.getMaxAttempts() > 1) { + adapter.setRetryTemplate(buildRetryTemplate(properties)); + adapter.setRecoveryCallback(errorInfrastructure.getRecoverer()); + } + else { + adapter.setErrorMessageStrategy(errorMessageStrategy); + adapter.setErrorChannel(errorInfrastructure.getErrorChannel()); + } + adapter.setMessageConverter(passThoughConverter); + if (properties.isBatchMode() && extension.isEnableBatching() + && ContainerType.SIMPLE.equals(extension.getContainerType())) { + adapter.setBatchMode(BatchMode.EXTRACT_PAYLOADS_WITH_HEADERS); + } + if (extension.getContainerType().equals(ContainerType.STREAM)) { + StreamUtils.configureAdapter(adapter); + } + return adapter; + } + + private MessageListenerContainer createAndConfigureContainer(ConsumerDestination consumerDestination, + String group, ExtendedConsumerProperties properties, String destination, + RabbitConsumerProperties extension) { + + if (extension.getContainerType().equals(ContainerType.STREAM)) { + return StreamUtils.createContainer(consumerDestination, group, properties, destination, extension, + getApplicationContext()); + } + boolean directContainer = extension.getContainerType() + .equals(ContainerType.DIRECT); + AbstractMessageListenerContainer listenerContainer = directContainer + ? new DirectMessageListenerContainer(this.connectionFactory) + : new SimpleMessageListenerContainer(this.connectionFactory); + listenerContainer.setBeanName(consumerDestination.getName() + "." + group + ".container"); + listenerContainer + .setAcknowledgeMode(extension.getAcknowledgeMode()); + listenerContainer.setChannelTransacted(extension.isTransacted()); + listenerContainer + .setDefaultRequeueRejected(extension.isRequeueRejected()); + int concurrency = properties.getConcurrency(); + concurrency = concurrency > 0 ? concurrency : 1; + if (directContainer) { + setDMLCProperties(properties, + (DirectMessageListenerContainer) listenerContainer, concurrency); + } + else { + setSMLCProperties(properties, + (SimpleMessageListenerContainer) listenerContainer, concurrency); + } + listenerContainer.setPrefetchCount(extension.getPrefetch()); + listenerContainer + .setRecoveryInterval(extension.getRecoveryInterval()); + listenerContainer.setTaskExecutor( + new SimpleAsyncTaskExecutor(consumerDestination.getName() + "-")); + listenerContainer.setAfterReceivePostProcessors(this.decompressingPostProcessor); + listenerContainer.setMessagePropertiesConverter( + RabbitMessageChannelBinder.inboundMessagePropertiesConverter); + listenerContainer.setExclusive(extension.isExclusive()); + listenerContainer + .setMissingQueuesFatal(extension.getMissingQueuesFatal()); + if (extension.getFailedDeclarationRetryInterval() != null) { + listenerContainer.setFailedDeclarationRetryInterval( + extension.getFailedDeclarationRetryInterval()); + } + if (getApplicationEventPublisher() != null) { + listenerContainer + .setApplicationEventPublisher(getApplicationEventPublisher()); + } + else if (getApplicationContext() != null) { + listenerContainer.setApplicationEventPublisher(getApplicationContext()); + } + if (StringUtils.hasText(extension.getConsumerTagPrefix())) { + final AtomicInteger index = new AtomicInteger(); + listenerContainer.setConsumerTagStrategy( + q -> extension.getConsumerTagPrefix() + "#" + + index.getAndIncrement()); + } + return listenerContainer; + } + + private void setSMLCProperties( + ExtendedConsumerProperties properties, + SimpleMessageListenerContainer listenerContainer, int concurrency) { + + RabbitConsumerProperties extension = properties.getExtension(); + listenerContainer.setConcurrentConsumers(concurrency); + int maxConcurrency = extension.getMaxConcurrency(); + if (maxConcurrency > concurrency) { + listenerContainer.setMaxConcurrentConsumers(maxConcurrency); + } + listenerContainer.setDeBatchingEnabled(!properties.isBatchMode()); + listenerContainer.setBatchSize(extension.getBatchSize()); + if (extension.getQueueDeclarationRetries() != null) { + listenerContainer.setDeclarationRetries( + extension.getQueueDeclarationRetries()); + } + if (properties.isBatchMode() && extension.isEnableBatching()) { + listenerContainer.setConsumerBatchEnabled(true); + listenerContainer.setDeBatchingEnabled(true); + } + if (extension.getReceiveTimeout() != null) { + listenerContainer.setReceiveTimeout(extension.getReceiveTimeout()); + } + } + + private void setDMLCProperties( + ExtendedConsumerProperties properties, + DirectMessageListenerContainer listenerContainer, int concurrency) { + + listenerContainer.setConsumersPerQueue(concurrency); + if (properties.getExtension().getMaxConcurrency() > concurrency) { + this.logger + .warn("maxConcurrency is not supported by the direct container type"); + } + if (properties.getExtension().getBatchSize() > 1) { + this.logger.warn("batchSize is not supported by the direct container type"); + } + if (properties.getExtension().getQueueDeclarationRetries() != null) { + this.logger.warn( + "queueDeclarationRetries is not supported by the direct container type"); + } + } + + @Override + protected PolledConsumerResources createPolledConsumerResources(String name, + String group, ConsumerDestination destination, + ExtendedConsumerProperties consumerProperties) { + + Assert.isTrue(!consumerProperties.isMultiplex(), + "The Spring Integration polled MessageSource does not currently support muiltiple queues"); + AmqpMessageSource source = new AmqpMessageSource(this.connectionFactory, + destination.getName()); + source.setRawMessageHeader(true); + getMessageSourceCustomizer().configure(source, destination.getName(), group); + return new PolledConsumerResources(source, registerErrorInfrastructure( + destination, group, consumerProperties, true)); + } + + @Override + protected void postProcessPollableSource(DefaultPollableMessageSource bindingTarget) { + bindingTarget.setAttributesProvider((accessor, message) -> { + Object rawMessage = message.getHeaders() + .get(AmqpMessageHeaderErrorMessageStrategy.AMQP_RAW_MESSAGE); + if (rawMessage != null) { + accessor.setAttribute( + AmqpMessageHeaderErrorMessageStrategy.AMQP_RAW_MESSAGE, + rawMessage); + } + }); + } + + @Override + protected ErrorMessageStrategy getErrorMessageStrategy() { + return errorMessageStrategy; + } + + @Override + protected MessageHandler getErrorMessageHandler(ConsumerDestination destination, + String group, + final ExtendedConsumerProperties properties) { + + if (properties.getExtension().isRepublishToDlq()) { + return new MessageHandler() { + + private static final long ACK_TIMEOUT = 10_000; + + private final RabbitTemplate template = new RabbitTemplate( + RabbitMessageChannelBinder.this.connectionFactory); + + private final ConfirmType confirmType; + + + { + this.template.setUsePublisherConnection(true); + this.template.setChannelTransacted(properties.getExtension().isTransacted()); + this.template.setMandatory(RabbitMessageChannelBinder.this.connectionFactory.isPublisherReturns()); + if (RabbitMessageChannelBinder.this.connectionFactory.isSimplePublisherConfirms()) { + this.confirmType = ConfirmType.SIMPLE; + } + else if (RabbitMessageChannelBinder.this.connectionFactory.isPublisherConfirms()) { + this.confirmType = ConfirmType.CORRELATED; + } + else { + this.confirmType = ConfirmType.NONE; + } + } + + private final String exchange = deadLetterExchangeName(properties.getExtension()); + + private final String routingKey = properties.getExtension() + .getDeadLetterRoutingKey(); + + private final int frameMaxHeadroom = properties.getExtension() + .getFrameMaxHeadroom(); + + private int maxStackTraceLength = -1; + + private Boolean dlxPresent; + + @Override + public void handleMessage(org.springframework.messaging.Message message) throws MessagingException { + Message amqpMessage = StaticMessageHeaderAccessor.getSourceData(message); + + if (!(message instanceof ErrorMessage)) { + logger.error("Expected an ErrorMessage, not a " + + message.getClass().toString() + " for: " + message); + } + else if (amqpMessage == null) { + logger.error("No raw message header in " + message); + } + else { + if (!checkDlx()) { + return; + } + Throwable cause = (Throwable) message.getPayload(); + if (!shouldRepublish(cause)) { + if (logger.isDebugEnabled()) { + logger.debug("Skipping republish of: " + message); + } + return; + } + MessageProperties messageProperties = amqpMessage + .getMessageProperties(); + Map headers = messageProperties.getHeaders(); + String stackTraceAsString = getStackTraceAsString(cause); + if (this.maxStackTraceLength < 0) { + int rabbitMaxStackTraceLength = RabbitUtils + .getMaxFrame(this.template.getConnectionFactory()); + if (rabbitMaxStackTraceLength > 0) { + // maxStackTraceLength -= this.frameMaxHeadroom; + this.maxStackTraceLength = rabbitMaxStackTraceLength + - this.frameMaxHeadroom; + } + } + if (this.maxStackTraceLength > 0 && stackTraceAsString + .length() > this.maxStackTraceLength) { + stackTraceAsString = stackTraceAsString.substring(0, + this.maxStackTraceLength); + logger.warn( + "Stack trace in republished message header truncated due to frame_max limitations; " + + "consider increasing frame_max on the broker or reduce the stack trace depth", + cause); + } + headers.put(RepublishMessageRecoverer.X_EXCEPTION_STACKTRACE, + stackTraceAsString); + headers.put(RepublishMessageRecoverer.X_EXCEPTION_MESSAGE, + cause.getCause() != null ? cause.getCause().getMessage() + : cause.getMessage()); + headers.put(RepublishMessageRecoverer.X_ORIGINAL_EXCHANGE, + messageProperties.getReceivedExchange()); + headers.put(RepublishMessageRecoverer.X_ORIGINAL_ROUTING_KEY, + messageProperties.getReceivedRoutingKey()); + if (properties.getExtension().getRepublishDeliveyMode() != null) { + messageProperties.setDeliveryMode( + properties.getExtension().getRepublishDeliveyMode()); + } + doSend(this.exchange, + this.routingKey != null ? this.routingKey + : messageProperties.getConsumerQueue(), + amqpMessage); + if (properties.getExtension().getAcknowledgeMode().equals(AcknowledgeMode.MANUAL)) { + org.springframework.messaging.Message original = + ((ErrorMessage) message).getOriginalMessage(); + if (original != null) { + // If we are using manual acks, ack the original message. + try { + original.getHeaders().get(AmqpHeaders.CHANNEL, Channel.class) + .basicAck(original.getHeaders() + .get(AmqpHeaders.DELIVERY_TAG, Long.class), false); + } + catch (IOException e) { + logger.debug("Failed to ack original message", e); + } + } + } + } + } + + private void doSend(String exchange, String routingKey, Message amqpMessage) { + if (ConfirmType.SIMPLE.equals(this.confirmType)) { + this.template.invoke(temp -> { + temp.send(exchange, routingKey, amqpMessage); + if (!temp.waitForConfirms(ACK_TIMEOUT)) { + throw new AmqpRejectAndDontRequeueException("Negative ack for DLQ message received"); + } + return null; + }); + } + else if (ConfirmType.CORRELATED.equals(this.confirmType)) { + CorrelationData corr = new CorrelationData(); + this.template.send(exchange, routingKey, amqpMessage, corr); + try { + Confirm confirm = corr.getFuture().get(ACK_TIMEOUT, TimeUnit.MILLISECONDS); + if (!confirm.isAck()) { + throw new AmqpRejectAndDontRequeueException("Negative ack for DLQ message received"); + } + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new AmqpRejectAndDontRequeueException(e); + } + catch (ExecutionException e) { + throw new AmqpRejectAndDontRequeueException(e.getCause()); + } + catch (TimeoutException e) { + throw new AmqpRejectAndDontRequeueException(e); + } + if (corr.getReturned() != null) { + RabbitMessageChannelBinder.this.logger.error("DLQ message was returned: " + amqpMessage); + throw new AmqpRejectAndDontRequeueException("DLQ message was returned"); + } + } + else { + this.template.send(exchange, routingKey, amqpMessage); + } + } + + private boolean checkDlx() { + if (this.dlxPresent == null) { + if (properties.getExtension().isAutoBindDlq()) { + this.dlxPresent = Boolean.TRUE; + } + else { + this.dlxPresent = this.template.execute(channel -> { + String dlx = deadLetterExchangeName(properties.getExtension()); + try { + channel.exchangeDeclarePassive(dlx); + return Boolean.TRUE; + } + catch (IOException e) { + logger.warn("'republishToDlq' is true, but the '" + + dlx + + "' dead letter exchange is not present; disabling 'republishToDlq'"); + return Boolean.FALSE; + } + }); + } + } + return this.dlxPresent; + } + + /** + * Traverse the cause tree, stopping at AmqpRejectAndDontRequeueException + * or ImmediateAcknowledgeAmqpException. + * @param throwable the throwable. + * @return true if neither found or AmqpRejectAndDontRequeueException is + * found first. + */ + private boolean shouldRepublish(Throwable throwable) { + Throwable cause = throwable; + while (cause != null + && !(cause instanceof AmqpRejectAndDontRequeueException) + && !(cause instanceof ImmediateAcknowledgeAmqpException)) { + cause = cause.getCause(); + } + return !(cause instanceof ImmediateAcknowledgeAmqpException); + } + + }; + } + else if (properties.getMaxAttempts() > 1) { + return new MessageHandler() { + private final RejectAndDontRequeueRecoverer recoverer = new RejectAndDontRequeueRecoverer(); + + @Override + public void handleMessage( + org.springframework.messaging.Message message) + throws MessagingException { + Message amqpMessage = StaticMessageHeaderAccessor.getSourceData(message); + /* + * NOTE: The following IF and subsequent ELSE IF should never happen + * under normal interaction and it should always go to the last ELSE + * However, given that this is a handler subscribing to the public + * channel and that we can't control what type of Message may be sent + * to that channel (user decides to send a Message manually) the + * 'IF/ELSE IF' provides a safety net to handle any message properly. + */ + if (!(message instanceof ErrorMessage)) { + logger.error("Expected an ErrorMessage, not a " + + message.getClass().toString() + " for: " + message); + throw new ListenerExecutionFailedException( + "Unexpected error message " + message, + new AmqpRejectAndDontRequeueException(""), (Message[]) null); + } + else if (amqpMessage == null) { + logger.error("No raw message header in " + message); + throw new ListenerExecutionFailedException( + "Unexpected error message " + message, + new AmqpRejectAndDontRequeueException(""), amqpMessage); + } + else { + this.recoverer.recover(amqpMessage, + (Throwable) message.getPayload()); + } + } + + }; + } + else { + return super.getErrorMessageHandler(destination, group, properties); + } + } + + @Override + protected MessageHandler getPolledConsumerErrorMessageHandler( + ConsumerDestination destination, String group, + ExtendedConsumerProperties properties) { + MessageHandler handler = getErrorMessageHandler(destination, group, properties); + if (handler != null) { + return handler; + } + final MessageHandler superHandler = super.getErrorMessageHandler(destination, + group, properties); + return message -> { + Message amqpMessage = (Message) message.getHeaders() + .get(AmqpMessageHeaderErrorMessageStrategy.AMQP_RAW_MESSAGE); + if (!(message instanceof ErrorMessage)) { + logger.error("Expected an ErrorMessage, not a " + + message.getClass().toString() + " for: " + message); + } + else if (amqpMessage == null) { + if (superHandler != null) { + superHandler.handleMessage(message); + } + } + else { + if (message.getPayload() instanceof MessagingException) { + AcknowledgmentCallback ack = StaticMessageHeaderAccessor + .getAcknowledgmentCallback( + ((MessagingException) message.getPayload()) + .getFailedMessage()); + if (ack != null) { + if (properties.getExtension().isRequeueRejected()) { + ack.acknowledge(Status.REQUEUE); + } + else { + ack.acknowledge(Status.REJECT); + } + } + } + } + }; + } + + @Override + protected String errorsBaseName(ConsumerDestination destination, String group, + ExtendedConsumerProperties consumerProperties) { + return destination.getName() + ".errors"; + } + + private String deadLetterExchangeName(RabbitCommonProperties properties) { + if (properties.getDeadLetterExchange() == null) { + return applyPrefix(properties.getPrefix(), + RabbitCommonProperties.DEAD_LETTER_EXCHANGE); + } + else { + return properties.getDeadLetterExchange(); + } + } + + @Override + protected void afterUnbindConsumer(ConsumerDestination consumerDestination, String group, + ExtendedConsumerProperties consumerProperties) { + + this.provisioningProvider.cleanAutoDeclareContext(consumerDestination, + consumerProperties); + } + + @Override + protected void afterUnbindProducer(ProducerDestination destination, + ExtendedProducerProperties producerProperties) { + + this.provisioningProvider.cleanAutoDeclareContext(destination, producerProperties); + } + + private RabbitTemplate buildRabbitTemplate(RabbitProducerProperties properties, boolean mandatory) { + RabbitTemplate rabbitTemplate; + if (properties.isBatchingEnabled()) { + BatchingStrategy batchingStrategy = getBatchingStrategy(properties); + TaskScheduler taskScheduler = getApplicationContext() + .getBean(IntegrationContextUtils.TASK_SCHEDULER_BEAN_NAME, TaskScheduler.class); + rabbitTemplate = new BatchingRabbitTemplate(batchingStrategy, taskScheduler); + } + else { + rabbitTemplate = new RabbitTemplate(); + } + rabbitTemplate.setMessageConverter(passThoughConverter); + rabbitTemplate.setChannelTransacted(properties.isTransacted()); + rabbitTemplate.setConnectionFactory(this.connectionFactory); + rabbitTemplate.setUsePublisherConnection(true); + if (properties.isCompress()) { + rabbitTemplate.setBeforePublishPostProcessors(this.compressingPostProcessor); + } + rabbitTemplate.setMandatory(mandatory); // returned messages + if (rabbitProperties != null + && rabbitProperties.getTemplate().getRetry().isEnabled()) { + Retry retry = rabbitProperties.getTemplate().getRetry(); + RetryPolicy retryPolicy = new SimpleRetryPolicy(retry.getMaxAttempts()); + ExponentialBackOffPolicy backOff = new ExponentialBackOffPolicy(); + backOff.setInitialInterval(retry.getInitialInterval().toMillis()); + backOff.setMultiplier(retry.getMultiplier()); + backOff.setMaxInterval(retry.getMaxInterval().toMillis()); + RetryTemplate retryTemplate = new RetryTemplate(); + retryTemplate.setRetryPolicy(retryPolicy); + retryTemplate.setBackOffPolicy(backOff); + rabbitTemplate.setRetryTemplate(retryTemplate); + } + rabbitTemplate.afterPropertiesSet(); + return rabbitTemplate; + } + + private BatchingStrategy getBatchingStrategy(RabbitProducerProperties properties) { + BatchingStrategy batchingStrategy; + if (properties.getBatchingStrategyBeanName() != null) { + batchingStrategy = getApplicationContext() + .getBean(properties.getBatchingStrategyBeanName(), BatchingStrategy.class); + } + else { + batchingStrategy = new SimpleBatchingStrategy( + properties.getBatchSize(), + properties.getBatchBufferLimit(), + properties.getBatchTimeout() + ); + } + return batchingStrategy; + } + + private String getStackTraceAsString(Throwable cause) { + StringWriter stringWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(stringWriter, true); + cause.printStackTrace(printWriter); + return stringWriter.getBuffer().toString(); + } + + private static final class SimplePassthroughMessageConverter + extends AbstractMessageConverter { + + private static final SimpleMessageConverter converter = new SimpleMessageConverter(); + + SimplePassthroughMessageConverter() { + super(); + } + + @Override + protected Message createMessage(Object object, + MessageProperties messageProperties) { + if (object instanceof byte[]) { + return new Message((byte[]) object, messageProperties); + } + else { + // just for safety (backwards compatibility) + return converter.toMessage(object, messageProperties); + } + } + + @Override + public Object fromMessage(Message message) throws MessageConversionException { + return message.getBody(); + } + + } + +} diff --git a/r-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/RabbitStreamMessageHandler.java b/r-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/RabbitStreamMessageHandler.java new file mode 100644 index 000000000..6930d42be --- /dev/null +++ b/r-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/RabbitStreamMessageHandler.java @@ -0,0 +1,259 @@ +/* + * Copyright 2021-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.stream.binder.rabbit; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.springframework.amqp.core.MessageProperties; +import org.springframework.amqp.support.AmqpHeaders; +import org.springframework.amqp.support.converter.ContentTypeDelegatingMessageConverter; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.context.Lifecycle; +import org.springframework.integration.amqp.support.AmqpHeaderMapper; +import org.springframework.integration.amqp.support.DefaultAmqpHeaderMapper; +import org.springframework.integration.handler.AbstractMessageHandler; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHandler; +import org.springframework.messaging.MessageHandlingException; +import org.springframework.messaging.MessageHeaders; +import org.springframework.rabbit.stream.producer.RabbitStreamOperations; +import org.springframework.rabbit.stream.support.StreamMessageProperties; +import org.springframework.util.Assert; +import org.springframework.util.MimeType; +import org.springframework.util.concurrent.ListenableFuture; +import org.springframework.util.concurrent.SuccessCallback; + +/** + * {@link MessageHandler} based on {@link RabbitStreamOperations}. + * + * TODO: This class will move to Spring Integration in 6.0. + * + * @author Gary Russell + * @since 3.2 + * + */ +public class RabbitStreamMessageHandler extends AbstractMessageHandler implements Lifecycle { + + private static final int DEFAULT_CONFIRM_TIMEOUT = 10_000; + + private final RabbitStreamOperations streamOperations; + + private boolean sync; + + private long confirmTimeout = DEFAULT_CONFIRM_TIMEOUT; + + private SuccessCallback> successCallback = msg -> { }; + + private FailureCallback failureCallback = (msg, ex) -> { }; + + private AmqpHeaderMapper headerMapper = DefaultAmqpHeaderMapper.outboundMapper(); + + private boolean headersMappedLast; + + /** + * Create an instance with the provided {@link RabbitStreamOperations}. + * @param streamOperations the operations. + */ + public RabbitStreamMessageHandler(RabbitStreamOperations streamOperations) { + Assert.notNull(streamOperations, "'streamOperations' cannot be null"); + this.streamOperations = streamOperations; + } + + /** + * Set a callback to be invoked when a send is successful. + * @param successCallback the callback. + */ + public void setSuccessCallback(SuccessCallback> successCallback) { + Assert.notNull(successCallback, "'successCallback' cannot be null"); + this.successCallback = successCallback; + } + + /** + * Set a callback to be invoked when a send fails. + * @param failureCallback the callback. + */ + public void setFailureCallback(FailureCallback failureCallback) { + Assert.notNull(failureCallback, "'failureCallback' cannot be null"); + this.failureCallback = failureCallback; + } + + /** + * Set to true to wait for a confirmation. + * @param sync true to wait. + * @see #setConfirmTimeout(long) + */ + public void setSync(boolean sync) { + this.sync = sync; + } + + /** + * Set the confirm timeout. + * @param confirmTimeout the timeout. + * @see #setSync(boolean) + */ + public void setConfirmTimeout(long confirmTimeout) { + this.confirmTimeout = confirmTimeout; + } + + /** + * Set a custom {@link AmqpHeaderMapper} for mapping request and reply headers. + * Defaults to {@link DefaultAmqpHeaderMapper#outboundMapper()}. + * @param headerMapper the {@link AmqpHeaderMapper} to use. + */ + public void setHeaderMapper(AmqpHeaderMapper headerMapper) { + Assert.notNull(headerMapper, "headerMapper must not be null"); + this.headerMapper = headerMapper; + } + + /** + * When mapping headers for the outbound message, determine whether the headers are + * mapped before the message is converted, or afterwards. This only affects headers + * that might be added by the message converter. When false, the converter's headers + * win; when true, any headers added by the converter will be overridden (if the + * source message has a header that maps to those headers). You might wish to set this + * to true, for example, when using a + * {@link org.springframework.amqp.support.converter.SimpleMessageConverter} with a + * String payload that contains json; the converter will set the content type to + * {@code text/plain} which can be overridden to {@code application/json} by setting + * the {@link AmqpHeaders#CONTENT_TYPE} message header. Default: false. + * @param headersMappedLast true if headers are mapped after conversion. + */ + public void setHeadersMappedLast(boolean headersMappedLast) { + this.headersMappedLast = headersMappedLast; + } + + /** + * Return the {@link RabbitStreamOperations}. + * @return the operations. + */ + public RabbitStreamOperations getStreamOperations() { + return this.streamOperations; + } + + @Override + protected void handleMessageInternal(Message requestMessage) { + ListenableFuture future; + com.rabbitmq.stream.Message streamMessage; + if (requestMessage.getPayload() instanceof com.rabbitmq.stream.Message) { + streamMessage = (com.rabbitmq.stream.Message) requestMessage.getPayload(); + } + else { + MessageConverter converter = streamOperations.messageConverter(); + org.springframework.amqp.core.Message amqpMessage = mapMessage(requestMessage, converter, + this.headerMapper, this.headersMappedLast); + streamMessage = this.streamOperations.streamMessageConverter().fromMessage(amqpMessage); + } + future = this.streamOperations.send(streamMessage); + handleConfirms(requestMessage, future); + } + + private void handleConfirms(Message message, ListenableFuture future) { + future.addCallback(bool -> this.successCallback.onSuccess(message), + ex -> this.failureCallback.failure(message, ex)); + if (this.sync) { + try { + future.get(this.confirmTimeout, TimeUnit.MILLISECONDS); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + throw new MessageHandlingException(message, ex); + } + catch (ExecutionException | TimeoutException ex) { + throw new MessageHandlingException(message, ex); + } + } + } + + /* + * TODO Copied/modified from MapppingUtils until SI 6.0 + */ + private static org.springframework.amqp.core.Message mapMessage(Message message, + MessageConverter converter, AmqpHeaderMapper headerMapper, boolean headersMappedLast) { + + MessageProperties amqpMessageProperties = new StreamMessageProperties(); + org.springframework.amqp.core.Message amqpMessage; + if (!headersMappedLast) { + mapHeaders(message.getHeaders(), amqpMessageProperties, headerMapper); + } + if (converter instanceof ContentTypeDelegatingMessageConverter && headersMappedLast) { + String contentType = contentTypeAsString(message.getHeaders()); + if (contentType != null) { + amqpMessageProperties.setContentType(contentType); + } + } + amqpMessage = converter.toMessage(message.getPayload(), amqpMessageProperties); + if (headersMappedLast) { + mapHeaders(message.getHeaders(), amqpMessageProperties, headerMapper); + } + return amqpMessage; + } + + private static void mapHeaders(MessageHeaders messageHeaders, MessageProperties amqpMessageProperties, + AmqpHeaderMapper headerMapper) { + + headerMapper.fromHeadersToRequest(messageHeaders, amqpMessageProperties); + } + + private static String contentTypeAsString(MessageHeaders headers) { + Object contentType = headers.get(AmqpHeaders.CONTENT_TYPE); + if (contentType instanceof MimeType) { + contentType = contentType.toString(); + } + if (contentType instanceof String) { + return (String) contentType; + } + else if (contentType != null) { + throw new IllegalArgumentException(AmqpHeaders.CONTENT_TYPE + + " header must be a MimeType or String, found: " + contentType.getClass().getName()); + } + return null; + } + /* + * End copied/modified from MappingUtils + */ + + @Override + public void start() { + } + + @Override + public void stop() { + this.streamOperations.close(); + } + + @Override + public boolean isRunning() { + return true; + } + + /** + * Callback for when publishing fails. + */ + public interface FailureCallback { + + /** + * Message publish failure. + * @param message the message. + * @param throwable the throwable. + */ + void failure(Message message, Throwable throwable); + + } + +} diff --git a/r-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/StreamUtils.java b/r-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/StreamUtils.java new file mode 100644 index 000000000..aee1466ac --- /dev/null +++ b/r-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/StreamUtils.java @@ -0,0 +1,169 @@ +/* + * Copyright 2021-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.stream.binder.rabbit; + +import java.util.Map; +import java.util.function.Function; + +import com.rabbitmq.stream.Environment; + +import org.springframework.amqp.core.MessageProperties; +import org.springframework.amqp.rabbit.listener.MessageListenerContainer; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; +import org.springframework.cloud.stream.binder.ExtendedProducerProperties; +import org.springframework.cloud.stream.binder.rabbit.properties.RabbitConsumerProperties; +import org.springframework.cloud.stream.binder.rabbit.properties.RabbitProducerProperties; +import org.springframework.cloud.stream.binder.rabbit.properties.RabbitProducerProperties.ProducerType; +import org.springframework.cloud.stream.provisioning.ConsumerDestination; +import org.springframework.cloud.stream.provisioning.ProducerDestination; +import org.springframework.context.support.AbstractApplicationContext; +import org.springframework.integration.amqp.inbound.AmqpInboundChannelAdapter; +import org.springframework.integration.amqp.support.AmqpHeaderMapper; +import org.springframework.integration.amqp.support.DefaultAmqpHeaderMapper; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.MessageHandler; +import org.springframework.messaging.MessageHandlingException; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.support.ErrorMessage; +import org.springframework.rabbit.stream.listener.ConsumerCustomizer; +import org.springframework.rabbit.stream.listener.StreamListenerContainer; +import org.springframework.rabbit.stream.producer.RabbitStreamTemplate; +import org.springframework.rabbit.stream.support.StreamMessageProperties; +import org.springframework.rabbit.stream.support.converter.StreamMessageConverter; + +/** + * Utilities for stream components. Used to prevent a hard runtime dependency on + * spring-rabbit-stream. + * + * @author Gary Russell + * @since 3.2 + * + */ +public final class StreamUtils { + + private StreamUtils() { + } + + /** + * Create a {@link StreamListenerContainer}. + * + * @param consumerDestination the destination. + * @param group the group. + * @param properties the properties. + * @param destination the destination. + * @param extension the properties extension. + * @param applicationContext the application context. + * @return the container. + */ + public static MessageListenerContainer createContainer(ConsumerDestination consumerDestination, String group, + ExtendedConsumerProperties properties, String destination, + RabbitConsumerProperties extension, AbstractApplicationContext applicationContext) { + + StreamListenerContainer container = new StreamListenerContainer(applicationContext.getBean(Environment.class)) { + + @Override + public synchronized void setConsumerCustomizer(ConsumerCustomizer consumerCustomizer) { + super.setConsumerCustomizer((id, builder) -> { + builder.name(consumerDestination.getName() + "." + group); + consumerCustomizer.accept(id, builder); + }); + } + + + }; + container.setBeanName(consumerDestination.getName() + "." + group + ".container"); + String beanName = extension.getStreamStreamMessageConverterBeanName(); + if (beanName != null) { + container.setStreamConverter(applicationContext.getBean(beanName, StreamMessageConverter.class)); + } + return container; + } + + /** + * Configure the channel adapter for streams support. + * @param adapter the adapter. + */ + public static void configureAdapter(AmqpInboundChannelAdapter adapter) { + adapter.setHeaderMapper(new AmqpHeaderMapper() { + + AmqpHeaderMapper mapper = DefaultAmqpHeaderMapper.inboundMapper(); + + @Override + public Map toHeadersFromRequest(MessageProperties source) { + Map headers = this.mapper.toHeadersFromRequest(source); + headers.put("rabbitmq_streamContext", ((StreamMessageProperties) source).getContext()); + return headers; + } + + @Override + public Map toHeadersFromReply(MessageProperties source) { + return null; + } + + @Override + public void fromHeadersToRequest(MessageHeaders headers, MessageProperties target) { + } + + @Override + public void fromHeadersToReply(MessageHeaders headers, MessageProperties target) { + } + + }); + } + + /** + * Create a {@link RabbitStreamMessageHandler}. + * + * @param producerDestination the destination. + * @param producerProperties the properties. + * @param errorChannel the error channel + * @param destination the destination. + * @param extendedProperties the extended properties. + * @param abstractApplicationContext the application context. + * @param headerMapperFunction the header mapper function. + * @return the handler. + */ + public static MessageHandler createStreamMessageHandler(ProducerDestination producerDestination, + ExtendedProducerProperties producerProperties, MessageChannel errorChannel, + String destination, RabbitProducerProperties extendedProperties, + AbstractApplicationContext applicationContext, + Function headerMapperFunction) { + + RabbitStreamTemplate template = new RabbitStreamTemplate(applicationContext.getBean(Environment.class), + producerDestination.getName()); + String beanName = extendedProperties.getStreamMessageConverterBeanName(); + if (beanName != null) { + template.setMessageConverter(applicationContext.getBean(beanName, MessageConverter.class)); + } + beanName = extendedProperties.getStreamStreamMessageConverterBeanName(); + if (beanName != null) { + template.setStreamConverter(applicationContext.getBean(beanName, StreamMessageConverter.class)); + } + RabbitStreamMessageHandler handler = new RabbitStreamMessageHandler(template); + if (errorChannel != null) { + handler.setFailureCallback((msg, ex) -> { + errorChannel.send(new ErrorMessage(new MessageHandlingException(msg, ex))); + }); + } + handler.setHeaderMapper(headerMapperFunction.apply(extendedProperties)); + handler.setSync(ProducerType.STREAM_SYNC.equals(producerProperties.getExtension().getProducerType())); + return handler; + } + +} + diff --git a/r-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/config/ExtendedBindingHandlerMappingsProviderConfiguration.java b/r-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/config/ExtendedBindingHandlerMappingsProviderConfiguration.java new file mode 100644 index 000000000..6f626f62f --- /dev/null +++ b/r-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/config/ExtendedBindingHandlerMappingsProviderConfiguration.java @@ -0,0 +1,47 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.stream.binder.rabbit.config; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.cloud.stream.config.BindingHandlerAdvise.MappingsProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Configuration for extended binding metadata. + * + * @author Oleg Zhurakousky + * + */ +@Configuration +public class ExtendedBindingHandlerMappingsProviderConfiguration { + + @Bean + public MappingsProvider rabbitExtendedPropertiesDefaultMappingsProvider() { + return () -> { + Map mappings = new HashMap<>(); + mappings.put( + ConfigurationPropertyName.of("spring.cloud.stream.rabbit.bindings"), + ConfigurationPropertyName.of("spring.cloud.stream.rabbit.default")); + return mappings; + }; + } + +} diff --git a/r-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/config/RabbitBinderConfiguration.java b/r-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/config/RabbitBinderConfiguration.java new file mode 100644 index 000000000..d04126370 --- /dev/null +++ b/r-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/config/RabbitBinderConfiguration.java @@ -0,0 +1,220 @@ +/* + * Copyright 2015-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.stream.binder.rabbit.config; + +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory.ConfirmType; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.actuate.amqp.RabbitHealthIndicator; +import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.autoconfigure.amqp.RabbitProperties; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.Cloud; +import org.springframework.cloud.CloudFactory; +import org.springframework.cloud.service.messaging.RabbitConnectionFactoryConfig; +import org.springframework.cloud.stream.binder.Binder; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Primary; +import org.springframework.context.annotation.Profile; +import org.springframework.util.StringUtils; + +/** + * Bind to services, either locally or in a cloud environment. + * + * @author Mark Fisher + * @author Dave Syer + * @author Glenn Renfro + * @author David Turanski + * @author Eric Bottard + * @author Marius Bogoevici + * @author Ilayaperumal Gopinathan + * @author Artem Bilan + * @author Gary Russell + * @author Chris Bono + * @author Soby Chacko + */ +@Configuration +@ConditionalOnMissingBean(Binder.class) +@Import({ RabbitMessageChannelBinderConfiguration.class, + RabbitBinderConfiguration.RabbitHealthIndicatorConfiguration.class }) +public abstract class RabbitBinderConfiguration { + + static void configureCachingConnectionFactory( + CachingConnectionFactory connectionFactory, + ConfigurableApplicationContext applicationContext, + RabbitProperties rabbitProperties) throws Exception { + + if (StringUtils.hasText(rabbitProperties.getAddresses())) { + connectionFactory.setAddresses(rabbitProperties.determineAddresses()); + } + + connectionFactory.setPublisherConfirmType(rabbitProperties.getPublisherConfirmType() == null ? ConfirmType.NONE : rabbitProperties.getPublisherConfirmType()); + connectionFactory.setPublisherReturns(rabbitProperties.isPublisherReturns()); + if (rabbitProperties.getCache().getChannel().getSize() != null) { + connectionFactory.setChannelCacheSize( + rabbitProperties.getCache().getChannel().getSize()); + } + if (rabbitProperties.getCache().getConnection().getMode() != null) { + connectionFactory + .setCacheMode(rabbitProperties.getCache().getConnection().getMode()); + } + if (rabbitProperties.getCache().getConnection().getSize() != null) { + connectionFactory.setConnectionCacheSize( + rabbitProperties.getCache().getConnection().getSize()); + } + if (rabbitProperties.getCache().getChannel().getCheckoutTimeout() != null) { + connectionFactory.setChannelCheckoutTimeout(rabbitProperties.getCache() + .getChannel().getCheckoutTimeout().toMillis()); + } + connectionFactory.setApplicationContext(applicationContext); + applicationContext.addApplicationListener(connectionFactory); + connectionFactory.afterPropertiesSet(); + } + + /** + * Configuration to be used when the cloud profile is set. + */ + @Configuration + @Profile("cloud") + protected static class CloudProfile { + + /** + * Configuration to be used when the cloud profile is set, and Cloud Connectors + * are found on the classpath. + */ + @Configuration + @ConditionalOnClass(Cloud.class) + protected static class CloudConnectors { + + @Bean + @ConditionalOnMissingBean + public Cloud cloud() { + return new CloudFactory().getCloud(); + } + + /** + * Active only if {@code spring.cloud.stream.override-cloud-connectors} is not + * set to {@code true}. + */ + // @checkstyle:off + @Configuration + @ConditionalOnProperty(value = "spring.cloud.stream.override-cloud-connectors", havingValue = "false", matchIfMissing = true) + // @checkstyle:on + // Required to parse Rabbit properties which are passed to the binder for + // clustering. We need to enable it here explicitly as the default Rabbit + // configuration is not triggered. + @EnableConfigurationProperties(RabbitProperties.class) + protected static class UseCloudConnectors { + + /** + * Creates a {@link ConnectionFactory} using the singleton service + * connector. + * @param cloud {@link Cloud} instance to be used for accessing services. + * @param connectorConfigObjectProvider the {@link ObjectProvider} for the + * {@link RabbitConnectionFactoryConfig}. + * @param applicationContext application context instance + * @param rabbitProperties rabbit properties + * @return the {@link ConnectionFactory} used by the binder. + * @throws Exception if configuration of connection factory fails + */ + @Bean + @Primary + ConnectionFactory rabbitConnectionFactory(Cloud cloud, + ObjectProvider connectorConfigObjectProvider, + ConfigurableApplicationContext applicationContext, + RabbitProperties rabbitProperties) throws Exception { + + ConnectionFactory connectionFactory = cloud + .getSingletonServiceConnector(ConnectionFactory.class, + connectorConfigObjectProvider.getIfUnique()); + + configureCachingConnectionFactory( + (CachingConnectionFactory) connectionFactory, + applicationContext, rabbitProperties); + + return connectionFactory; + } + + @Bean + @ConditionalOnMissingBean(RabbitTemplate.class) + RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) { + return new RabbitTemplate(connectionFactory); + } + + } + + /** + * Configuration to be used if + * {@code spring.cloud.stream.override-cloud-connectors} is set to + * {@code true}. Defers to Spring Boot auto-configuration. + */ + @Configuration + @ConditionalOnProperty("spring.cloud.stream.override-cloud-connectors") + @Import(RabbitConfiguration.class) + protected static class OverrideCloudConnectors { + + } + + } + + @Configuration + @ConditionalOnMissingClass("org.springframework.cloud.Cloud") + @Import(RabbitConfiguration.class) + protected static class NoCloudConnectors { + + } + + } + + /** + * Configuration to be used when the cloud profile is not set. Defer to Spring Boot + * auto-configuration. + */ + @Configuration + @Profile("!cloud") + @Import(RabbitConfiguration.class) + protected static class NoCloudProfile { + + } + + /** + * Configuration for Rabbit health indicator. + * + */ + @Configuration + @ConditionalOnClass(name = "org.springframework.boot.actuate.health.HealthIndicator") + @ConditionalOnEnabledHealthIndicator("binders") + public static class RabbitHealthIndicatorConfiguration { + + @Bean + public HealthIndicator binderHealthIndicator(RabbitTemplate rabbitTemplate) { + return new RabbitHealthIndicator(rabbitTemplate); + } + + } + +} diff --git a/r-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/config/RabbitConfiguration.java b/r-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/config/RabbitConfiguration.java new file mode 100644 index 000000000..1d9744677 --- /dev/null +++ b/r-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/config/RabbitConfiguration.java @@ -0,0 +1,115 @@ +/* + * Copyright 2015-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.stream.binder.rabbit.config; + +import java.util.stream.Collectors; + +import com.rabbitmq.client.impl.CredentialsProvider; +import com.rabbitmq.client.impl.CredentialsRefreshService; + +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.connection.ConnectionNameStrategy; +import org.springframework.amqp.rabbit.connection.RabbitConnectionFactoryBean; +import org.springframework.amqp.rabbit.core.RabbitOperations; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.amqp.CachingConnectionFactoryConfigurer; +import org.springframework.boot.autoconfigure.amqp.ConnectionFactoryCustomizer; +import org.springframework.boot.autoconfigure.amqp.RabbitConnectionFactoryBeanConfigurer; +import org.springframework.boot.autoconfigure.amqp.RabbitProperties; +import org.springframework.boot.autoconfigure.amqp.RabbitRetryTemplateCustomizer; +import org.springframework.boot.autoconfigure.amqp.RabbitTemplateConfigurer; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ResourceLoader; + +/** + * Configuration for {@link RabbitTemplate} and {@link CachingConnectionFactory}. + * + * @author Chris Bono + * @since 3.2 + */ +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(RabbitProperties.class) +public class RabbitConfiguration { + + @Bean + @ConditionalOnMissingBean + RabbitConnectionFactoryBeanConfigurer rabbitConnectionFactoryBeanConfigurer(RabbitProperties properties, + ResourceLoader resourceLoader, ObjectProvider credentialsProvider, + ObjectProvider credentialsRefreshService) { + RabbitConnectionFactoryBeanConfigurer configurer = new RabbitConnectionFactoryBeanConfigurer(resourceLoader, properties); + configurer.setCredentialsProvider(credentialsProvider.getIfUnique()); + configurer.setCredentialsRefreshService(credentialsRefreshService.getIfUnique()); + return configurer; + } + + @Bean + @ConditionalOnMissingBean + CachingConnectionFactoryConfigurer rabbitConnectionFactoryConfigurer(RabbitProperties rabbitProperties, + ObjectProvider connectionNameStrategy) { + CachingConnectionFactoryConfigurer configurer = new CachingConnectionFactoryConfigurer(rabbitProperties); + configurer.setConnectionNameStrategy(connectionNameStrategy.getIfUnique()); + return configurer; + } + + @Bean + @ConditionalOnMissingBean(ConnectionFactory.class) + CachingConnectionFactory rabbitConnectionFactory( + RabbitConnectionFactoryBeanConfigurer rabbitConnectionFactoryBeanConfigurer, + CachingConnectionFactoryConfigurer rabbitCachingConnectionFactoryConfigurer, + ObjectProvider connectionFactoryCustomizers) throws Exception { + + RabbitConnectionFactoryBean connectionFactoryBean = new RabbitConnectionFactoryBean(); + rabbitConnectionFactoryBeanConfigurer.configure(connectionFactoryBean); + connectionFactoryBean.afterPropertiesSet(); + com.rabbitmq.client.ConnectionFactory connectionFactory = connectionFactoryBean.getObject(); + connectionFactoryCustomizers.orderedStream() + .forEach((customizer) -> customizer.customize(connectionFactory)); + + CachingConnectionFactory cachingConnectionFactory = new CachingConnectionFactory(connectionFactory); + rabbitCachingConnectionFactoryConfigurer.configure(cachingConnectionFactory); + + return cachingConnectionFactory; + } + + @Bean + @ConditionalOnMissingBean + public RabbitTemplateConfigurer rabbitTemplateConfigurer(RabbitProperties properties, + ObjectProvider messageConverter, + ObjectProvider retryTemplateCustomizers) { + RabbitTemplateConfigurer configurer = new RabbitTemplateConfigurer(properties); + configurer.setMessageConverter(messageConverter.getIfUnique()); + configurer.setRetryTemplateCustomizers(retryTemplateCustomizers.orderedStream().collect(Collectors.toList())); + return configurer; + } + + @Bean + @ConditionalOnSingleCandidate(ConnectionFactory.class) + @ConditionalOnMissingBean(RabbitOperations.class) + public RabbitTemplate rabbitTemplate(RabbitTemplateConfigurer configurer, ConnectionFactory connectionFactory) { + RabbitTemplate template = new RabbitTemplate(); + configurer.configure(template, connectionFactory); + return template; + } + +} diff --git a/r-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/config/RabbitMessageChannelBinderConfiguration.java b/r-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/config/RabbitMessageChannelBinderConfiguration.java new file mode 100644 index 000000000..3bfd1d53f --- /dev/null +++ b/r-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/config/RabbitMessageChannelBinderConfiguration.java @@ -0,0 +1,124 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.stream.binder.rabbit.config; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import org.springframework.amqp.core.DeclarableCustomizer; +import org.springframework.amqp.core.MessagePostProcessor; +import org.springframework.amqp.rabbit.connection.AbstractConnectionFactory; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.connection.ConnectionNameStrategy; +import org.springframework.amqp.rabbit.listener.MessageListenerContainer; +import org.springframework.amqp.support.postprocessor.DelegatingDecompressingPostProcessor; +import org.springframework.amqp.support.postprocessor.GZipPostProcessor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.amqp.RabbitProperties; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.stream.binder.rabbit.RabbitMessageChannelBinder; +import org.springframework.cloud.stream.binder.rabbit.properties.RabbitBinderConfigurationProperties; +import org.springframework.cloud.stream.binder.rabbit.properties.RabbitExtendedBindingProperties; +import org.springframework.cloud.stream.binder.rabbit.provisioning.RabbitExchangeQueueProvisioner; +import org.springframework.cloud.stream.config.ConsumerEndpointCustomizer; +import org.springframework.cloud.stream.config.ListenerContainerCustomizer; +import org.springframework.cloud.stream.config.MessageSourceCustomizer; +import org.springframework.cloud.stream.config.ProducerMessageHandlerCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.integration.amqp.inbound.AmqpInboundChannelAdapter; +import org.springframework.integration.amqp.inbound.AmqpMessageSource; +import org.springframework.integration.amqp.outbound.AmqpOutboundEndpoint; +import org.springframework.lang.Nullable; + + +/** + * Configuration class for RabbitMQ message channel binder. + * + * @author David Turanski + * @author Vinicius Carvalho + * @author Artem Bilan + * @author Oleg Zhurakousky + * @author Gary Russell + */ +@Configuration +@Import({ PropertyPlaceholderAutoConfiguration.class }) +@EnableConfigurationProperties({ RabbitBinderConfigurationProperties.class, + RabbitExtendedBindingProperties.class }) +public class RabbitMessageChannelBinderConfiguration { + + @Autowired + private ConnectionFactory rabbitConnectionFactory; + + @Autowired + private RabbitProperties rabbitProperties; + + @Autowired + private RabbitBinderConfigurationProperties rabbitBinderConfigurationProperties; + + @Autowired + private RabbitExtendedBindingProperties rabbitExtendedBindingProperties; + + @Bean + RabbitMessageChannelBinder rabbitMessageChannelBinder( + @Nullable ListenerContainerCustomizer listenerContainerCustomizer, + @Nullable MessageSourceCustomizer sourceCustomizer, + @Nullable ProducerMessageHandlerCustomizer producerMessageHandlerCustomizer, + @Nullable ConsumerEndpointCustomizer consumerCustomizer, + List declarableCustomizers, + @Nullable ConnectionNameStrategy connectionNameStrategy) { + + String connectionNamePrefix = this.rabbitBinderConfigurationProperties.getConnectionNamePrefix(); + if (this.rabbitConnectionFactory instanceof AbstractConnectionFactory && connectionNamePrefix != null && connectionNameStrategy == null) { + final AtomicInteger nameIncrementer = new AtomicInteger(); + ((AbstractConnectionFactory) this.rabbitConnectionFactory).setConnectionNameStrategy(f -> connectionNamePrefix + + "#" + nameIncrementer.getAndIncrement()); + } + RabbitMessageChannelBinder binder = new RabbitMessageChannelBinder( + this.rabbitConnectionFactory, this.rabbitProperties, + provisioningProvider(declarableCustomizers), listenerContainerCustomizer, sourceCustomizer); + binder.setAdminAddresses( + this.rabbitBinderConfigurationProperties.getAdminAddresses()); + binder.setCompressingPostProcessor(gZipPostProcessor()); + binder.setDecompressingPostProcessor(deCompressingPostProcessor()); + binder.setNodes(this.rabbitBinderConfigurationProperties.getNodes()); + binder.setExtendedBindingProperties(this.rabbitExtendedBindingProperties); + binder.setProducerMessageHandlerCustomizer(producerMessageHandlerCustomizer); + binder.setConsumerEndpointCustomizer(consumerCustomizer); + return binder; + } + + @Bean + MessagePostProcessor deCompressingPostProcessor() { + return new DelegatingDecompressingPostProcessor(); + } + + @Bean + MessagePostProcessor gZipPostProcessor() { + GZipPostProcessor gZipPostProcessor = new GZipPostProcessor(); + gZipPostProcessor + .setLevel(this.rabbitBinderConfigurationProperties.getCompressionLevel()); + return gZipPostProcessor; + } + + @Bean + RabbitExchangeQueueProvisioner provisioningProvider(List customizers) { + return new RabbitExchangeQueueProvisioner(this.rabbitConnectionFactory, customizers); + } +} diff --git a/r-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/deployer/RabbitDeployer.java b/r-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/deployer/RabbitDeployer.java new file mode 100644 index 000000000..f31c9a48a --- /dev/null +++ b/r-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/deployer/RabbitDeployer.java @@ -0,0 +1,59 @@ +/* + * Copyright 2021-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.stream.binder.rabbit.deployer; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + + +/** + * Main application class to trigger the deployment of Java Functions based + * user application with RabitMQ bindings. + *
+ *
+ * It is based on Spring Cloud Function + * deploy feature + * + *
+ * All you need to do is to provide few additional properties to assist Spring Cloud Function deployer. + *
+ * For example: + *
+ *
+ *
+ * java -jar spring-cloud-stream-binder-rabbit-deployer/target/spring-cloud-stream-binder-rabbit-deployer-3.2.0-SNAPSHOT.jar\
+ * 		 	--spring.cloud.function.location=/foo/bar/simplestjar-1.0.0.RELEASE.jar
+ * 			--spring.cloud.function.function-class=function.example.UpperCaseFunction
+ *
+ * 
+ * The only required property is 'spring.cloud.function.location'. Every other property is optional and is based on the style of user application. + * For more details on the supported styles of user application please refer to + * + * Deploying a Packaged Function section of Spring Cloud Function. + * + * @author Oleg Zhurakousky + * @since 3.1.2 + * + */ +@SpringBootApplication +public class RabbitDeployer { + + public static void main(String[] args) { + SpringApplication.run(RabbitDeployer.class, args); + } + +} diff --git a/r-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/deployer/RabbitDeployerEnvironmentPostProcessor.java b/r-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/deployer/RabbitDeployerEnvironmentPostProcessor.java new file mode 100644 index 000000000..91fead450 --- /dev/null +++ b/r-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/deployer/RabbitDeployerEnvironmentPostProcessor.java @@ -0,0 +1,40 @@ +/* + * Copyright 2021-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.stream.binder.rabbit.deployer; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.core.env.ConfigurableEnvironment; + +/** + * + * @author Oleg Zhurakousky + * + * @since 3.2 + * + */ +class RabbitDeployerEnvironmentPostProcessor implements EnvironmentPostProcessor { + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, + SpringApplication application) { + if (!environment.containsProperty("spring.cloud.function.rsocket.enabled")) { + environment.getSystemProperties().putIfAbsent("spring.cloud.function.rsocket.enabled", false); + } + } + +} diff --git a/r-binder/spring-cloud-stream-binder-rabbit/src/main/resources/META-INF/spring.binders b/r-binder/spring-cloud-stream-binder-rabbit/src/main/resources/META-INF/spring.binders new file mode 100644 index 000000000..78f94972b --- /dev/null +++ b/r-binder/spring-cloud-stream-binder-rabbit/src/main/resources/META-INF/spring.binders @@ -0,0 +1,2 @@ +rabbit:\ +org.springframework.cloud.stream.binder.rabbit.config.RabbitBinderConfiguration diff --git a/r-binder/spring-cloud-stream-binder-rabbit/src/main/resources/META-INF/spring.factories b/r-binder/spring-cloud-stream-binder-rabbit/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..bb946aecc --- /dev/null +++ b/r-binder/spring-cloud-stream-binder-rabbit/src/main/resources/META-INF/spring.factories @@ -0,0 +1,4 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration:\ +org.springframework.cloud.stream.binder.rabbit.config.ExtendedBindingHandlerMappingsProviderConfiguration +org.springframework.boot.env.EnvironmentPostProcessor:\ +org.springframework.cloud.stream.binder.rabbit.deployer.RabbitDeployerEnvironmentPostProcessor \ No newline at end of file diff --git a/r-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/LocalizedQueueConnectionFactoryIntegrationTests.java b/r-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/LocalizedQueueConnectionFactoryIntegrationTests.java new file mode 100644 index 000000000..acb13785c --- /dev/null +++ b/r-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/LocalizedQueueConnectionFactoryIntegrationTests.java @@ -0,0 +1,74 @@ +/* + * Copyright 2015-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.stream.binder.rabbit; + +import java.util.UUID; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.connection.LocalizedQueueConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitAdmin; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.cloud.stream.binder.test.junit.rabbit.RabbitTestSupport; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Gary Russell + */ +public class LocalizedQueueConnectionFactoryIntegrationTests { + + @RegisterExtension + public static RabbitTestSupport rabbitAvailableRule = new RabbitTestSupport(true); + + private LocalizedQueueConnectionFactory lqcf; + + @BeforeEach + public void setup() { + ConnectionFactory defaultConnectionFactory = rabbitAvailableRule.getResource(); + String[] addresses = new String[] { "localhost:9999", "localhost:5672" }; + String[] adminAddresses = new String[] { "http://localhost:15672", + "http://localhost:15672" }; + String[] nodes = new String[] { "foo@bar", "rabbit@localhost" }; + String vhost = "/"; + String username = "guest"; + String password = "guest"; + this.lqcf = new LocalizedQueueConnectionFactory(defaultConnectionFactory, + addresses, adminAddresses, nodes, vhost, username, password, false, null, + null, null, null); + } + + @Test + public void testConnect() { + RabbitAdmin admin = new RabbitAdmin(this.lqcf); + Queue queue = new Queue(UUID.randomUUID().toString(), false, false, true); + admin.declareQueue(queue); + ConnectionFactory targetConnectionFactory = this.lqcf + .getTargetConnectionFactory("[" + queue.getName() + "]"); + RabbitTemplate template = new RabbitTemplate(targetConnectionFactory); + template.convertAndSend("", queue.getName(), "foo"); + assertThat(template.receiveAndConvert(queue.getName())).isEqualTo("foo"); + ((CachingConnectionFactory) targetConnectionFactory).destroy(); + admin.deleteQueue(queue.getName()); + } + +} diff --git a/r-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitBinderCleanerTests.java b/r-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitBinderCleanerTests.java new file mode 100644 index 000000000..dc5e5cf7e --- /dev/null +++ b/r-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitBinderCleanerTests.java @@ -0,0 +1,217 @@ +/* + * Copyright 2015-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.stream.binder.rabbit; + +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.DefaultConsumer; +import com.rabbitmq.http.client.Client; +import com.rabbitmq.http.client.domain.QueueInfo; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import org.springframework.amqp.core.Base64UrlNamingStrategy; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.core.TopicExchange; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.core.ChannelCallback; +import org.springframework.amqp.rabbit.core.RabbitAdmin; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.cloud.stream.binder.AbstractBinder; +import org.springframework.cloud.stream.binder.rabbit.admin.RabbitAdminException; +import org.springframework.cloud.stream.binder.rabbit.admin.RabbitBindingCleaner; +import org.springframework.cloud.stream.binder.test.junit.rabbit.RabbitTestSupport; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.fail; + +/** + * @author Gary Russell + * @since 1.2 + */ +public class RabbitBinderCleanerTests { + + private static final String BINDER_PREFIX = "binder."; + + private static final Client client; + + static { + try { + client = new Client("http://localhost:15672/api", "guest", "guest"); + } + catch (MalformedURLException | URISyntaxException e) { + throw new RabbitAdminException("Couldn't create a Client", e); + } + } + + @RegisterExtension + public RabbitTestSupport rabbitWithMgmtEnabled = new RabbitTestSupport(true); + + @Test + public void testCleanStream() { + final RabbitBindingCleaner cleaner = new RabbitBindingCleaner(); + final String stream1 = new Base64UrlNamingStrategy("foo").generateName(); + String stream2 = stream1 + "-1"; + String firstQueue = null; + CachingConnectionFactory connectionFactory = rabbitWithMgmtEnabled.getResource(); + RabbitAdmin rabbitAdmin = new RabbitAdmin(connectionFactory); + for (int i = 0; i < 5; i++) { + String queue1Name = AbstractBinder.applyPrefix(BINDER_PREFIX, + stream1 + ".default." + i); + String queue2Name = AbstractBinder.applyPrefix(BINDER_PREFIX, + stream2 + ".default." + i); + if (firstQueue == null) { + firstQueue = queue1Name; + } + rabbitAdmin.declareQueue(new Queue(queue1Name, true, false, false)); + rabbitAdmin.declareQueue(new Queue(queue2Name, true, false, false)); + rabbitAdmin.declareQueue(new Queue(AbstractBinder.constructDLQName(queue1Name), true, false, false)); + TopicExchange exchange = new TopicExchange(queue1Name); + rabbitAdmin.declareExchange(exchange); + rabbitAdmin.declareBinding(BindingBuilder.bind(new Queue(queue1Name)) + .to(exchange).with(queue1Name)); + exchange = new TopicExchange(queue2Name); + rabbitAdmin.declareExchange(exchange); + rabbitAdmin.declareBinding(BindingBuilder.bind(new Queue(queue2Name)) + .to(exchange).with(queue2Name)); + } + final TopicExchange topic1 = new TopicExchange( + AbstractBinder.applyPrefix(BINDER_PREFIX, stream1 + ".foo.bar")); + rabbitAdmin.declareExchange(topic1); + rabbitAdmin.declareBinding( + BindingBuilder.bind(new Queue(firstQueue)).to(topic1).with("#")); + String foreignQueue = UUID.randomUUID().toString(); + rabbitAdmin.declareQueue(new Queue(foreignQueue)); + rabbitAdmin.declareBinding( + BindingBuilder.bind(new Queue(foreignQueue)).to(topic1).with("#")); + final TopicExchange topic2 = new TopicExchange( + AbstractBinder.applyPrefix(BINDER_PREFIX, stream2 + ".foo.bar")); + rabbitAdmin.declareExchange(topic2); + rabbitAdmin.declareBinding( + BindingBuilder.bind(new Queue(firstQueue)).to(topic2).with("#")); + new RabbitTemplate(connectionFactory).execute(new ChannelCallback() { + + @Override + public Void doInRabbit(Channel channel) throws Exception { + String queueName = AbstractBinder.applyPrefix(BINDER_PREFIX, + stream1 + ".default." + 4); + String consumerTag = channel.basicConsume(queueName, + new DefaultConsumer(channel)); + try { + waitForConsumerStateNot(queueName, 0); + cleaner.clean(stream1, false); + fail("Expected exception"); + } + catch (RabbitAdminException e) { + assertThat(e) + .hasMessageContaining("Queue " + queueName + " is in use"); + } + channel.basicCancel(consumerTag); + waitForConsumerStateNot(queueName, 1); + try { + cleaner.clean(stream1, false); + fail("Expected exception"); + } + catch (RabbitAdminException e) { + assertThat(e).hasMessageContaining("Cannot delete exchange "); + assertThat(e).hasMessageContaining("; it has bindings:"); + } + return null; + } + + private void waitForConsumerStateNot(String queueName, long state) throws InterruptedException { + int n = 0; + QueueInfo queue = client.getQueue("/", queueName); + while (n++ < 100 && (queue == null || queue.getConsumerCount() == state)) { + Thread.sleep(100); + queue = client.getQueue("/", queueName); + } + assertThat(n).withFailMessage( + "Consumer state remained at " + state + " after 10 seconds") + .isLessThan(100); + } + + }); + rabbitAdmin.deleteExchange(topic1.getName()); // easier than deleting the binding + rabbitAdmin.declareExchange(topic1); + rabbitAdmin.deleteQueue(foreignQueue); + connectionFactory.destroy(); + Map> cleanedMap = cleaner.clean(stream1, false); + assertThat(cleanedMap).hasSize(2); + List cleanedQueues = cleanedMap.get("queues"); + // should *not* clean stream2 + assertThat(cleanedQueues).hasSize(10); + for (int i = 0; i < 5; i++) { + assertThat(cleanedQueues.get(i * 2)) + .isEqualTo(BINDER_PREFIX + stream1 + ".default." + i); + assertThat(cleanedQueues.get(i * 2 + 1)) + .isEqualTo(BINDER_PREFIX + stream1 + ".default." + i + ".dlq"); + } + List cleanedExchanges = cleanedMap.get("exchanges"); + assertThat(cleanedExchanges).hasSize(6); + + // wild card *should* clean stream2 + cleanedMap = cleaner.clean(stream1 + "*", false); + assertThat(cleanedMap).hasSize(2); + cleanedQueues = cleanedMap.get("queues"); + assertThat(cleanedQueues).hasSize(5); + for (int i = 0; i < 5; i++) { + assertThat(cleanedQueues.get(i)) + .isEqualTo(BINDER_PREFIX + stream2 + ".default." + i); + } + cleanedExchanges = cleanedMap.get("exchanges"); + assertThat(cleanedExchanges).hasSize(6); + } + + public static class AmqpQueue { + + private boolean autoDelete; + + private boolean durable; + + public AmqpQueue(boolean autoDelete, boolean durable) { + this.autoDelete = autoDelete; + this.durable = durable; + } + + @JsonProperty("auto_delete") + protected boolean isAutoDelete() { + return autoDelete; + } + + protected void setAutoDelete(boolean autoDelete) { + this.autoDelete = autoDelete; + } + + protected boolean isDurable() { + return durable; + } + + protected void setDurable(boolean durable) { + this.durable = durable; + } + + } + +} diff --git a/r-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitBinderTests.java b/r-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitBinderTests.java new file mode 100644 index 000000000..fe2724561 --- /dev/null +++ b/r-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitBinderTests.java @@ -0,0 +1,2655 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.stream.binder.rabbit; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.reflect.Constructor; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; +import java.util.zip.Deflater; + +import com.rabbitmq.client.LongString; +import com.rabbitmq.http.client.Client; +import com.rabbitmq.http.client.domain.BindingInfo; +import com.rabbitmq.http.client.domain.ExchangeInfo; +import com.rabbitmq.http.client.domain.QueueInfo; +import org.apache.commons.logging.Log; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.ArgumentCaptor; + +import org.springframework.amqp.AmqpIOException; +import org.springframework.amqp.ImmediateAcknowledgeAmqpException; +import org.springframework.amqp.core.AcknowledgeMode; +import org.springframework.amqp.core.AmqpTemplate; +import org.springframework.amqp.core.AnonymousQueue; +import org.springframework.amqp.core.Binding.DestinationType; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.Declarable; +import org.springframework.amqp.core.DirectExchange; +import org.springframework.amqp.core.ExchangeTypes; +import org.springframework.amqp.core.MessageDeliveryMode; +import org.springframework.amqp.core.MessageProperties; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.core.TopicExchange; +import org.springframework.amqp.rabbit.batch.BatchingStrategy; +import org.springframework.amqp.rabbit.batch.MessageBatch; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory.ConfirmType; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.connection.CorrelationData; +import org.springframework.amqp.rabbit.connection.CorrelationData.Confirm; +import org.springframework.amqp.rabbit.connection.RabbitUtils; +import org.springframework.amqp.rabbit.core.RabbitAdmin; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer; +import org.springframework.amqp.rabbit.listener.AsyncConsumerStartedEvent; +import org.springframework.amqp.rabbit.listener.DirectMessageListenerContainer; +import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; +import org.springframework.amqp.rabbit.retry.RepublishMessageRecoverer; +import org.springframework.amqp.support.AmqpHeaders; +import org.springframework.amqp.support.postprocessor.DelegatingDecompressingPostProcessor; +import org.springframework.amqp.utils.test.TestUtils; +import org.springframework.beans.DirectFieldAccessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.boot.autoconfigure.amqp.RabbitProperties; +import org.springframework.cloud.stream.binder.BinderException; +import org.springframework.cloud.stream.binder.BinderHeaders; +import org.springframework.cloud.stream.binder.Binding; +import org.springframework.cloud.stream.binder.DefaultPollableMessageSource; +import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; +import org.springframework.cloud.stream.binder.ExtendedProducerProperties; +import org.springframework.cloud.stream.binder.PartitionCapableBinderTests; +import org.springframework.cloud.stream.binder.PartitionKeyExtractorStrategy; +import org.springframework.cloud.stream.binder.PartitionSelectorStrategy; +import org.springframework.cloud.stream.binder.PartitionTestSupport; +import org.springframework.cloud.stream.binder.PollableSource; +import org.springframework.cloud.stream.binder.RequeueCurrentMessageException; +import org.springframework.cloud.stream.binder.Spy; +import org.springframework.cloud.stream.binder.rabbit.properties.RabbitCommonProperties.QuorumConfig; +import org.springframework.cloud.stream.binder.rabbit.properties.RabbitConsumerProperties; +import org.springframework.cloud.stream.binder.rabbit.properties.RabbitConsumerProperties.ContainerType; +import org.springframework.cloud.stream.binder.rabbit.properties.RabbitProducerProperties; +import org.springframework.cloud.stream.binder.rabbit.provisioning.RabbitExchangeQueueProvisioner; +import org.springframework.cloud.stream.binder.test.junit.rabbit.RabbitTestSupport; +import org.springframework.cloud.stream.config.BindingProperties; +import org.springframework.cloud.stream.provisioning.ConsumerDestination; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationListener; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.Lifecycle; +import org.springframework.expression.spel.standard.SpelExpression; +import org.springframework.integration.IntegrationMessageHeaderAccessor; +import org.springframework.integration.amqp.outbound.AmqpOutboundEndpoint; +import org.springframework.integration.amqp.support.NackedAmqpMessageException; +import org.springframework.integration.amqp.support.ReturnedAmqpMessageException; +import org.springframework.integration.channel.DirectChannel; +import org.springframework.integration.channel.QueueChannel; +import org.springframework.integration.context.IntegrationContextUtils; +import org.springframework.integration.expression.ValueExpression; +import org.springframework.integration.handler.BridgeHandler; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.MessageHandler; +import org.springframework.messaging.MessageHandlingException; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.MessagingException; +import org.springframework.messaging.SubscribableChannel; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.messaging.support.ErrorMessage; +import org.springframework.messaging.support.GenericMessage; +import org.springframework.retry.support.RetryTemplate; +import org.springframework.util.MimeTypeUtils; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * @author Mark Fisher + * @author Gary Russell + * @author David Turanski + * @author Artem Bilan + */ +// @checkstyle:off +public class RabbitBinderTests extends + PartitionCapableBinderTests, ExtendedProducerProperties> { + + private final String CLASS_UNDER_TEST_NAME = RabbitMessageChannelBinder.class + .getSimpleName(); + + public static final String TEST_PREFIX = "bindertest."; + + private static final String BIG_EXCEPTION_MESSAGE = new String(new byte[10_000]) + .replaceAll("\u0000", "x"); + + private int maxStackTraceSize; + + + @RegisterExtension + RabbitTestSupport rabbitAvailableRule = new RabbitTestSupport(true); + + @Override + protected RabbitTestBinder getBinder() { + if (this.testBinder == null) { + RabbitProperties rabbitProperties = new RabbitProperties(); + this.testBinder = new RabbitTestBinder(this.rabbitAvailableRule.getResource(), rabbitProperties); + } + return this.testBinder; + } + + @Override + protected ExtendedConsumerProperties createConsumerProperties() { + return new ExtendedConsumerProperties<>(new RabbitConsumerProperties()); + } + + @Override + protected ExtendedProducerProperties createProducerProperties(TestInfo testInfo) { + ExtendedProducerProperties props = new ExtendedProducerProperties<>( + new RabbitProducerProperties()); + + if (testInfo.getTestMethod().get().getName().equals("testPartitionedModuleSpEL")) { + props.getExtension().setRoutingKeyExpression( + spelExpressionParser.parseExpression("'part.0'")); + } + return props; + } + + @Override + protected boolean usesExplicitRouting() { + return true; + } + + @Test + public void testSendAndReceiveBad(TestInfo testInfo) throws Exception { + RabbitTestBinder binder = getBinder(); + final AtomicReference event = new AtomicReference<>(); + binder.getApplicationContext().addApplicationListener( + (ApplicationListener) e -> event.set(e)); + DirectChannel moduleOutputChannel = createBindableChannel("output", + new BindingProperties()); + DirectChannel moduleInputChannel = createBindableChannel("input", + new BindingProperties()); + Binding producerBinding = binder.bindProducer("bad.0", + moduleOutputChannel, createProducerProperties(testInfo)); + assertThat(TestUtils.getPropertyValue(producerBinding, + "lifecycle.headersMappedLast", Boolean.class)).isTrue(); + assertThat( + TestUtils + .getPropertyValue(producerBinding, + "lifecycle.amqpTemplate.messageConverter") + .getClass().getName()).contains("Passthrough"); + ExtendedConsumerProperties consumerProps = createConsumerProperties(); + consumerProps.getExtension().setContainerType(ContainerType.DIRECT); + Binding consumerBinding = binder.bindConsumer("bad.0", "test", + moduleInputChannel, consumerProps); + assertThat( + TestUtils.getPropertyValue(consumerBinding, "lifecycle.messageConverter") + .getClass().getName()).contains("Passthrough"); + assertThat(TestUtils.getPropertyValue(consumerBinding, + "lifecycle.messageListenerContainer")) + .isInstanceOf(DirectMessageListenerContainer.class); + Message message = MessageBuilder.withPayload("bad".getBytes()) + .setHeader(MessageHeaders.CONTENT_TYPE, "foo/bar").build(); + final CountDownLatch latch = new CountDownLatch(3); + moduleInputChannel.subscribe(new MessageHandler() { + + @Override + public void handleMessage(Message message) throws MessagingException { + latch.countDown(); + throw new RuntimeException("bad"); + } + }); + moduleOutputChannel.send(message); + assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(event.get()).isNotNull(); + producerBinding.unbind(); + consumerBinding.unbind(); + + verifyAutoDeclareContextClear(binder); + } + + @Test + public void testProducerErrorChannel(TestInfo testInfo) throws Exception { + RabbitTestBinder binder = getBinder(); + CachingConnectionFactory ccf = this.rabbitAvailableRule.getResource(); + ccf.setPublisherReturns(true); + ccf.setPublisherConfirmType(ConfirmType.CORRELATED); + ccf.resetConnection(); + DirectChannel moduleOutputChannel = createBindableChannel("output", + new BindingProperties()); + ExtendedProducerProperties producerProps = createProducerProperties(testInfo); + producerProps.setErrorChannelEnabled(true); + Binding producerBinding = binder.bindProducer("ec.0", + moduleOutputChannel, producerProps); + final Message message = MessageBuilder.withPayload("bad".getBytes()) + .setHeader(MessageHeaders.CONTENT_TYPE, "foo/bar").build(); + SubscribableChannel ec = binder.getApplicationContext().getBean("ec.0.errors", + SubscribableChannel.class); + final AtomicReference> errorMessage = new AtomicReference<>(); + final CountDownLatch latch = new CountDownLatch(2); + ec.subscribe(new MessageHandler() { + + @Override + public void handleMessage(Message message) throws MessagingException { + errorMessage.set(message); + latch.countDown(); + } + + }); + SubscribableChannel globalEc = binder.getApplicationContext().getBean( + IntegrationContextUtils.ERROR_CHANNEL_BEAN_NAME, + SubscribableChannel.class); + globalEc.subscribe(new MessageHandler() { + + @Override + public void handleMessage(Message message) throws MessagingException { + latch.countDown(); + } + + }); + moduleOutputChannel.send(message); + assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(errorMessage.get()).isInstanceOf(ErrorMessage.class); + assertThat(errorMessage.get().getPayload()) + .isInstanceOf(ReturnedAmqpMessageException.class); + ReturnedAmqpMessageException exception = (ReturnedAmqpMessageException) errorMessage + .get().getPayload(); + assertThat(exception.getReplyCode()).isEqualTo(312); + assertThat(exception.getReplyText()).isEqualTo("NO_ROUTE"); + + AmqpOutboundEndpoint endpoint = TestUtils.getPropertyValue(producerBinding, + "lifecycle", AmqpOutboundEndpoint.class); + assertThat(TestUtils.getPropertyValue(endpoint, + "confirmCorrelationExpression.expression")).isEqualTo("#root"); + class WrapperAccessor extends AmqpOutboundEndpoint { + + WrapperAccessor(AmqpTemplate amqpTemplate) { + super(amqpTemplate); + } + + CorrelationDataWrapper getWrapper() throws Exception { + Constructor constructor = CorrelationDataWrapper.class + .getDeclaredConstructor(String.class, Object.class, + Message.class); + ReflectionUtils.makeAccessible(constructor); + return constructor.newInstance(UUID.randomUUID().toString(), message, message); + } + + } + endpoint.confirm(new WrapperAccessor(mock(AmqpTemplate.class)).getWrapper(), + false, "Mock NACK"); + assertThat(errorMessage.get()).isInstanceOf(ErrorMessage.class); + assertThat(errorMessage.get().getPayload()) + .isInstanceOf(NackedAmqpMessageException.class); + NackedAmqpMessageException nack = (NackedAmqpMessageException) errorMessage.get() + .getPayload(); + assertThat(nack.getNackReason()).isEqualTo("Mock NACK"); + assertThat(nack.getCorrelationData()).isEqualTo(message); + assertThat(nack.getFailedMessage()).isEqualTo(message); + producerBinding.unbind(); + + verifyAutoDeclareContextClear(binder); + } + + @Test + public void testProducerAckChannel(TestInfo testInfo) throws Exception { + RabbitTestBinder binder = getBinder(); + CachingConnectionFactory ccf = this.rabbitAvailableRule.getResource(); + ccf.setPublisherReturns(true); + ccf.setPublisherConfirmType(ConfirmType.CORRELATED); + ccf.resetConnection(); + DirectChannel moduleOutputChannel = createBindableChannel("output", + new BindingProperties()); + ExtendedProducerProperties producerProps = createProducerProperties(testInfo); + producerProps.setErrorChannelEnabled(true); + producerProps.getExtension().setConfirmAckChannel("acksChannel"); + Binding producerBinding = binder.bindProducer("acks.0", + moduleOutputChannel, producerProps); + final Message message = MessageBuilder.withPayload("acksMessage".getBytes()) + .build(); + final AtomicReference> confirm = new AtomicReference<>(); + final CountDownLatch confirmLatch = new CountDownLatch(1); + binder.getApplicationContext().getBean("acksChannel", DirectChannel.class) + .subscribe(m -> { + confirm.set(m); + confirmLatch.countDown(); + }); + moduleOutputChannel.send(message); + assertThat(confirmLatch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(confirm.get().getPayload()).isEqualTo("acksMessage".getBytes()); + producerBinding.unbind(); + + verifyAutoDeclareContextClear(binder); + } + + @Test + public void testProducerConfirmHeader(TestInfo testInfo) throws Exception { + RabbitTestBinder binder = getBinder(); + CachingConnectionFactory ccf = this.rabbitAvailableRule.getResource(); + ccf.setPublisherReturns(true); + ccf.setPublisherConfirmType(ConfirmType.CORRELATED); + ccf.resetConnection(); + DirectChannel moduleOutputChannel = createBindableChannel("output", + new BindingProperties()); + ExtendedProducerProperties producerProps = createProducerProperties(testInfo); + producerProps.getExtension().setUseConfirmHeader(true); + Binding producerBinding = binder.bindProducer("confirms.0", + moduleOutputChannel, producerProps); + CorrelationData correlation = new CorrelationData("testConfirm"); + final Message message = MessageBuilder.withPayload("confirmsMessage".getBytes()) + .setHeader(AmqpHeaders.PUBLISH_CONFIRM_CORRELATION, correlation) + .build(); + moduleOutputChannel.send(message); + Confirm confirm = correlation.getFuture().get(10, TimeUnit.SECONDS); + assertThat(confirm.isAck()).isTrue(); + assertThat(correlation.getReturnedMessage()).isNotNull(); + producerBinding.unbind(); + + verifyAutoDeclareContextClear(binder); + } + + @Test + public void testConsumerProperties() throws Exception { + RabbitTestBinder binder = getBinder(); + ExtendedConsumerProperties properties = createConsumerProperties(); + properties.getExtension().setRequeueRejected(true); + properties.getExtension().setTransacted(true); + properties.getExtension().setExclusive(true); + properties.getExtension().setMissingQueuesFatal(true); + properties.getExtension().setFailedDeclarationRetryInterval(1500L); + properties.getExtension().setQueueDeclarationRetries(23); + Binding consumerBinding = binder.bindConsumer("props.0", null, + createBindableChannel("input", new BindingProperties()), properties); + Lifecycle endpoint = extractEndpoint(consumerBinding); + SimpleMessageListenerContainer container = TestUtils.getPropertyValue(endpoint, + "messageListenerContainer", SimpleMessageListenerContainer.class); + assertThat(container.getAcknowledgeMode()).isEqualTo(AcknowledgeMode.AUTO); + assertThat(container.getQueueNames()[0]) + .startsWith(properties.getExtension().getPrefix()); + assertThat(TestUtils.getPropertyValue(container, "transactional", Boolean.class)) + .isTrue(); + assertThat(TestUtils.getPropertyValue(container, "exclusive", Boolean.class)) + .isTrue(); + assertThat(TestUtils.getPropertyValue(container, "concurrentConsumers")) + .isEqualTo(1); + assertThat(TestUtils.getPropertyValue(container, "maxConcurrentConsumers")) + .isNull(); + assertThat(TestUtils.getPropertyValue(container, "defaultRequeueRejected", + Boolean.class)).isTrue(); + assertThat(TestUtils.getPropertyValue(container, "prefetchCount")).isEqualTo(1); + assertThat(TestUtils.getPropertyValue(container, "batchSize")).isEqualTo(1); + assertThat(TestUtils.getPropertyValue(container, "missingQueuesFatal", + Boolean.class)).isTrue(); + assertThat( + TestUtils.getPropertyValue(container, "failedDeclarationRetryInterval")) + .isEqualTo(1500L); + assertThat(TestUtils.getPropertyValue(container, "declarationRetries")) + .isEqualTo(23); + RetryTemplate retry = TestUtils.getPropertyValue(endpoint, "retryTemplate", + RetryTemplate.class); + assertThat(TestUtils.getPropertyValue(retry, "retryPolicy.maxAttempts")) + .isEqualTo(3); + assertThat(TestUtils.getPropertyValue(retry, "backOffPolicy.initialInterval")) + .isEqualTo(1000L); + assertThat(TestUtils.getPropertyValue(retry, "backOffPolicy.maxInterval")) + .isEqualTo(10000L); + assertThat(TestUtils.getPropertyValue(retry, "backOffPolicy.multiplier")) + .isEqualTo(2.0); + consumerBinding.unbind(); + assertThat(endpoint.isRunning()).isFalse(); + + properties = createConsumerProperties(); + properties.getExtension().setAcknowledgeMode(AcknowledgeMode.NONE); + properties.setBackOffInitialInterval(2000); + properties.setBackOffMaxInterval(20000); + properties.setBackOffMultiplier(5.0); + properties.setConcurrency(2); + properties.setMaxAttempts(23); + properties.getExtension().setMaxConcurrency(3); + properties.getExtension().setPrefix("foo."); + properties.getExtension().setPrefetch(20); + properties.getExtension().setHeaderPatterns(new String[] { "foo" }); + properties.getExtension().setTxSize(10); + QuorumConfig quorum = properties.getExtension().getQuorum(); + quorum.setEnabled(true); + quorum.setDeliveryLimit(10); + quorum.setInitialGroupSize(1); + properties.setInstanceIndex(0); + consumerBinding = binder.bindConsumer("props.0", "test", + createBindableChannel("input", new BindingProperties()), properties); + + endpoint = extractEndpoint(consumerBinding); + container = verifyContainer(endpoint); + + assertThat(container.getQueueNames()[0]).isEqualTo("foo.props.0.test"); + + consumerBinding.unbind(); + assertThat(endpoint.isRunning()).isFalse(); + + verifyAutoDeclareContextClear(binder); + } + + @Test + public void testMultiplexOnPartitionedConsumer() throws Exception { + final ExtendedConsumerProperties consumerProperties = createConsumerProperties(); + RabbitTestSupport.RabbitProxy proxy = new RabbitTestSupport.RabbitProxy(); + CachingConnectionFactory cf = new CachingConnectionFactory("localhost", + proxy.getPort()); + + final RabbitExchangeQueueProvisioner rabbitExchangeQueueProvisioner = new RabbitExchangeQueueProvisioner(cf); + + consumerProperties.setMultiplex(true); + consumerProperties.setPartitioned(true); + consumerProperties.setInstanceIndexList(Arrays.asList(1, 2, 3)); + + final ConsumerDestination consumerDestination = rabbitExchangeQueueProvisioner.provisionConsumerDestination("foo", "boo", consumerProperties); + + final String name = consumerDestination.getName(); + + assertThat(name).isEqualTo("foo.boo-1,foo.boo-2,foo.boo-3"); + } + + @Test + public void testMultiplexOnPartitionedConsumerWithMultipleDestinations() throws Exception { + final ExtendedConsumerProperties consumerProperties = createConsumerProperties(); + RabbitTestSupport.RabbitProxy proxy = new RabbitTestSupport.RabbitProxy(); + CachingConnectionFactory cf = new CachingConnectionFactory("localhost", + proxy.getPort()); + + final RabbitExchangeQueueProvisioner rabbitExchangeQueueProvisioner = new RabbitExchangeQueueProvisioner(cf); + + consumerProperties.setMultiplex(true); + consumerProperties.setPartitioned(true); + consumerProperties.setInstanceIndexList(Arrays.asList(1, 2, 3)); + + final ConsumerDestination consumerDestination = rabbitExchangeQueueProvisioner.provisionConsumerDestination("foo,qaa", "boo", consumerProperties); + + final String name = consumerDestination.getName(); + + assertThat(name).isEqualTo("foo.boo-1,foo.boo-2,foo.boo-3,qaa.boo-1,qaa.boo-2,qaa.boo-3"); + } + + @Test + public void testConsumerPropertiesWithUserInfrastructureNoBind() throws Exception { + RabbitAdmin admin = new RabbitAdmin(this.rabbitAvailableRule.getResource()); + Queue queue = new Queue("propsUser1.infra"); + admin.declareQueue(queue); + DirectExchange exchange = new DirectExchange("propsUser1"); + admin.declareExchange(exchange); + admin.declareBinding(BindingBuilder.bind(queue).to(exchange).with("foo")); + + RabbitTestBinder binder = getBinder(); + ExtendedConsumerProperties properties = createConsumerProperties(); + properties.getExtension().setDeclareExchange(false); + properties.getExtension().setBindQueue(false); + + Binding consumerBinding = binder.bindConsumer("propsUser1", + "infra", createBindableChannel("input", new BindingProperties()), + properties); + Lifecycle endpoint = extractEndpoint(consumerBinding); + SimpleMessageListenerContainer container = TestUtils.getPropertyValue(endpoint, + "messageListenerContainer", SimpleMessageListenerContainer.class); + assertThat(TestUtils.getPropertyValue(container, "missingQueuesFatal", + Boolean.class)).isFalse(); + assertThat(container.isRunning()).isTrue(); + consumerBinding.unbind(); + assertThat(container.isRunning()).isFalse(); + Client client = new Client("http://guest:guest@localhost:15672/api/"); + List bindings = client.getBindingsBySource("/", exchange.getName()); + assertThat(bindings.size()).isEqualTo(1); + } + + @Test + public void testAnonWithBuiltInExchange() throws Exception { + RabbitTestBinder binder = getBinder(); + ExtendedConsumerProperties properties = createConsumerProperties(); + properties.getExtension().setDeclareExchange(false); + properties.getExtension().setQueueNameGroupOnly(true); + + Binding consumerBinding = binder.bindConsumer("amq.topic", null, + createBindableChannel("input", new BindingProperties()), properties); + Lifecycle endpoint = extractEndpoint(consumerBinding); + SimpleMessageListenerContainer container = TestUtils.getPropertyValue(endpoint, + "messageListenerContainer", SimpleMessageListenerContainer.class); + String queueName = container.getQueueNames()[0]; + assertThat(queueName).startsWith("anonymous."); + assertThat(container.isRunning()).isTrue(); + consumerBinding.unbind(); + assertThat(container.isRunning()).isFalse(); + + verifyAutoDeclareContextClear(binder); + } + + + @Test + public void testAnonWithBuiltInExchangeCustomPrefix() throws Exception { + RabbitTestBinder binder = getBinder(); + ExtendedConsumerProperties properties = createConsumerProperties(); + properties.getExtension().setDeclareExchange(false); + properties.getExtension().setQueueNameGroupOnly(true); + properties.getExtension().setAnonymousGroupPrefix("customPrefix."); + + Binding consumerBinding = binder.bindConsumer("amq.topic", null, + createBindableChannel("input", new BindingProperties()), properties); + Lifecycle endpoint = extractEndpoint(consumerBinding); + SimpleMessageListenerContainer container = TestUtils.getPropertyValue(endpoint, + "messageListenerContainer", SimpleMessageListenerContainer.class); + String queueName = container.getQueueNames()[0]; + assertThat(queueName).startsWith("customPrefix."); + assertThat(container.isRunning()).isTrue(); + consumerBinding.unbind(); + assertThat(container.isRunning()).isFalse(); + + verifyAutoDeclareContextClear(binder); + } + + + @Test + public void testConsumerPropertiesWithUserInfrastructureCustomExchangeAndRK() + throws Exception { + RabbitTestBinder binder = getBinder(); + ExtendedConsumerProperties properties = createConsumerProperties(); + properties.getExtension().setExchangeType(ExchangeTypes.DIRECT); + properties.getExtension().setBindingRoutingKey("foo,bar"); + properties.getExtension().setBindingRoutingKeyDelimiter(","); + properties.getExtension().setQueueNameGroupOnly(true); + // properties.getExtension().setDelayedExchange(true); // requires delayed message + // exchange plugin; tested locally + + String group = "infra"; + Binding consumerBinding = binder.bindConsumer("propsUser2", group, + createBindableChannel("input", new BindingProperties()), properties); + Lifecycle endpoint = extractEndpoint(consumerBinding); + SimpleMessageListenerContainer container = TestUtils.getPropertyValue(endpoint, + "messageListenerContainer", SimpleMessageListenerContainer.class); + assertThat(container.isRunning()).isTrue(); + consumerBinding.unbind(); + assertThat(container.isRunning()).isFalse(); + assertThat(container.getQueueNames()[0]).isEqualTo(group); + Client client = new Client("http://guest:guest@localhost:15672/api/"); + List bindings = client.getBindingsBySource("/", "propsUser2"); + int n = 0; + while (n++ < 100 && bindings == null || bindings.size() < 1) { + Thread.sleep(100); + bindings = client.getBindingsBySource("/", "propsUser2"); + } + assertThat(bindings.size()).isEqualTo(2); + assertThat(bindings.get(0).getSource()).isEqualTo("propsUser2"); + assertThat(bindings.get(0).getDestination()).isEqualTo(group); + assertThat(bindings.get(0).getRoutingKey()).isIn("foo", "bar"); + assertThat(bindings.get(1).getSource()).isEqualTo("propsUser2"); + assertThat(bindings.get(1).getDestination()).isEqualTo(group); + assertThat(bindings.get(1).getRoutingKey()).isIn("foo", "bar"); + assertThat(bindings.get(1).getRoutingKey()).isNotEqualTo(bindings.get(0).getRoutingKey()); + + ExchangeInfo exchange = client.getExchange("/", "propsUser2"); + while (n++ < 100 && exchange == null) { + Thread.sleep(100); + exchange = client.getExchange("/", "propsUser2"); + } + assertThat(exchange.getType()).isEqualTo("direct"); + assertThat(exchange.isDurable()).isEqualTo(true); + assertThat(exchange.isAutoDelete()).isEqualTo(false); + + verifyAutoDeclareContextClear(binder); + } + + + @Test + public void testConsumerPropertiesWithUserInfrastructureCustomQueueArgs() + throws Exception { + RabbitTestBinder binder = getBinder(); + ExtendedConsumerProperties properties = createConsumerProperties(); + RabbitConsumerProperties extProps = properties.getExtension(); + extProps.setExchangeType(ExchangeTypes.DIRECT); + extProps.setExchangeDurable(false); + extProps.setExchangeAutoDelete(true); + extProps.setBindingRoutingKey("foo"); + extProps.setExpires(30_000); + extProps.setLazy(true); + extProps.setMaxLength(10_000); + extProps.setMaxLengthBytes(100_000); + extProps.setMaxPriority(10); + extProps.setOverflowBehavior("drop-head"); + extProps.setTtl(2_000); + extProps.setAutoBindDlq(true); + extProps.setDeadLetterQueueName("customDLQ"); + extProps.setDeadLetterExchange("customDLX"); + extProps.setDeadLetterExchangeType(ExchangeTypes.TOPIC); + extProps.setDeadLetterRoutingKey("customDLRK"); + extProps.setDlqDeadLetterExchange("propsUser3"); + // GH-259 - if the next line was commented, the test failed. + extProps.setDlqDeadLetterRoutingKey("propsUser3"); + extProps.setDlqExpires(60_000); + extProps.setDlqLazy(true); + extProps.setDlqMaxLength(20_000); + extProps.setDlqMaxLengthBytes(40_000); + extProps.setDlqOverflowBehavior("reject-publish"); + extProps.setDlqMaxPriority(8); + extProps.setDlqTtl(1_000); + extProps.setConsumerTagPrefix("testConsumerTag"); + extProps.setExclusive(true); + + Binding consumerBinding = binder.bindConsumer("propsUser3", + "infra", createBindableChannel("input", new BindingProperties()), + properties); + Lifecycle endpoint = extractEndpoint(consumerBinding); + SimpleMessageListenerContainer container = TestUtils.getPropertyValue(endpoint, + "messageListenerContainer", SimpleMessageListenerContainer.class); + assertThat(container.isRunning()).isTrue(); + Client client = new Client("http://guest:guest@localhost:15672/api"); + List bindings = client.getBindingsBySource("/", "propsUser3"); + int n = 0; + while (n++ < 100 && bindings == null || bindings.size() < 1) { + Thread.sleep(100); + bindings = client.getBindingsBySource("/", "propsUser3"); + } + assertThat(bindings.size()).isEqualTo(1); + assertThat(bindings.get(0).getSource()).isEqualTo("propsUser3"); + assertThat(bindings.get(0).getDestination()).isEqualTo("propsUser3.infra"); + assertThat(bindings.get(0).getRoutingKey()).isEqualTo("foo"); + + bindings = client.getBindingsBySource("/", "customDLX"); + n = 0; + while (n++ < 100 && bindings == null || bindings.size() < 1) { + Thread.sleep(100); + bindings = client.getBindingsBySource("/", "customDLX"); + } +// assertThat(bindings.size()).isEqualTo(1); + assertThat(bindings.get(0).getSource()).isEqualTo("customDLX"); + assertThat(bindings.get(0).getDestination()).isEqualTo("customDLQ"); + assertThat(bindings.get(0).getRoutingKey()).isEqualTo("customDLRK"); + + ExchangeInfo exchange = client.getExchange("/", "propsUser3"); + n = 0; + while (n++ < 100 && exchange == null) { + Thread.sleep(100); + exchange = client.getExchange("/", "propsUser3"); + } + assertThat(exchange.getType()).isEqualTo("direct"); + assertThat(exchange.isDurable()).isEqualTo(false); + assertThat(exchange.isAutoDelete()).isEqualTo(true); + + exchange = client.getExchange("/", "customDLX"); + n = 0; + while (n++ < 100 && exchange == null) { + Thread.sleep(100); + exchange = client.getExchange("/", "customDLX"); + } + assertThat(exchange.getType()).isEqualTo("topic"); + assertThat(exchange.isDurable()).isEqualTo(true); + assertThat(exchange.isAutoDelete()).isEqualTo(false); + + QueueInfo queue = client.getQueue("/", "propsUser3.infra"); + n = 0; + while (n++ < 100 && queue == null || queue.getConsumerCount() == 0) { + Thread.sleep(100); + queue = client.getQueue("/", "propsUser3.infra"); + } + assertThat(queue).isNotNull(); + Map args = queue.getArguments(); + assertThat(args.get("x-expires")).isEqualTo(30_000); + assertThat(args.get("x-max-length")).isEqualTo(10_000); + assertThat(args.get("x-max-length-bytes")).isEqualTo(100_000); + assertThat(args.get("x-overflow")).isEqualTo("drop-head"); + assertThat(args.get("x-max-priority")).isEqualTo(10); + assertThat(args.get("x-message-ttl")).isEqualTo(2_000); + assertThat(args.get("x-dead-letter-exchange")).isEqualTo("customDLX"); + assertThat(args.get("x-dead-letter-routing-key")).isEqualTo("customDLRK"); + assertThat(args.get("x-queue-mode")).isEqualTo("lazy"); + assertThat(queue.getExclusiveConsumerTag()).isEqualTo("testConsumerTag#0"); + + queue = client.getQueue("/", "customDLQ"); + + n = 0; + while (n++ < 100 && queue == null) { + Thread.sleep(100); + queue = client.getQueue("/", "customDLQ"); + } + assertThat(queue).isNotNull(); + args = queue.getArguments(); + assertThat(args.get("x-expires")).isEqualTo(60_000); + assertThat(args.get("x-max-length")).isEqualTo(20_000); + assertThat(args.get("x-max-length-bytes")).isEqualTo(40_000); + assertThat(args.get("x-overflow")).isEqualTo("reject-publish"); + assertThat(args.get("x-max-priority")).isEqualTo(8); + assertThat(args.get("x-message-ttl")).isEqualTo(1_000); + assertThat(args.get("x-dead-letter-exchange")).isEqualTo("propsUser3"); + assertThat(args.get("x-dead-letter-routing-key")).isEqualTo("propsUser3"); + assertThat(args.get("x-queue-mode")).isEqualTo("lazy"); + + consumerBinding.unbind(); + assertThat(container.isRunning()).isFalse(); + + verifyAutoDeclareContextClear(binder); + } + + + @Test + public void testConsumerPropertiesWithHeaderExchanges() throws Exception { + RabbitTestBinder binder = getBinder(); + ExtendedConsumerProperties properties = createConsumerProperties(); + properties.getExtension().setExchangeType(ExchangeTypes.HEADERS); + properties.getExtension().setAutoBindDlq(true); + properties.getExtension().setDeadLetterExchange(ExchangeTypes.HEADERS); + properties.getExtension().setDeadLetterExchange("propsHeader.dlx"); + Map queueBindingArguments = new HashMap<>(); + queueBindingArguments.put("x-match", "any"); + queueBindingArguments.put("foo", "bar"); + properties.getExtension().setQueueBindingArguments(queueBindingArguments); + properties.getExtension().setDlqBindingArguments(queueBindingArguments); + + String group = "bindingArgs"; + Binding consumerBinding = binder.bindConsumer("propsHeader", group, + createBindableChannel("input", new BindingProperties()), properties); + Lifecycle endpoint = extractEndpoint(consumerBinding); + SimpleMessageListenerContainer container = TestUtils.getPropertyValue(endpoint, + "messageListenerContainer", SimpleMessageListenerContainer.class); + assertThat(container.isRunning()).isTrue(); + consumerBinding.unbind(); + assertThat(container.isRunning()).isFalse(); + assertThat(container.getQueueNames()[0]).isEqualTo("propsHeader." + group); + Client client = new Client("http://guest:guest@localhost:15672/api/"); + List bindings = client.getBindingsBySource("/", "propsHeader"); + int n = 0; + while (n++ < 100 && bindings == null || bindings.size() < 1) { + Thread.sleep(100); + bindings = client.getBindingsBySource("/", "propsHeader"); + } + assertThat(bindings.size()).isEqualTo(1); + assertThat(bindings.get(0).getSource()).isEqualTo("propsHeader"); + assertThat(bindings.get(0).getDestination()).isEqualTo("propsHeader." + group); + assertThat(bindings.get(0).getArguments()).hasEntrySatisfying("x-match", v -> assertThat(v).isEqualTo("any")); + assertThat(bindings.get(0).getArguments()).hasEntrySatisfying("foo", v -> assertThat(v).isEqualTo("bar")); + + bindings = client.getBindingsBySource("/", "propsHeader.dlx"); + n = 0; + while (n++ < 100 && bindings == null || bindings.size() < 1) { + Thread.sleep(100); + bindings = client.getBindingsBySource("/", "propsHeader.dlx"); + } + assertThat(bindings.size()).isEqualTo(1); + assertThat(bindings.get(0).getSource()).isEqualTo("propsHeader.dlx"); + assertThat(bindings.get(0).getDestination()).isEqualTo("propsHeader." + group + ".dlq"); + assertThat(bindings.get(0).getArguments()).hasEntrySatisfying("x-match", v -> assertThat(v).isEqualTo("any")); + assertThat(bindings.get(0).getArguments()).hasEntrySatisfying("foo", v -> assertThat(v).isEqualTo("bar")); + + verifyAutoDeclareContextClear(binder); + } + + + @Test + public void testProducerProperties(TestInfo testInfo) throws Exception { + RabbitTestBinder binder = getBinder(); + Binding producerBinding = binder.bindProducer("props.0", + createBindableChannel("input", new BindingProperties()), + createProducerProperties(testInfo)); + Lifecycle endpoint = extractEndpoint(producerBinding); + MessageDeliveryMode mode = TestUtils.getPropertyValue(endpoint, + "defaultDeliveryMode", MessageDeliveryMode.class); + assertThat(mode).isEqualTo(MessageDeliveryMode.PERSISTENT); + List requestHeaders = TestUtils.getPropertyValue(endpoint, + "headerMapper.requestHeaderMatcher.matchers", List.class); + assertThat(requestHeaders).hasSize(5); + producerBinding.unbind(); + assertThat(endpoint.isRunning()).isFalse(); + assertThat(TestUtils.getPropertyValue(endpoint, "amqpTemplate.transactional", + Boolean.class)).isFalse(); + + ExtendedProducerProperties producerProperties = createProducerProperties(testInfo); + this.applicationContext.registerBean("pkExtractor", + TestPartitionKeyExtractorClass.class, () -> new TestPartitionKeyExtractorClass()); + this.applicationContext.registerBean("pkSelector", + TestPartitionSelectorClass.class, () -> new TestPartitionSelectorClass()); + producerProperties.setPartitionKeyExtractorName("pkExtractor"); + producerProperties.setPartitionSelectorName("pkSelector"); + producerProperties.getExtension().setPrefix("foo."); + producerProperties.getExtension() + .setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT); + producerProperties.getExtension().setHeaderPatterns(new String[] { "foo" }); + producerProperties + .setPartitionKeyExpression(spelExpressionParser.parseExpression("'foo'")); + producerProperties.setPartitionSelectorExpression( + spelExpressionParser.parseExpression("0")); + producerProperties.setPartitionCount(1); + producerProperties.getExtension().setTransacted(true); + producerProperties.getExtension() + .setDelayExpression(spelExpressionParser.parseExpression("42")); + producerProperties.setRequiredGroups("prodPropsRequired"); + + BindingProperties producerBindingProperties = createProducerBindingProperties( + producerProperties); + DirectChannel channel = createBindableChannel("output", + producerBindingProperties); + producerBinding = binder.bindProducer("props.0", channel, producerProperties); + + ConnectionFactory producerConnectionFactory = TestUtils.getPropertyValue( + producerBinding, "lifecycle.amqpTemplate.connectionFactory", + ConnectionFactory.class); + + assertThat(this.rabbitAvailableRule.getResource()) + .isSameAs(producerConnectionFactory); + + endpoint = extractEndpoint(producerBinding); + assertThat(getEndpointRouting(endpoint)).isEqualTo( + "'props.0-' + headers['" + BinderHeaders.PARTITION_HEADER + "']"); + assertThat(TestUtils + .getPropertyValue(endpoint, "delayExpression", SpelExpression.class) + .getExpressionString()).isEqualTo("42"); + mode = TestUtils.getPropertyValue(endpoint, "defaultDeliveryMode", + MessageDeliveryMode.class); + assertThat(mode).isEqualTo(MessageDeliveryMode.NON_PERSISTENT); + assertThat(TestUtils.getPropertyValue(endpoint, "amqpTemplate.transactional", + Boolean.class)).isTrue(); + verifyFooRequestProducer(endpoint); + channel.send(new GenericMessage<>("foo")); + org.springframework.amqp.core.Message received = new RabbitTemplate( + this.rabbitAvailableRule.getResource()) + .receive("foo.props.0.prodPropsRequired-0", 10_000); + assertThat(received).isNotNull(); + assertThat(received.getMessageProperties().getReceivedDelay()).isEqualTo(42); + + producerBinding.unbind(); + assertThat(endpoint.isRunning()).isFalse(); + + verifyAutoDeclareContextClear(binder); + } + + + @Test + public void testDurablePubSubWithAutoBindDLQ() throws Exception { + RabbitAdmin admin = new RabbitAdmin(this.rabbitAvailableRule.getResource()); + + RabbitTestBinder binder = getBinder(); + + ExtendedConsumerProperties consumerProperties = createConsumerProperties(); + consumerProperties.getExtension().setPrefix(TEST_PREFIX); + consumerProperties.getExtension().setAutoBindDlq(true); + consumerProperties.getExtension().setDurableSubscription(true); + consumerProperties.setMaxAttempts(1); // disable retry + DirectChannel moduleInputChannel = createBindableChannel("input", + createConsumerBindingProperties(consumerProperties)); + moduleInputChannel.setBeanName("durableTest"); + moduleInputChannel.subscribe(new MessageHandler() { + + @Override + public void handleMessage(Message message) throws MessagingException { + throw new RuntimeException("foo"); + } + + }); + Binding consumerBinding = binder.bindConsumer("durabletest.0", + "tgroup", moduleInputChannel, consumerProperties); + + RabbitTemplate template = new RabbitTemplate( + this.rabbitAvailableRule.getResource()); + template.convertAndSend(TEST_PREFIX + "durabletest.0", "", "foo"); + + int n = 0; + while (n++ < 100) { + Object deadLetter = template + .receiveAndConvert(TEST_PREFIX + "durabletest.0.tgroup.dlq"); + if (deadLetter != null) { + assertThat(deadLetter).isEqualTo("foo"); + break; + } + Thread.sleep(100); + } + assertThat(n).isLessThan(100); + + consumerBinding.unbind(); + assertThat(admin.getQueueProperties(TEST_PREFIX + "durabletest.0.tgroup.dlq")) + .isNotNull(); + + verifyAutoDeclareContextClear(binder); + } + + + @Test + public void testNonDurablePubSubWithAutoBindDLQ() throws Exception { + RabbitAdmin admin = new RabbitAdmin(this.rabbitAvailableRule.getResource()); + + RabbitTestBinder binder = getBinder(); + ExtendedConsumerProperties consumerProperties = createConsumerProperties(); + consumerProperties.getExtension().setPrefix(TEST_PREFIX); + consumerProperties.getExtension().setAutoBindDlq(true); + consumerProperties.getExtension().setDurableSubscription(false); + consumerProperties.setMaxAttempts(1); // disable retry + BindingProperties bindingProperties = createConsumerBindingProperties( + consumerProperties); + DirectChannel moduleInputChannel = createBindableChannel("input", + bindingProperties); + moduleInputChannel.setBeanName("nondurabletest"); + moduleInputChannel.subscribe(new MessageHandler() { + + @Override + public void handleMessage(Message message) throws MessagingException { + throw new RuntimeException("foo"); + } + + }); + Binding consumerBinding = binder.bindConsumer("nondurabletest.0", + "tgroup", moduleInputChannel, consumerProperties); + + consumerBinding.unbind(); + assertThat(admin.getQueueProperties(TEST_PREFIX + "nondurabletest.0.dlq")) + .isNull(); + + verifyAutoDeclareContextClear(binder); + } + + + @Test + public void testAutoBindDLQ() throws Exception { + RabbitTestBinder binder = getBinder(); + ExtendedConsumerProperties consumerProperties = createConsumerProperties(); + consumerProperties.getExtension().setPrefix(TEST_PREFIX); + consumerProperties.getExtension().setAutoBindDlq(true); + consumerProperties.setMaxAttempts(1); // disable retry + consumerProperties.getExtension().setDurableSubscription(true); + BindingProperties bindingProperties = createConsumerBindingProperties( + consumerProperties); + DirectChannel moduleInputChannel = createBindableChannel("input", + bindingProperties); + moduleInputChannel.setBeanName("dlqTest"); + moduleInputChannel.subscribe(new MessageHandler() { + + @Override + public void handleMessage(Message message) throws MessagingException { + throw new RuntimeException("foo"); + } + + }); + consumerProperties.setMultiplex(true); + Binding consumerBinding = binder.bindConsumer("dlqtest,dlqtest2", + "default", moduleInputChannel, consumerProperties); + AbstractMessageListenerContainer container = TestUtils.getPropertyValue( + consumerBinding, "lifecycle.messageListenerContainer", + AbstractMessageListenerContainer.class); + assertThat(container.getQueueNames().length).isEqualTo(2); + + RabbitTemplate template = new RabbitTemplate( + this.rabbitAvailableRule.getResource()); + template.convertAndSend("", TEST_PREFIX + "dlqtest.default", "foo"); + + int n = 0; + while (n++ < 100) { + Object deadLetter = template + .receiveAndConvert(TEST_PREFIX + "dlqtest.default.dlq"); + if (deadLetter != null) { + assertThat(deadLetter).isEqualTo("foo"); + break; + } + Thread.sleep(100); + } + assertThat(n).isLessThan(100); + + template.convertAndSend("", TEST_PREFIX + "dlqtest2.default", "bar"); + + n = 0; + while (n++ < 100) { + Object deadLetter = template + .receiveAndConvert(TEST_PREFIX + "dlqtest2.default.dlq"); + if (deadLetter != null) { + assertThat(deadLetter).isEqualTo("bar"); + break; + } + Thread.sleep(100); + } + assertThat(n).isLessThan(100); + + consumerBinding.unbind(); + + ApplicationContext context = TestUtils.getPropertyValue(binder, + "binder.provisioningProvider.autoDeclareContext", + ApplicationContext.class); + assertThat(context.containsBean(TEST_PREFIX + "dlqtest.default.binding")) + .isFalse(); + assertThat(context.containsBean(TEST_PREFIX + "dlqtest.default")).isFalse(); + assertThat(context.containsBean(TEST_PREFIX + "dlqtest.default.dlq.binding")) + .isFalse(); + assertThat(context.containsBean(TEST_PREFIX + "dlqtest.default.dlq")).isFalse(); + + verifyAutoDeclareContextClear(binder); + } + + + @Test + public void testAutoBindDLQManualAcks() throws Exception { + RabbitTestBinder binder = getBinder(); + ExtendedConsumerProperties consumerProperties = createConsumerProperties(); + consumerProperties.getExtension().setPrefix(TEST_PREFIX); + consumerProperties.getExtension().setAutoBindDlq(true); + consumerProperties.setMaxAttempts(2); + consumerProperties.getExtension().setDurableSubscription(true); + consumerProperties.getExtension().setAcknowledgeMode(AcknowledgeMode.MANUAL); + BindingProperties bindingProperties = createConsumerBindingProperties( + consumerProperties); + DirectChannel moduleInputChannel = createBindableChannel("input", + bindingProperties); + moduleInputChannel.setBeanName("dlqTestManual"); + Client client = new Client("http://guest:guest@localhost:15672/api"); + moduleInputChannel.subscribe(new MessageHandler() { + + @Override + public void handleMessage(Message message) throws MessagingException { + // Wait until the unacked state is reflected in the admin + QueueInfo info = client.getQueue("/", TEST_PREFIX + "dlqTestManual.default"); + int n = 0; + while (n++ < 100 && info.getMessagesUnacknowledged() < 1L) { + try { + Thread.sleep(100); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + info = client.getQueue("/", TEST_PREFIX + "dlqTestManual.default"); + } + throw new RuntimeException("foo"); + } + + }); + Binding consumerBinding = binder.bindConsumer("dlqTestManual", + "default", moduleInputChannel, consumerProperties); + + RabbitTemplate template = new RabbitTemplate( + this.rabbitAvailableRule.getResource()); + template.convertAndSend("", TEST_PREFIX + "dlqTestManual.default", "foo"); + + int n = 0; + while (n++ < 100) { + Object deadLetter = template + .receiveAndConvert(TEST_PREFIX + "dlqTestManual.default.dlq"); + if (deadLetter != null) { + assertThat(deadLetter).isEqualTo("foo"); + break; + } + Thread.sleep(100); + } + assertThat(n).isLessThan(100); + + n = 0; + QueueInfo info = client.getQueue("/", TEST_PREFIX + "dlqTestManual.default"); + while (n++ < 100 && info.getMessagesUnacknowledged() > 0L) { + Thread.sleep(100); + info = client.getQueue("/", TEST_PREFIX + "dlqTestManual.default"); + } + assertThat(info.getMessagesUnacknowledged()).isEqualTo(0L); + + consumerBinding.unbind(); + + ApplicationContext context = TestUtils.getPropertyValue(binder, + "binder.provisioningProvider.autoDeclareContext", + ApplicationContext.class); + assertThat(context.containsBean(TEST_PREFIX + "dlqTestManual.default.binding")) + .isFalse(); + assertThat(context.containsBean(TEST_PREFIX + "dlqTestManual.default")).isFalse(); + assertThat(context.containsBean(TEST_PREFIX + "dlqTestManual.default.dlq.binding")) + .isFalse(); + assertThat(context.containsBean(TEST_PREFIX + "dlqTestManual.default.dlq")).isFalse(); + + verifyAutoDeclareContextClear(binder); + } + + + @Test + public void testAutoBindDLQPartionedConsumerFirst(TestInfo testInfo) throws Exception { + RabbitTestBinder binder = getBinder(); + ExtendedConsumerProperties properties = createConsumerProperties(); + properties.getExtension().setPrefix("bindertest."); + properties.getExtension().setAutoBindDlq(true); + properties.setMaxAttempts(1); // disable retry + properties.setPartitioned(true); + properties.setInstanceIndex(0); + DirectChannel input0 = createBindableChannel("input", + createConsumerBindingProperties(properties)); + input0.setBeanName("test.input0DLQ"); + Binding input0Binding = binder.bindConsumer("partDLQ.0", + "dlqPartGrp", input0, properties); + Binding defaultConsumerBinding1 = binder.bindConsumer("partDLQ.0", + "default", new QueueChannel(), properties); + properties.setInstanceIndex(1); + DirectChannel input1 = createBindableChannel("input1", + createConsumerBindingProperties(properties)); + input1.setBeanName("test.input1DLQ"); + Binding input1Binding = binder.bindConsumer("partDLQ.0", + "dlqPartGrp", input1, properties); + Binding defaultConsumerBinding2 = binder.bindConsumer("partDLQ.0", + "default", new QueueChannel(), properties); + + ExtendedProducerProperties producerProperties = createProducerProperties(testInfo); + producerProperties.getExtension().setPrefix("bindertest."); + this.applicationContext.registerBean("pkExtractor", PartitionTestSupport.class, () -> new PartitionTestSupport()); + this.applicationContext.registerBean("pkSelector", PartitionTestSupport.class, () -> new PartitionTestSupport()); + producerProperties.getExtension().setAutoBindDlq(true); + producerProperties.setPartitionKeyExtractorName("pkExtractor"); + producerProperties.setPartitionSelectorName("pkSelector"); + producerProperties.setPartitionCount(2); + BindingProperties bindingProperties = createProducerBindingProperties( + producerProperties); + DirectChannel output = createBindableChannel("output", bindingProperties); + output.setBeanName("test.output"); + Binding outputBinding = binder.bindProducer("partDLQ.0", output, + producerProperties); + + final CountDownLatch latch0 = new CountDownLatch(1); + input0.subscribe(new MessageHandler() { + + @Override + public void handleMessage(Message message) throws MessagingException { + if (latch0.getCount() <= 0) { + throw new RuntimeException("dlq"); + } + latch0.countDown(); + } + + }); + + final CountDownLatch latch1 = new CountDownLatch(1); + input1.subscribe(new MessageHandler() { + + @Override + public void handleMessage(Message message) throws MessagingException { + if (latch1.getCount() <= 0) { + throw new RuntimeException("dlq"); + } + latch1.countDown(); + } + + }); + + output.send(new GenericMessage<>(1)); + assertThat(latch1.await(10, TimeUnit.SECONDS)).isTrue(); + + output.send(new GenericMessage<>(0)); + assertThat(latch0.await(10, TimeUnit.SECONDS)).isTrue(); + + output.send(new GenericMessage<>(1)); + + RabbitTemplate template = new RabbitTemplate( + this.rabbitAvailableRule.getResource()); + template.setReceiveTimeout(10000); + + String streamDLQName = "bindertest.partDLQ.0.dlqPartGrp.dlq"; + + org.springframework.amqp.core.Message received = template.receive(streamDLQName); + assertThat(received).isNotNull(); + assertThat(received.getMessageProperties().getReceivedRoutingKey()) + .isEqualTo("bindertest.partDLQ.0.dlqPartGrp-1"); + assertThat(received.getMessageProperties().getHeaders()) + .doesNotContainKey(BinderHeaders.PARTITION_HEADER); + + output.send(new GenericMessage<>(0)); + received = template.receive(streamDLQName); + assertThat(received).isNotNull(); + assertThat(received.getMessageProperties().getReceivedRoutingKey()) + .isEqualTo("bindertest.partDLQ.0.dlqPartGrp-0"); + assertThat(received.getMessageProperties().getHeaders()) + .doesNotContainKey(BinderHeaders.PARTITION_HEADER); + + input0Binding.unbind(); + input1Binding.unbind(); + defaultConsumerBinding1.unbind(); + defaultConsumerBinding2.unbind(); + outputBinding.unbind(); + + verifyAutoDeclareContextClear(binder); + } + + + @Test + @Disabled + public void testAutoBindDLQPartitionedConsumerFirstWithRepublishNoRetry(TestInfo testInfo) + throws Exception { + testAutoBindDLQPartionedConsumerFirstWithRepublishGuts(false, testInfo); + } + + @Test + public void testAutoBindDLQPartitionedConsumerFirstWithRepublishWithRetry(TestInfo testInfo) + throws Exception { + testAutoBindDLQPartionedConsumerFirstWithRepublishGuts(true, testInfo); + } + + private void testAutoBindDLQPartionedConsumerFirstWithRepublishGuts( + final boolean withRetry, TestInfo testInfo) throws Exception { + RabbitTestBinder binder = getBinder(); + ExtendedConsumerProperties properties = createConsumerProperties(); + properties.getExtension().setPrefix("bindertest."); + properties.getExtension().setAutoBindDlq(true); + properties.getExtension().setRepublishToDlq(true); + properties.getExtension() + .setRepublishDeliveyMode(MessageDeliveryMode.NON_PERSISTENT); + properties.setMaxAttempts(withRetry ? 2 : 1); + properties.setPartitioned(true); + properties.setInstanceIndex(0); + DirectChannel input0 = createBindableChannel("input", + createConsumerBindingProperties(properties)); + input0.setBeanName("test.input0DLQ"); + Binding input0Binding = binder.bindConsumer("partPubDLQ.0", + "dlqPartGrp", input0, properties); + Binding defaultConsumerBinding1 = binder + .bindConsumer("partPubDLQ.0", "default", new QueueChannel(), properties); + properties.setInstanceIndex(1); + DirectChannel input1 = createBindableChannel("input1", + createConsumerBindingProperties(properties)); + input1.setBeanName("test.input1DLQ"); + Binding input1Binding = binder.bindConsumer("partPubDLQ.0", + "dlqPartGrp", input1, properties); + Binding defaultConsumerBinding2 = binder + .bindConsumer("partPubDLQ.0", "default", new QueueChannel(), properties); + + ExtendedProducerProperties producerProperties = createProducerProperties(testInfo); + producerProperties.getExtension().setPrefix("bindertest."); + producerProperties.getExtension().setAutoBindDlq(true); + this.applicationContext.registerBean("pkExtractor", PartitionTestSupport.class, () -> new PartitionTestSupport()); + this.applicationContext.registerBean("pkSelector", PartitionTestSupport.class, () -> new PartitionTestSupport()); + producerProperties.setPartitionKeyExtractorName("pkExtractor"); + producerProperties.setPartitionSelectorName("pkSelector"); + producerProperties.setPartitionCount(2); + BindingProperties bindingProperties = createProducerBindingProperties( + producerProperties); + DirectChannel output = createBindableChannel("output", bindingProperties); + output.setBeanName("test.output"); + Binding outputBinding = binder.bindProducer("partPubDLQ.0", + output, producerProperties); + + final CountDownLatch latch0 = new CountDownLatch(1); + input0.subscribe(new MessageHandler() { + + @Override + public void handleMessage(Message message) throws MessagingException { + if (latch0.getCount() <= 0) { + throw new RuntimeException("dlq"); + } + latch0.countDown(); + } + + }); + + final CountDownLatch latch1 = new CountDownLatch(1); + input1.subscribe(new MessageHandler() { + + @Override + public void handleMessage(Message message) throws MessagingException { + if (latch1.getCount() <= 0) { + throw new RuntimeException("dlq"); + } + latch1.countDown(); + } + + }); + + ApplicationContext context = TestUtils.getPropertyValue(binder.getBinder(), + "applicationContext", ApplicationContext.class); + SubscribableChannel boundErrorChannel = context.getBean( + "bindertest.partPubDLQ.0.dlqPartGrp-0.errors", SubscribableChannel.class); + SubscribableChannel globalErrorChannel = context.getBean("errorChannel", + SubscribableChannel.class); + final AtomicReference> boundErrorChannelMessage = new AtomicReference<>(); + final AtomicReference> globalErrorChannelMessage = new AtomicReference<>(); + final AtomicBoolean hasRecovererInCallStack = new AtomicBoolean(!withRetry); + boundErrorChannel.subscribe(new MessageHandler() { + + @Override + public void handleMessage(Message message) throws MessagingException { + boundErrorChannelMessage.set(message); + String stackTrace = Arrays + .toString(new RuntimeException().getStackTrace()); + hasRecovererInCallStack + .set(stackTrace.contains("ErrorMessageSendingRecoverer")); + } + + }); + globalErrorChannel.subscribe(new MessageHandler() { + + @Override + public void handleMessage(Message message) throws MessagingException { + globalErrorChannelMessage.set(message); + } + + }); + + output.send(new GenericMessage<>(1)); + assertThat(latch1.await(10, TimeUnit.SECONDS)).isTrue(); + + output.send(new GenericMessage<>(0)); + assertThat(latch0.await(10, TimeUnit.SECONDS)).isTrue(); + + output.send(new GenericMessage<>(1)); + + RabbitTemplate template = new RabbitTemplate( + this.rabbitAvailableRule.getResource()); + template.setReceiveTimeout(10000); + + String streamDLQName = "bindertest.partPubDLQ.0.dlqPartGrp.dlq"; + + org.springframework.amqp.core.Message received = template.receive(streamDLQName); + assertThat(received).isNotNull(); + assertThat( + received.getMessageProperties().getHeaders().get("x-original-routingKey")) + .isEqualTo("partPubDLQ.0-1"); + assertThat(received.getMessageProperties().getHeaders()) + .doesNotContainKey(BinderHeaders.PARTITION_HEADER); + assertThat(received.getMessageProperties().getReceivedDeliveryMode()) + .isEqualTo(MessageDeliveryMode.NON_PERSISTENT); + + output.send(new GenericMessage<>(0)); + received = template.receive(streamDLQName); + assertThat(received).isNotNull(); + assertThat( + received.getMessageProperties().getHeaders().get("x-original-routingKey")) + .isEqualTo("partPubDLQ.0-0"); + assertThat(received.getMessageProperties().getHeaders()) + .doesNotContainKey(BinderHeaders.PARTITION_HEADER); + + // verify we got a message on the dedicated error channel and the global (via + // bridge) + assertThat(boundErrorChannelMessage.get()).isNotNull(); + assertThat(globalErrorChannelMessage.get()).isNotNull(); + assertThat(hasRecovererInCallStack.get()).isEqualTo(withRetry); + + input0Binding.unbind(); + input1Binding.unbind(); + defaultConsumerBinding1.unbind(); + defaultConsumerBinding2.unbind(); + outputBinding.unbind(); + + verifyAutoDeclareContextClear(binder); + } + + @Test + public void testAutoBindDLQPartitionedProducerFirst(TestInfo testInfo) throws Exception { + RabbitTestBinder binder = getBinder(); + ExtendedProducerProperties properties = createProducerProperties(testInfo); + + properties.getExtension().setPrefix("bindertest."); + properties.getExtension().setAutoBindDlq(true); + properties.setRequiredGroups("dlqPartGrp"); + this.applicationContext.registerBean("pkExtractor", PartitionTestSupport.class, () -> new PartitionTestSupport()); + properties.setPartitionKeyExtractorName("pkExtractor"); + properties.setPartitionSelectorName("pkExtractor"); + properties.setPartitionCount(2); + DirectChannel output = createBindableChannel("output", + createProducerBindingProperties(properties)); + output.setBeanName("test.output"); + Binding outputBinding = binder.bindProducer("partDLQ.1", output, + properties); + + ExtendedConsumerProperties consumerProperties = createConsumerProperties(); + consumerProperties.getExtension().setPrefix("bindertest."); + consumerProperties.getExtension().setAutoBindDlq(true); + consumerProperties.setMaxAttempts(1); // disable retry + consumerProperties.setPartitioned(true); + consumerProperties.setInstanceIndex(0); + DirectChannel input0 = createBindableChannel("input", + createConsumerBindingProperties(consumerProperties)); + input0.setBeanName("test.input0DLQ"); + Binding input0Binding = binder.bindConsumer("partDLQ.1", + "dlqPartGrp", input0, consumerProperties); + Binding defaultConsumerBinding1 = binder.bindConsumer("partDLQ.1", + "defaultConsumer", new QueueChannel(), consumerProperties); + consumerProperties.setInstanceIndex(1); + DirectChannel input1 = createBindableChannel("input1", + createConsumerBindingProperties(consumerProperties)); + input1.setBeanName("test.input1DLQ"); + Binding input1Binding = binder.bindConsumer("partDLQ.1", + "dlqPartGrp", input1, consumerProperties); + Binding defaultConsumerBinding2 = binder.bindConsumer("partDLQ.1", + "defaultConsumer", new QueueChannel(), consumerProperties); + + final CountDownLatch latch0 = new CountDownLatch(1); + input0.subscribe(new MessageHandler() { + + @Override + public void handleMessage(Message message) throws MessagingException { + if (latch0.getCount() <= 0) { + throw new RuntimeException("dlq"); + } + latch0.countDown(); + } + + }); + + final CountDownLatch latch1 = new CountDownLatch(1); + input1.subscribe(new MessageHandler() { + + @Override + public void handleMessage(Message message) throws MessagingException { + if (latch1.getCount() <= 0) { + throw new RuntimeException("dlq"); + } + latch1.countDown(); + } + + }); + + output.send(new GenericMessage(1)); + assertThat(latch1.await(10, TimeUnit.SECONDS)).isTrue(); + + output.send(new GenericMessage(0)); + assertThat(latch0.await(10, TimeUnit.SECONDS)).isTrue(); + + output.send(new GenericMessage(1)); + + RabbitTemplate template = new RabbitTemplate( + this.rabbitAvailableRule.getResource()); + template.setReceiveTimeout(10000); + + String streamDLQName = "bindertest.partDLQ.1.dlqPartGrp.dlq"; + + org.springframework.amqp.core.Message received = template.receive(streamDLQName); + assertThat(received).isNotNull(); + assertThat(received.getMessageProperties().getReceivedRoutingKey()) + .isEqualTo("bindertest.partDLQ.1.dlqPartGrp-1"); + assertThat(received.getMessageProperties().getHeaders()) + .doesNotContainKey(BinderHeaders.PARTITION_HEADER); + assertThat(received.getMessageProperties().getReceivedDeliveryMode()) + .isEqualTo(MessageDeliveryMode.PERSISTENT); + + output.send(new GenericMessage(0)); + received = template.receive(streamDLQName); + assertThat(received).isNotNull(); + assertThat(received.getMessageProperties().getReceivedRoutingKey()) + .isEqualTo("bindertest.partDLQ.1.dlqPartGrp-0"); + assertThat(received.getMessageProperties().getHeaders()) + .doesNotContainKey(BinderHeaders.PARTITION_HEADER); + + input0Binding.unbind(); + input1Binding.unbind(); + defaultConsumerBinding1.unbind(); + defaultConsumerBinding2.unbind(); + outputBinding.unbind(); + + verifyAutoDeclareContextClear(binder); + } + + + @Test + public void testAutoBindDLQwithRepublish() throws Exception { + this.maxStackTraceSize = RabbitUtils + .getMaxFrame(rabbitAvailableRule.getResource()) - 20_000; + assertThat(this.maxStackTraceSize).isGreaterThan(0); + + RabbitTestBinder binder = getBinder(); + ExtendedConsumerProperties consumerProperties = createConsumerProperties(); + consumerProperties.getExtension().setPrefix(TEST_PREFIX); + consumerProperties.getExtension().setAutoBindDlq(true); + consumerProperties.getExtension().setRepublishToDlq(true); + consumerProperties.setMaxAttempts(1); // disable retry + consumerProperties.getExtension().setDurableSubscription(true); + DirectChannel moduleInputChannel = createBindableChannel("input", + createConsumerBindingProperties(consumerProperties)); + moduleInputChannel.setBeanName("dlqPubTest"); + RuntimeException exception = bigCause( + new RuntimeException(BIG_EXCEPTION_MESSAGE)); + assertThat(getStackTraceAsString(exception).length()) + .isGreaterThan(this.maxStackTraceSize); + AtomicBoolean dontRepublish = new AtomicBoolean(); + moduleInputChannel.subscribe(new MessageHandler() { + + @Override + public void handleMessage(Message message) throws MessagingException { + if (dontRepublish.get()) { + throw new ImmediateAcknowledgeAmqpException("testDontRepublish"); + } + throw exception; + } + + }); + consumerProperties.setMultiplex(true); + Binding consumerBinding = binder.bindConsumer( + "foo.dlqpubtest,foo.dlqpubtest2", "foo", moduleInputChannel, + consumerProperties); + + RabbitTemplate template = new RabbitTemplate( + this.rabbitAvailableRule.getResource()); + template.convertAndSend("", TEST_PREFIX + "foo.dlqpubtest.foo", "foo"); + + template.setReceiveTimeout(10_000); + org.springframework.amqp.core.Message deadLetter = template + .receive(TEST_PREFIX + "foo.dlqpubtest.foo.dlq"); + assertThat(deadLetter).isNotNull(); + assertThat(new String(deadLetter.getBody())).isEqualTo("foo"); + assertThat(deadLetter.getMessageProperties().getHeaders()) + .containsKey((RepublishMessageRecoverer.X_EXCEPTION_STACKTRACE)); + assertThat(((LongString) deadLetter.getMessageProperties().getHeaders() + .get(RepublishMessageRecoverer.X_EXCEPTION_STACKTRACE)).length()) + .isEqualTo(this.maxStackTraceSize); + + template.convertAndSend("", TEST_PREFIX + "foo.dlqpubtest2.foo", "bar"); + + deadLetter = template.receive(TEST_PREFIX + "foo.dlqpubtest2.foo.dlq"); + assertThat(deadLetter).isNotNull(); + assertThat(new String(deadLetter.getBody())).isEqualTo("bar"); + assertThat(deadLetter.getMessageProperties().getHeaders()) + .containsKey(("x-exception-stacktrace")); + + dontRepublish.set(true); + template.convertAndSend("", TEST_PREFIX + "foo.dlqpubtest2.foo", "baz"); + template.setReceiveTimeout(500); + assertThat(template.receive(TEST_PREFIX + "foo.dlqpubtest2.foo.dlq")).isNull(); + + consumerBinding.unbind(); + + verifyAutoDeclareContextClear(binder); + } + + + @SuppressWarnings("unchecked") + @Test + public void testAutoBindDLQwithRepublishTx() throws Exception { + RabbitTestBinder binder = getBinder(); + ExtendedConsumerProperties consumerProperties = createConsumerProperties(); + consumerProperties.getExtension().setPrefix(TEST_PREFIX); + consumerProperties.getExtension().setAutoBindDlq(true); + consumerProperties.getExtension().setRepublishToDlq(true); + consumerProperties.setMaxAttempts(1); // disable retry + consumerProperties.getExtension().setDurableSubscription(true); + consumerProperties.getExtension().setTransacted(true); + DirectChannel moduleInputChannel = createBindableChannel("input", + createConsumerBindingProperties(consumerProperties)); + moduleInputChannel.setBeanName("dlqPubTestTx"); + moduleInputChannel.subscribe(new MessageHandler() { + + @Override + public void handleMessage(Message message) throws MessagingException { + throw new RuntimeException("test"); + } + + }); + Binding consumerBinding = binder.bindConsumer( + "foo.dlqpubtestTx", "foo", moduleInputChannel, consumerProperties); + + RabbitTemplate template = new RabbitTemplate(this.rabbitAvailableRule.getResource()); + template.convertAndSend("", TEST_PREFIX + "foo.dlqpubtestTx.foo", "foo"); + + template.setReceiveTimeout(10_000); + org.springframework.amqp.core.Message deadLetter = template + .receive(TEST_PREFIX + "foo.dlqpubtestTx.foo.dlq"); + assertThat(deadLetter).isNotNull(); + assertThat(deadLetter.getBody()).isEqualTo("foo".getBytes()); + assertThat(deadLetter.getMessageProperties().getHeaders()) + .containsKey((RepublishMessageRecoverer.X_EXCEPTION_STACKTRACE)); + List errorHandler = (List) TestUtils.getPropertyValue(consumerBinding, + "lifecycle.errorChannel.dispatcher.handlers", Set.class).stream() + .filter(handler -> !handler.getClass().equals(BridgeHandler.class)) + .collect(Collectors.toList()); + assertThat(errorHandler).hasSize(1); + assertThat(TestUtils.getPropertyValue(errorHandler.get(0), "template.transactional", Boolean.class)).isTrue(); + assertThat(TestUtils.getPropertyValue(errorHandler.get(0), "confirmType", ConfirmType.class)) + .isEqualTo(ConfirmType.NONE); + consumerBinding.unbind(); + + verifyAutoDeclareContextClear(binder); + } + + + @SuppressWarnings("unchecked") + @Test + public void testAutoBindDLQwithRepublishSimpleConfirms() throws Exception { + CachingConnectionFactory ccf = this.rabbitAvailableRule.getResource(); + ccf.setPublisherReturns(true); + ccf.setPublisherConfirmType(ConfirmType.SIMPLE); + ccf.resetConnection(); + RabbitTestBinder binder = getBinder(); + ExtendedConsumerProperties consumerProperties = createConsumerProperties(); + consumerProperties.getExtension().setPrefix(TEST_PREFIX); + consumerProperties.getExtension().setAutoBindDlq(true); + consumerProperties.getExtension().setRepublishToDlq(true); + consumerProperties.setMaxAttempts(1); // disable retry + consumerProperties.getExtension().setDurableSubscription(true); + DirectChannel moduleInputChannel = createBindableChannel("input", + createConsumerBindingProperties(consumerProperties)); + moduleInputChannel.setBeanName("dlqPubtestSimple"); + moduleInputChannel.subscribe(new MessageHandler() { + + @Override + public void handleMessage(Message message) throws MessagingException { + throw new RuntimeException("test"); + } + + }); + Binding consumerBinding = binder.bindConsumer( + "foo.dlqpubtestSimple", "foo", moduleInputChannel, consumerProperties); + + RabbitTemplate template = new RabbitTemplate(this.rabbitAvailableRule.getResource()); + template.convertAndSend("", TEST_PREFIX + "foo.dlqpubtestSimple.foo", "foo"); + + template.setReceiveTimeout(10_000); + org.springframework.amqp.core.Message deadLetter = template + .receive(TEST_PREFIX + "foo.dlqpubtestSimple.foo.dlq"); + assertThat(deadLetter).isNotNull(); + assertThat(deadLetter.getBody()).isEqualTo("foo".getBytes()); + assertThat(deadLetter.getMessageProperties().getHeaders()) + .containsKey((RepublishMessageRecoverer.X_EXCEPTION_STACKTRACE)); + List errorHandler = (List) TestUtils.getPropertyValue(consumerBinding, + "lifecycle.errorChannel.dispatcher.handlers", Set.class).stream() + .filter(handler -> !handler.getClass().equals(BridgeHandler.class)) + .collect(Collectors.toList()); + assertThat(errorHandler).hasSize(1); + assertThat(TestUtils.getPropertyValue(errorHandler.get(0), "confirmType", ConfirmType.class)) + .isEqualTo(ConfirmType.SIMPLE); + consumerBinding.unbind(); + + verifyAutoDeclareContextClear(binder); + } + + + @SuppressWarnings("unchecked") + @Test + public void testAutoBindDLQwithRepublishCorrelatedConfirms() throws Exception { + CachingConnectionFactory ccf = this.rabbitAvailableRule.getResource(); + ccf.setPublisherReturns(true); + ccf.setPublisherConfirmType(ConfirmType.CORRELATED); + ccf.resetConnection(); + RabbitTestBinder binder = getBinder(); + ExtendedConsumerProperties consumerProperties = createConsumerProperties(); + consumerProperties.getExtension().setPrefix(TEST_PREFIX); + consumerProperties.getExtension().setAutoBindDlq(true); + consumerProperties.getExtension().setRepublishToDlq(true); + consumerProperties.setMaxAttempts(1); // disable retry + consumerProperties.getExtension().setDurableSubscription(true); + DirectChannel moduleInputChannel = createBindableChannel("input", + createConsumerBindingProperties(consumerProperties)); + moduleInputChannel.setBeanName("dlqPubtestCorrelated"); + moduleInputChannel.subscribe(new MessageHandler() { + + @Override + public void handleMessage(Message message) throws MessagingException { + throw new RuntimeException("test"); + } + + }); + Binding consumerBinding = binder.bindConsumer( + "foo.dlqpubtestCorrelated", "foo", moduleInputChannel, consumerProperties); + + RabbitTemplate template = new RabbitTemplate(this.rabbitAvailableRule.getResource()); + template.convertAndSend("", TEST_PREFIX + "foo.dlqpubtestCorrelated.foo", "foo"); + + template.setReceiveTimeout(10_000); + org.springframework.amqp.core.Message deadLetter = template + .receive(TEST_PREFIX + "foo.dlqpubtestCorrelated.foo.dlq"); + assertThat(deadLetter).isNotNull(); + assertThat(deadLetter.getBody()).isEqualTo("foo".getBytes()); + assertThat(deadLetter.getMessageProperties().getHeaders()) + .containsKey((RepublishMessageRecoverer.X_EXCEPTION_STACKTRACE)); + List errorHandler = (List) TestUtils.getPropertyValue(consumerBinding, + "lifecycle.errorChannel.dispatcher.handlers", Set.class).stream() + .filter(handler -> !handler.getClass().equals(BridgeHandler.class)) + .collect(Collectors.toList()); + assertThat(errorHandler).hasSize(1); + assertThat(TestUtils.getPropertyValue(errorHandler.get(0), "confirmType", ConfirmType.class)) + .isEqualTo(ConfirmType.CORRELATED); + consumerBinding.unbind(); + + verifyAutoDeclareContextClear(binder); + } + + + @SuppressWarnings("unchecked") + @Test + public void testBatchingAndCompression(TestInfo testInfo) throws Exception { + RabbitTestBinder binder = getBinder(); + ExtendedProducerProperties producerProperties = createProducerProperties(testInfo); + producerProperties.getExtension() + .setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT); + producerProperties.getExtension().setBatchingEnabled(true); + producerProperties.getExtension().setBatchSize(2); + producerProperties.getExtension().setBatchBufferLimit(100000); + producerProperties.getExtension().setBatchTimeout(30000); + producerProperties.getExtension().setCompress(true); + producerProperties.setRequiredGroups("default"); + + DirectChannel output = createBindableChannel("output", + createProducerBindingProperties(producerProperties)); + output.setBeanName("batchingProducer"); + Binding producerBinding = binder.bindProducer("batching.0", + output, producerProperties); + + Log logger = spy(TestUtils.getPropertyValue(binder, + "binder.compressingPostProcessor.logger", Log.class)); + new DirectFieldAccessor( + TestUtils.getPropertyValue(binder, "binder.compressingPostProcessor")) + .setPropertyValue("logger", logger); + when(logger.isTraceEnabled()).thenReturn(true); + + assertThat(TestUtils.getPropertyValue(binder, + "binder.compressingPostProcessor.level")).isEqualTo(Deflater.BEST_SPEED); + + output.send(new GenericMessage<>("foo".getBytes())); + output.send(new GenericMessage<>("bar".getBytes())); + + Object out = spyOn("batching.0.default").receive(false); + assertThat(out).isInstanceOf(byte[].class); + assertThat(new String((byte[]) out)) + .isEqualTo("\u0000\u0000\u0000\u0003foo\u0000\u0000\u0000\u0003bar"); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Object.class); + verify(logger).trace(captor.capture()); + assertThat(captor.getValue().toString()).contains(("Compressed 14 to ")); + + QueueChannel input = new QueueChannel(); + input.setBeanName("batchingConsumer"); + Binding consumerBinding = binder.bindConsumer("batching.0", + "test", input, createConsumerProperties()); + + output.send(new GenericMessage<>("foo".getBytes())); + output.send(new GenericMessage<>("bar".getBytes())); + + Message in = (Message) input.receive(10000); + assertThat(in).isNotNull(); + assertThat(new String(in.getPayload())).isEqualTo("foo"); + in = (Message) input.receive(10000); + assertThat(in).isNotNull(); + assertThat(new String(in.getPayload())).isEqualTo("bar"); + assertThat(in.getHeaders().get(AmqpHeaders.DELIVERY_MODE)).isNull(); + + producerBinding.unbind(); + consumerBinding.unbind(); + + verifyAutoDeclareContextClear(binder); + } + + + @SuppressWarnings("unchecked") + @Test + public void testProducerBatching(TestInfo testInfo) throws Exception { + RabbitTestBinder binder = getBinder(); + ExtendedProducerProperties producerProperties = createProducerProperties(testInfo); + producerProperties.getExtension() + .setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT); + producerProperties.getExtension().setBatchingEnabled(true); + producerProperties.getExtension().setBatchSize(2); + producerProperties.getExtension().setBatchBufferLimit(100000); + producerProperties.getExtension().setBatchTimeout(30000); + producerProperties.getExtension().setCompress(true); + + DirectChannel output = createBindableChannel("output", + createProducerBindingProperties(producerProperties)); + output.setBeanName("producerBatchingProducer"); + Binding producerBinding = binder.bindProducer("p.batching.0", + output, producerProperties); + + QueueChannel input = new QueueChannel(); + input.setBeanName("producerBatchingConsumer"); + ExtendedConsumerProperties consumerProperties = createConsumerProperties(); + consumerProperties.setBatchMode(true); + consumerProperties.getExtension().setBatchSize(2); + Binding consumerBinding = binder.bindConsumer("p.batching.0", + "producerBatching", input, consumerProperties); + output.send(new GenericMessage<>("foo".getBytes())); + output.send(new GenericMessage<>("bar".getBytes())); + + + Message in = input.receive(10000); + assertThat(in).isNotNull(); + assertThat(in.getPayload()).isInstanceOf(List.class); + List payload = (List) in.getPayload(); + assertThat(payload).hasSize(2); + assertThat(payload.get(0)).isEqualTo("foo".getBytes()); + assertThat(payload.get(1)).isEqualTo("bar".getBytes()); + + producerBinding.unbind(); + consumerBinding.unbind(); + + verifyAutoDeclareContextClear(binder); + } + + + @SuppressWarnings("unchecked") + @Test + public void testConsumerBatching(TestInfo testInfo) throws Exception { + RabbitTestBinder binder = getBinder(); + ExtendedProducerProperties producerProperties = createProducerProperties(testInfo); + producerProperties.getExtension() + .setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT); + + DirectChannel output = createBindableChannel("output", + createProducerBindingProperties(producerProperties)); + output.setBeanName("consumerBatching.Producer"); + Binding producerBinding = binder.bindProducer("c.batching.0", + output, producerProperties); + + QueueChannel input = new QueueChannel(); + input.setBeanName("batchingConsumer"); + ExtendedConsumerProperties consumerProperties = createConsumerProperties(); + consumerProperties.setBatchMode(true); + consumerProperties.getExtension().setBatchSize(2); + consumerProperties.getExtension().setEnableBatching(true); + Binding consumerBinding = binder.bindConsumer("c.batching.0", + "consumerBatching", input, consumerProperties); + output.send(new GenericMessage<>("foo".getBytes())); + output.send(new GenericMessage<>("bar".getBytes())); + + + Message in = input.receive(10000); + assertThat(in).isNotNull(); + assertThat(in.getPayload()).isInstanceOf(List.class); + List payload = (List) in.getPayload(); + assertThat(payload).hasSize(2); + assertThat(payload.get(0)).isEqualTo("foo".getBytes()); + assertThat(payload.get(1)).isEqualTo("bar".getBytes()); + + producerBinding.unbind(); + consumerBinding.unbind(); + + verifyAutoDeclareContextClear(binder); + } + + + @SuppressWarnings("unchecked") + @Test + public void testInternalHeadersNotPropagated(TestInfo testInfo) throws Exception { + RabbitTestBinder binder = getBinder(); + ExtendedProducerProperties producerProperties = createProducerProperties(testInfo); + producerProperties.getExtension() + .setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT); + + DirectChannel output = createBindableChannel("output", + createProducerBindingProperties(producerProperties)); + output.setBeanName("propagate.out"); + Binding producerBinding = binder.bindProducer("propagate.1", + output, producerProperties); + + QueueChannel input = new QueueChannel(); + input.setBeanName("propagate.in"); + ExtendedConsumerProperties consumerProperties = createConsumerProperties(); + Binding consumerBinding = binder.bindConsumer("propagate.0", + "propagate", input, consumerProperties); + RabbitAdmin admin = new RabbitAdmin(rabbitAvailableRule.getResource()); + admin.declareQueue(new Queue("propagate")); + admin.declareBinding(new org.springframework.amqp.core.Binding("propagate", DestinationType.QUEUE, + "propagate.1", "#", null)); + RabbitTemplate template = new RabbitTemplate(this.rabbitAvailableRule.getResource()); + template.convertAndSend("propagate.0.propagate", "foo"); + output.send(input.receive(10_000)); + org.springframework.amqp.core.Message received = template.receive("propagate", 10_000); + assertThat(received).isNotNull(); + assertThat(received.getBody()).isEqualTo("foo".getBytes()); + Object header = received.getMessageProperties().getHeader(IntegrationMessageHeaderAccessor.SOURCE_DATA); + assertThat(header).isNull(); + header = received.getMessageProperties().getHeader(IntegrationMessageHeaderAccessor.DELIVERY_ATTEMPT); + assertThat(header).isNull(); + + producerBinding.unbind(); + consumerBinding.unbind(); + admin.deleteQueue("propagate"); + + verifyAutoDeclareContextClear(binder); + } + + + /* + * Test late binding due to broker down; queues with and without DLQs, and partitioned + * queues. + */ + @Test + public void testLateBinding(TestInfo testInfo) throws Exception { + RabbitTestSupport.RabbitProxy proxy = new RabbitTestSupport.RabbitProxy(); + CachingConnectionFactory cf = new CachingConnectionFactory("localhost", + proxy.getPort()); + + RabbitMessageChannelBinder rabbitBinder = new RabbitMessageChannelBinder(cf, + new RabbitProperties(), new RabbitExchangeQueueProvisioner(cf)); + RabbitTestBinder binder = new RabbitTestBinder(cf, rabbitBinder); + + ExtendedProducerProperties producerProperties = createProducerProperties(testInfo); + producerProperties.getExtension().setPrefix("latebinder."); + producerProperties.getExtension().setAutoBindDlq(true); + producerProperties.getExtension().setTransacted(true); + + MessageChannel moduleOutputChannel = createBindableChannel("output", + createProducerBindingProperties(producerProperties)); + Binding late0ProducerBinding = binder.bindProducer("late.0", + moduleOutputChannel, producerProperties); + + QueueChannel moduleInputChannel = new QueueChannel(); + ExtendedConsumerProperties rabbitConsumerProperties = createConsumerProperties(); + rabbitConsumerProperties.getExtension().setPrefix("latebinder."); + Binding late0ConsumerBinding = binder.bindConsumer("late.0", + "test", moduleInputChannel, rabbitConsumerProperties); + + producerProperties.setPartitionKeyExpression( + spelExpressionParser.parseExpression("payload.equals('0') ? 0 : 1")); + producerProperties.setPartitionSelectorExpression( + spelExpressionParser.parseExpression("hashCode()")); + producerProperties.setPartitionCount(2); + + MessageChannel partOutputChannel = createBindableChannel("output", + createProducerBindingProperties(producerProperties)); + Binding partlate0ProducerBinding = binder + .bindProducer("partlate.0", partOutputChannel, producerProperties); + + QueueChannel partInputChannel0 = new QueueChannel(); + QueueChannel partInputChannel1 = new QueueChannel(); + + ExtendedConsumerProperties partLateConsumerProperties = createConsumerProperties(); + partLateConsumerProperties.getExtension().setPrefix("latebinder."); + partLateConsumerProperties.setPartitioned(true); + partLateConsumerProperties.setInstanceIndex(0); + Binding partlate0Consumer0Binding = binder.bindConsumer( + "partlate.0", "test", partInputChannel0, partLateConsumerProperties); + partLateConsumerProperties.setInstanceIndex(1); + Binding partlate0Consumer1Binding = binder.bindConsumer( + "partlate.0", "test", partInputChannel1, partLateConsumerProperties); + + ExtendedProducerProperties noDlqProducerProperties = createProducerProperties(testInfo); + noDlqProducerProperties.getExtension().setPrefix("latebinder."); + MessageChannel noDLQOutputChannel = createBindableChannel("output", + createProducerBindingProperties(noDlqProducerProperties)); + Binding noDlqProducerBinding = binder.bindProducer("lateNoDLQ.0", + noDLQOutputChannel, noDlqProducerProperties); + + QueueChannel noDLQInputChannel = new QueueChannel(); + ExtendedConsumerProperties noDlqConsumerProperties = createConsumerProperties(); + noDlqConsumerProperties.getExtension().setPrefix("latebinder."); + Binding noDlqConsumerBinding = binder.bindConsumer("lateNoDLQ.0", + "test", noDLQInputChannel, noDlqConsumerProperties); + + MessageChannel outputChannel = createBindableChannel("output", + createProducerBindingProperties(noDlqProducerProperties)); + Binding pubSubProducerBinding = binder.bindProducer("latePubSub", + outputChannel, noDlqProducerProperties); + QueueChannel pubSubInputChannel = new QueueChannel(); + noDlqConsumerProperties.getExtension().setDurableSubscription(false); + Binding nonDurableConsumerBinding = binder.bindConsumer( + "latePubSub", "lategroup", pubSubInputChannel, noDlqConsumerProperties); + QueueChannel durablePubSubInputChannel = new QueueChannel(); + noDlqConsumerProperties.getExtension().setDurableSubscription(true); + Binding durableConsumerBinding = binder.bindConsumer("latePubSub", + "lateDurableGroup", durablePubSubInputChannel, noDlqConsumerProperties); + + proxy.start(); + + moduleOutputChannel.send(MessageBuilder.withPayload("foo") + .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN) + .build()); + Message message = moduleInputChannel.receive(10000); + assertThat(message).isNotNull(); + assertThat(message.getPayload()).isNotNull(); + + noDLQOutputChannel.send(MessageBuilder.withPayload("bar") + .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN) + .build()); + message = noDLQInputChannel.receive(10000); + assertThat(message); + assertThat(message.getPayload()).isEqualTo("bar".getBytes()); + + outputChannel.send(MessageBuilder.withPayload("baz") + .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN) + .build()); + message = pubSubInputChannel.receive(10000); + assertThat(message); + assertThat(message.getPayload()).isEqualTo("baz".getBytes()); + message = durablePubSubInputChannel.receive(10000); + assertThat(message).isNotNull(); + assertThat(message.getPayload()).isEqualTo("baz".getBytes()); + + partOutputChannel.send(MessageBuilder.withPayload("0") + .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN) + .build()); + partOutputChannel.send(MessageBuilder.withPayload("1") + .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN) + .build()); + message = partInputChannel0.receive(10000); + assertThat(message).isNotNull(); + assertThat(message.getPayload()).isEqualTo("0".getBytes()); + message = partInputChannel1.receive(10000); + assertThat(message).isNotNull(); + assertThat(message.getPayload()).isEqualTo("1".getBytes()); + + late0ProducerBinding.unbind(); + late0ConsumerBinding.unbind(); + partlate0ProducerBinding.unbind(); + partlate0Consumer0Binding.unbind(); + partlate0Consumer1Binding.unbind(); + noDlqProducerBinding.unbind(); + noDlqConsumerBinding.unbind(); + pubSubProducerBinding.unbind(); + nonDurableConsumerBinding.unbind(); + durableConsumerBinding.unbind(); + + binder.cleanup(); + + proxy.stop(); + cf.destroy(); + + this.rabbitAvailableRule.getResource().destroy(); + + verifyAutoDeclareContextClear(binder); + } + + + @Test + public void testBadUserDeclarationsFatal() throws Exception { + RabbitTestBinder binder = getBinder(); + ConfigurableApplicationContext context = binder.getApplicationContext(); + ConfigurableListableBeanFactory bf = context.getBeanFactory(); + bf.registerSingleton("testBadUserDeclarationsFatal", + new Queue("testBadUserDeclarationsFatal", false)); + bf.registerSingleton("binder", binder); + RabbitExchangeQueueProvisioner provisioner = TestUtils.getPropertyValue(binder, + "binder.provisioningProvider", RabbitExchangeQueueProvisioner.class); + bf.initializeBean(provisioner, "provisioner"); + bf.registerSingleton("provisioner", provisioner); + context.addApplicationListener(provisioner); + RabbitAdmin admin = new RabbitAdmin(rabbitAvailableRule.getResource()); + admin.declareQueue(new Queue("testBadUserDeclarationsFatal")); + // reset the connection and configure the "user" admin to auto declare queues... + rabbitAvailableRule.getResource().resetConnection(); + bf.initializeBean(admin, "rabbitAdmin"); + bf.registerSingleton("rabbitAdmin", admin); + admin.afterPropertiesSet(); + // the mis-configured queue should be fatal + Binding binding = null; + try { + binding = binder.bindConsumer("input", "baddecls", + this.createBindableChannel("input", new BindingProperties()), + createConsumerProperties()); + fail("Expected exception"); + } + catch (BinderException e) { + assertThat(e.getCause()).isInstanceOf(AmqpIOException.class); + } + finally { + admin.deleteQueue("testBadUserDeclarationsFatal"); + if (binding != null) { + binding.unbind(); + } + } + + verifyAutoDeclareContextClear(binder); + } + + + @Test + public void testRoutingKeyExpression(TestInfo testInfo) throws Exception { + RabbitTestBinder binder = getBinder(); + ExtendedProducerProperties producerProperties = createProducerProperties(testInfo); + producerProperties.getExtension().setRoutingKeyExpression( + spelExpressionParser.parseExpression("payload.field")); + + DirectChannel output = createBindableChannel("output", + createProducerBindingProperties(producerProperties)); + output.setBeanName("rkeProducer"); + Binding producerBinding = binder.bindProducer("rke", output, + producerProperties); + + RabbitAdmin admin = new RabbitAdmin(this.rabbitAvailableRule.getResource()); + Queue queue = new AnonymousQueue(); + TopicExchange exchange = new TopicExchange("rke"); + org.springframework.amqp.core.Binding binding = BindingBuilder.bind(queue) + .to(exchange).with("rkeTest"); + admin.declareQueue(queue); + admin.declareBinding(binding); + + output.addInterceptor(new ChannelInterceptor() { + + @Override + public Message preSend(Message message, MessageChannel channel) { + assertThat(message.getHeaders() + .get(RabbitExpressionEvaluatingInterceptor.ROUTING_KEY_HEADER)) + .isEqualTo("rkeTest"); + return message; + } + + }); + + output.send(new GenericMessage<>(new Pojo("rkeTest"))); + + Object out = spyOn(queue.getName()).receive(false); + assertThat(out).isInstanceOf(byte[].class); + assertThat(new String((byte[]) out, StandardCharsets.UTF_8)) + .isEqualTo("{\"field\":\"rkeTest\"}"); + + producerBinding.unbind(); + + verifyAutoDeclareContextClear(binder); + } + + + @Test + public void testRoutingKey(TestInfo testInfo) throws Exception { + String routingKey = "static.key"; + RabbitTestBinder binder = getBinder(); + ExtendedProducerProperties producerProperties = createProducerProperties(testInfo); + producerProperties.getExtension().setRoutingKey(routingKey); + + DirectChannel output = createBindableChannel("output", + createProducerBindingProperties(producerProperties)); + output.setBeanName("rkeProducer"); + Binding producerBinding = binder.bindProducer("rke", output, + producerProperties); + + RabbitAdmin admin = new RabbitAdmin(this.rabbitAvailableRule.getResource()); + Queue queue = new AnonymousQueue(); + DirectExchange exchange = new DirectExchange("rke"); + org.springframework.amqp.core.Binding binding = BindingBuilder.bind(queue) + .to(exchange).with(routingKey); + admin.declareQueue(queue); + admin.declareBinding(binding); + + output.send(new GenericMessage<>(new Pojo("rkeTest"))); + + Object out = spyOn(queue.getName()).receive(false); + assertThat(out).isInstanceOf(byte[].class); + assertThat(new String((byte[]) out, StandardCharsets.UTF_8)) + .isEqualTo("{\"field\":\"rkeTest\"}"); + + + producerBinding.unbind(); + + verifyAutoDeclareContextClear(binder); + } + + + @Test + public void testRoutingKeyExpressionPartitionedAndDelay(TestInfo testInfo) throws Exception { + RabbitTestBinder binder = getBinder(); + ExtendedProducerProperties producerProperties = createProducerProperties(testInfo); + producerProperties.getExtension().setRoutingKeyExpression( + spelExpressionParser.parseExpression("#root.getPayload().field")); + // requires delayed message exchange plugin; tested locally + // producerProperties.getExtension().setDelayedExchange(true); + producerProperties.getExtension() + .setDelayExpression(spelExpressionParser.parseExpression("1000")); + producerProperties.setPartitionKeyExpression(new ValueExpression<>(0)); + + DirectChannel output = createBindableChannel("output", + createProducerBindingProperties(producerProperties)); + output.setBeanName("rkeProducer"); + Binding producerBinding = binder.bindProducer("rkep", output, + producerProperties); + + RabbitAdmin admin = new RabbitAdmin(this.rabbitAvailableRule.getResource()); + Queue queue = new AnonymousQueue(); + TopicExchange exchange = new TopicExchange("rkep"); + org.springframework.amqp.core.Binding binding = BindingBuilder.bind(queue) + .to(exchange).with("rkepTest-0"); + admin.declareQueue(queue); + admin.declareBinding(binding); + + output.addInterceptor(new ChannelInterceptor() { + + @Override + public Message preSend(Message message, MessageChannel channel) { + assertThat(message.getHeaders() + .get(RabbitExpressionEvaluatingInterceptor.ROUTING_KEY_HEADER)) + .isEqualTo("rkepTest"); + assertThat(message.getHeaders() + .get(RabbitExpressionEvaluatingInterceptor.DELAY_HEADER)) + .isEqualTo(1000); + return message; + } + + }); + + output.send(new GenericMessage<>(new Pojo("rkepTest"))); + + Object out = spyOn(queue.getName()).receive(false); + assertThat(out).isInstanceOf(byte[].class); + assertThat(new String((byte[]) out, StandardCharsets.UTF_8)) + .isEqualTo("{\"field\":\"rkepTest\"}"); + + producerBinding.unbind(); + + verifyAutoDeclareContextClear(binder); + } + + + @Test + public void testPolledConsumer() throws Exception { + RabbitTestBinder binder = getBinder(); + PollableSource inboundBindTarget = new DefaultPollableMessageSource( + this.messageConverter); + Binding> binding = binder.bindPollableConsumer( + "pollable", "group", inboundBindTarget, createConsumerProperties()); + RabbitTemplate template = new RabbitTemplate( + this.rabbitAvailableRule.getResource()); + template.convertAndSend("pollable.group", "testPollable"); + boolean polled = inboundBindTarget.poll(m -> { + assertThat(m.getPayload()).isEqualTo("testPollable"); + }); + int n = 0; + while (n++ < 100 && !polled) { + polled = inboundBindTarget.poll(m -> { + assertThat(m.getPayload()).isEqualTo("testPollable"); + }); + } + assertThat(polled).isTrue(); + binding.unbind(); + + verifyAutoDeclareContextClear(binder); + } + + + @Test + public void testPolledConsumerRequeue() throws Exception { + RabbitTestBinder binder = getBinder(); + PollableSource inboundBindTarget = new DefaultPollableMessageSource( + this.messageConverter); + ExtendedConsumerProperties properties = createConsumerProperties(); + Binding> binding = binder.bindPollableConsumer( + "pollableRequeue", "group", inboundBindTarget, properties); + RabbitTemplate template = new RabbitTemplate( + this.rabbitAvailableRule.getResource()); + template.convertAndSend("pollableRequeue.group", "testPollable"); + try { + boolean polled = false; + int n = 0; + while (n++ < 100 && !polled) { + polled = inboundBindTarget.poll(m -> { + assertThat(m.getPayload()).isEqualTo("testPollable"); + throw new RequeueCurrentMessageException(); + }); + } + } + catch (MessageHandlingException e) { + assertThat(e.getCause()).isInstanceOf(RequeueCurrentMessageException.class); + } + boolean polled = inboundBindTarget.poll(m -> { + assertThat(m.getPayload()).isEqualTo("testPollable"); + }); + assertThat(polled).isTrue(); + binding.unbind(); + + verifyAutoDeclareContextClear(binder); + } + + + @Test + public void testPolledConsumerWithDlq() throws Exception { + RabbitTestBinder binder = getBinder(); + PollableSource inboundBindTarget = new DefaultPollableMessageSource( + this.messageConverter); + ExtendedConsumerProperties properties = createConsumerProperties(); + properties.setMaxAttempts(2); + properties.setBackOffInitialInterval(0); + properties.getExtension().setAutoBindDlq(true); + Binding> binding = binder.bindPollableConsumer( + "pollableDlq", "group", inboundBindTarget, properties); + RabbitTemplate template = new RabbitTemplate( + this.rabbitAvailableRule.getResource()); + template.convertAndSend("pollableDlq.group", "testPollable"); + try { + int n = 0; + while (n++ < 100) { + inboundBindTarget.poll(m -> { + throw new RuntimeException("test DLQ"); + }); + Thread.sleep(100); + } + } + catch (MessageHandlingException e) { + assertThat( + e.getCause().getCause().getCause().getCause().getCause().getMessage()) + .isEqualTo("test DLQ"); + } + org.springframework.amqp.core.Message deadLetter = template + .receive("pollableDlq.group.dlq", 10_000); + assertThat(deadLetter).isNotNull(); + binding.unbind(); + + verifyAutoDeclareContextClear(binder); + } + + + @Test + public void testPolledConsumerWithDlqNoRetry() throws Exception { + RabbitTestBinder binder = getBinder(); + PollableSource inboundBindTarget = new DefaultPollableMessageSource( + this.messageConverter); + ExtendedConsumerProperties properties = createConsumerProperties(); + properties.setMaxAttempts(1); + // properties.getExtension().setRequeueRejected(true); // loops, correctly + properties.getExtension().setAutoBindDlq(true); + Binding> binding = binder.bindPollableConsumer( + "pollableDlqNoRetry", "group", inboundBindTarget, properties); + RabbitTemplate template = new RabbitTemplate( + this.rabbitAvailableRule.getResource()); + template.convertAndSend("pollableDlqNoRetry.group", "testPollable"); + try { + int n = 0; + while (n++ < 100) { + inboundBindTarget.poll(m -> { + throw new RuntimeException("test DLQ"); + }); + Thread.sleep(100); + } + } + catch (MessageHandlingException e) { + assertThat(e.getCause().getMessage()).isEqualTo("test DLQ"); + } + org.springframework.amqp.core.Message deadLetter = template + .receive("pollableDlqNoRetry.group.dlq", 10_000); + assertThat(deadLetter).isNotNull(); + binding.unbind(); + + verifyAutoDeclareContextClear(binder); + } + + + @Test + public void testPolledConsumerWithDlqRePub() throws Exception { + RabbitTestBinder binder = getBinder(); + PollableSource inboundBindTarget = new DefaultPollableMessageSource( + this.messageConverter); + ExtendedConsumerProperties properties = createConsumerProperties(); + properties.setMaxAttempts(2); + properties.setBackOffInitialInterval(0); + properties.getExtension().setAutoBindDlq(true); + properties.getExtension().setRepublishToDlq(true); + Binding> binding = binder.bindPollableConsumer( + "pollableDlqRePub", "group", inboundBindTarget, properties); + RabbitTemplate template = new RabbitTemplate( + this.rabbitAvailableRule.getResource()); + template.convertAndSend("pollableDlqRePub.group", "testPollable"); + boolean polled = false; + int n = 0; + while (n++ < 100 && !polled) { + Thread.sleep(100); + polled = inboundBindTarget.poll(m -> { + throw new RuntimeException("test DLQ"); + }); + } + assertThat(polled).isTrue(); + org.springframework.amqp.core.Message deadLetter = template + .receive("pollableDlqRePub.group.dlq", 10_000); + assertThat(deadLetter).isNotNull(); + binding.unbind(); + + verifyAutoDeclareContextClear(binder); + } + + + @Test + public void testCustomBatchingStrategy(TestInfo testInfo) throws Exception { + RabbitTestBinder binder = getBinder(); + ExtendedProducerProperties producerProperties = createProducerProperties(testInfo); + producerProperties.getExtension().setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT); + producerProperties.getExtension().setBatchingEnabled(true); + producerProperties.getExtension().setBatchingStrategyBeanName("testCustomBatchingStrategy"); + producerProperties.setRequiredGroups("default"); + + ConfigurableListableBeanFactory beanFactory = binder.getApplicationContext().getBeanFactory(); + beanFactory.registerSingleton("testCustomBatchingStrategy", new TestBatchingStrategy()); + + DirectChannel output = createBindableChannel("output", createProducerBindingProperties(producerProperties)); + output.setBeanName("batchingProducer"); + Binding producerBinding = binder.bindProducer("batching.0", output, producerProperties); + + Log logger = spy(TestUtils.getPropertyValue(binder, "binder.compressingPostProcessor.logger", Log.class)); + new DirectFieldAccessor(TestUtils.getPropertyValue(binder, "binder.compressingPostProcessor")) + .setPropertyValue("logger", logger); + when(logger.isTraceEnabled()).thenReturn(true); + + assertThat(TestUtils.getPropertyValue(binder, "binder.compressingPostProcessor.level")) + .isEqualTo(Deflater.BEST_SPEED); + + output.send(new GenericMessage<>("0".getBytes())); + output.send(new GenericMessage<>("1".getBytes())); + output.send(new GenericMessage<>("2".getBytes())); + output.send(new GenericMessage<>("3".getBytes())); + output.send(new GenericMessage<>("4".getBytes())); + + Object out = spyOn("batching.0.default").receive(false); + assertThat(out).isInstanceOf(byte[].class); + assertThat(new String((byte[]) out)).isEqualTo("0\u0000\n1\u0000\n2\u0000\n3\u0000\n4\u0000\n"); + + producerBinding.unbind(); + + verifyAutoDeclareContextClear(binder); + } + + + private SimpleMessageListenerContainer verifyContainer(Lifecycle endpoint) { + SimpleMessageListenerContainer container; + RetryTemplate retry; + container = TestUtils.getPropertyValue(endpoint, "messageListenerContainer", + SimpleMessageListenerContainer.class); + assertThat(container.getAcknowledgeMode()).isEqualTo(AcknowledgeMode.NONE); + assertThat(container.getQueueNames()[0]).startsWith("foo.props.0"); + assertThat(TestUtils.getPropertyValue(container, "transactional", Boolean.class)) + .isFalse(); + assertThat(TestUtils.getPropertyValue(container, "concurrentConsumers")) + .isEqualTo(2); + assertThat(TestUtils.getPropertyValue(container, "maxConcurrentConsumers")) + .isEqualTo(3); + assertThat(TestUtils.getPropertyValue(container, "defaultRequeueRejected", + Boolean.class)).isFalse(); + assertThat(TestUtils.getPropertyValue(container, "prefetchCount")).isEqualTo(20); + assertThat(TestUtils.getPropertyValue(container, "batchSize")).isEqualTo(10); + retry = TestUtils.getPropertyValue(endpoint, "retryTemplate", + RetryTemplate.class); + assertThat(TestUtils.getPropertyValue(retry, "retryPolicy.maxAttempts")) + .isEqualTo(23); + assertThat(TestUtils.getPropertyValue(retry, "backOffPolicy.initialInterval")) + .isEqualTo(2000L); + assertThat(TestUtils.getPropertyValue(retry, "backOffPolicy.maxInterval")) + .isEqualTo(20000L); + assertThat(TestUtils.getPropertyValue(retry, "backOffPolicy.multiplier")) + .isEqualTo(5.0); + + List requestMatchers = TestUtils.getPropertyValue(endpoint, + "headerMapper.requestHeaderMatcher.matchers", List.class); + assertThat(requestMatchers).hasSize(1); + assertThat(TestUtils.getPropertyValue(requestMatchers.get(0), "pattern")) + .isEqualTo("foo"); + + return container; + } + + private void verifyFooRequestProducer(Lifecycle endpoint) { + List requestMatchers = TestUtils.getPropertyValue(endpoint, + "headerMapper.requestHeaderMatcher.matchers", List.class); + assertThat(requestMatchers).hasSize(5); + assertThat(TestUtils.getPropertyValue(requestMatchers.get(4), "pattern")) + .isEqualTo("foo"); + } + + @Override + protected String getEndpointRouting(Object endpoint) { + return TestUtils + .getPropertyValue(endpoint, "routingKeyExpression", SpelExpression.class) + .getExpressionString(); + } + + @Override + protected String getExpectedRoutingBaseDestination(String name, String group) { + return name; + } + + @Override + protected String getClassUnderTestName() { + return CLASS_UNDER_TEST_NAME; + } + + @Override + protected void checkRkExpressionForPartitionedModuleSpEL(Object endpoint) { + assertThat(getEndpointRouting(endpoint)) + .contains(getExpectedRoutingBaseDestination("'part.0'", "test") + + " + '-' + headers['" + BinderHeaders.PARTITION_HEADER + "']"); + } + + @Override + public Spy spyOn(final String queue) { + final RabbitTemplate template = new RabbitTemplate( + this.rabbitAvailableRule.getResource()); + template.setAfterReceivePostProcessors( + new DelegatingDecompressingPostProcessor()); + return new Spy() { + + @Override + public Object receive(boolean expectNull) throws Exception { + if (expectNull) { + Thread.sleep(50); + return template.receiveAndConvert( + new RabbitConsumerProperties().getPrefix() + queue); + } + Object bar = null; + int n = 0; + while (n++ < 100 && bar == null) { + bar = template.receiveAndConvert( + new RabbitConsumerProperties().getPrefix() + queue); + Thread.sleep(100); + } + assertThat(n).isLessThan(100) + .withFailMessage("Message did not arrive in RabbitMQ"); + return bar; + } + + }; + } + + private RuntimeException bigCause(RuntimeException cause) { + if (getStackTraceAsString(cause).length() > this.maxStackTraceSize) { + return cause; + } + return bigCause(new RuntimeException(BIG_EXCEPTION_MESSAGE, cause)); + } + + private String getStackTraceAsString(Throwable cause) { + StringWriter stringWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(stringWriter, true); + cause.printStackTrace(printWriter); + return stringWriter.getBuffer().toString(); + } + + private void verifyAutoDeclareContextClear(RabbitTestBinder binder) { + ApplicationContext ctx = + TestUtils.getPropertyValue(binder, "binder.provisioningProvider.autoDeclareContext", + ApplicationContext.class); + assertThat(ctx.getBeansOfType(Declarable.class)).isEmpty(); + } + + public static class TestPartitionKeyExtractorClass + implements PartitionKeyExtractorStrategy { + + @Override + public Object extractKey(Message message) { + return 0; + } + + } + + public static class TestPartitionSelectorClass implements PartitionSelectorStrategy { + + @Override + public int selectPartition(Object key, int partitionCount) { + return 0; + } + + } + + public static class Pojo { + + private String field; + + public Pojo() { + super(); + } + + public Pojo(String field) { + this.field = field; + } + + public String getField() { + return this.field; + } + + public void setField(String field) { + this.field = field; + } + + } + // @checkstyle:on + + public static class TestBatchingStrategy implements BatchingStrategy { + + private final List messages = new ArrayList<>(); + private String exchange; + private String routingKey; + private int currentSize; + + @Override + public MessageBatch addToBatch(String exchange, String routingKey, org.springframework.amqp.core.Message message) { + this.exchange = exchange; + this.routingKey = routingKey; + this.messages.add(message); + currentSize += message.getBody().length + 2; + + MessageBatch batch = null; + if (this.messages.size() == 5) { + batch = this.doReleaseBatch(); + } + + return batch; + } + + @Override + public Date nextRelease() { + return null; + } + + @Override + public Collection releaseBatches() { + MessageBatch batch = this.doReleaseBatch(); + return batch == null ? Collections.emptyList() : Collections.singletonList(batch); + } + + private MessageBatch doReleaseBatch() { + if (this.messages.size() < 1) { + return null; + } + else { + ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[this.currentSize]); + for (org.springframework.amqp.core.Message message: messages) { + byteBuffer.put(message.getBody()).putChar('\n'); + } + MessageBatch messageBatch = new MessageBatch(this.exchange, this.routingKey, + new org.springframework.amqp.core.Message(byteBuffer.array(), new MessageProperties())); + this.messages.clear(); + return messageBatch; + } + } + } + +} diff --git a/r-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitTestBinder.java b/r-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitTestBinder.java new file mode 100644 index 000000000..f68c8fd50 --- /dev/null +++ b/r-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitTestBinder.java @@ -0,0 +1,188 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.stream.binder.rabbit; + +import java.util.HashSet; +import java.util.Set; + +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitAdmin; +import org.springframework.boot.autoconfigure.amqp.RabbitProperties; +import org.springframework.cloud.stream.binder.AbstractPollableConsumerTestBinder; +import org.springframework.cloud.stream.binder.Binding; +import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; +import org.springframework.cloud.stream.binder.ExtendedProducerProperties; +import org.springframework.cloud.stream.binder.PollableSource; +import org.springframework.cloud.stream.binder.rabbit.properties.RabbitCommonProperties; +import org.springframework.cloud.stream.binder.rabbit.properties.RabbitConsumerProperties; +import org.springframework.cloud.stream.binder.rabbit.properties.RabbitProducerProperties; +import org.springframework.cloud.stream.binder.rabbit.provisioning.RabbitExchangeQueueProvisioner; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.integration.config.EnableIntegration; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.MessageHandler; +import org.springframework.util.StringUtils; + +/** + * Test support class for {@link RabbitMessageChannelBinder}. + * + * @author Ilayaperumal Gopinathan + * @author Gary Russell + * @author David Turanski + * @author Mark Fisher + */ +// @checkstyle:off +public class RabbitTestBinder extends + AbstractPollableConsumerTestBinder, ExtendedProducerProperties> { + + // @checkstyle:on + private final RabbitAdmin rabbitAdmin; + + private final Set prefixes = new HashSet<>(); + + private final Set queues = new HashSet(); + + private final Set exchanges = new HashSet(); + + private final AnnotationConfigApplicationContext applicationContext; + + public RabbitTestBinder(ConnectionFactory connectionFactory, + RabbitProperties rabbitProperties) { + this(connectionFactory, new RabbitMessageChannelBinder(connectionFactory, + rabbitProperties, new RabbitExchangeQueueProvisioner(connectionFactory))); + } + + public RabbitTestBinder(ConnectionFactory connectionFactory, + RabbitMessageChannelBinder binder) { + this.applicationContext = new AnnotationConfigApplicationContext(Config.class); + binder.setApplicationContext(this.applicationContext); + this.setPollableConsumerBinder(binder); + this.rabbitAdmin = new RabbitAdmin(connectionFactory); + } + + public AnnotationConfigApplicationContext getApplicationContext() { + return this.applicationContext; + } + + @Override + public Binding bindConsumer(String name, String group, + MessageChannel moduleInputChannel, + ExtendedConsumerProperties properties) { + captureConsumerResources(name, group, properties); + return super.bindConsumer(name, group, moduleInputChannel, properties); + } + + @Override + public Binding> bindPollableConsumer(String name, + String group, PollableSource inboundBindTarget, + ExtendedConsumerProperties properties) { + captureConsumerResources(name, group, properties); + return super.bindPollableConsumer(name, group, inboundBindTarget, properties); + } + + private void captureConsumerResources(String name, String group, + ExtendedConsumerProperties properties) { + String[] names = null; + if (group != null) { + if (properties.getExtension().isQueueNameGroupOnly()) { + this.queues.add(properties.getExtension().getPrefix() + group); + } + else { + if (properties.isMultiplex()) { + names = StringUtils.commaDelimitedListToStringArray(name); + for (String nayme : names) { + this.queues.add(properties.getExtension().getPrefix() + + nayme.trim() + "." + group); + } + } + else { + this.queues.add( + properties.getExtension().getPrefix() + name + "." + group); + } + } + } + if (names != null) { + for (String nayme : names) { + this.exchanges.add(properties.getExtension().getPrefix() + nayme.trim()); + } + } + else { + this.exchanges.add(properties.getExtension().getPrefix() + name); + } + this.prefixes.add(properties.getExtension().getPrefix()); + deadLetters(properties.getExtension()); + } + + @Override + public Binding bindProducer(String name, + MessageChannel moduleOutputChannel, + ExtendedProducerProperties properties) { + this.queues.add(properties.getExtension().getPrefix() + name + ".default"); + this.exchanges.add(properties.getExtension().getPrefix() + name); + if (properties.getRequiredGroups() != null) { + for (String group : properties.getRequiredGroups()) { + if (properties.getExtension().isQueueNameGroupOnly()) { + this.queues.add(properties.getExtension().getPrefix() + group); + } + else { + this.queues.add( + properties.getExtension().getPrefix() + name + "." + group); + } + } + } + this.prefixes.add(properties.getExtension().getPrefix()); + deadLetters(properties.getExtension()); + return super.bindProducer(name, moduleOutputChannel, properties); + } + + private void deadLetters(RabbitCommonProperties properties) { + if (properties.getDeadLetterExchange() != null) { + this.exchanges.add(properties.getDeadLetterExchange()); + } + if (properties.getDeadLetterQueueName() != null) { + this.queues.add(properties.getDeadLetterQueueName()); + } + } + + @Override + public void cleanup() { + for (String queue : this.queues) { + this.rabbitAdmin.deleteQueue(queue); + this.rabbitAdmin.deleteQueue(queue + ".dlq"); + // delete any partitioned queues + for (int i = 0; i < 10; i++) { + this.rabbitAdmin.deleteQueue(queue + "-" + i); + this.rabbitAdmin.deleteQueue(queue + "-" + i + ".dlq"); + } + } + for (String exchange : this.exchanges) { + this.rabbitAdmin.deleteExchange(exchange); + } + for (String prefix : this.prefixes) { + this.rabbitAdmin.deleteExchange(prefix + "DLX"); + } + this.applicationContext.close(); + } + + @Configuration + @EnableIntegration + static class Config { + + } + +} diff --git a/r-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RepublishUnitTests.java b/r-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RepublishUnitTests.java new file mode 100644 index 000000000..873348d63 --- /dev/null +++ b/r-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RepublishUnitTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2020-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.stream.binder.rabbit; + +import java.io.IOException; +import java.util.Collections; + +import com.rabbitmq.client.Channel; +import org.junit.Test; + +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageProperties; +import org.springframework.amqp.rabbit.connection.Connection; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.boot.autoconfigure.amqp.RabbitProperties; +import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; +import org.springframework.cloud.stream.binder.rabbit.properties.RabbitConsumerProperties; +import org.springframework.cloud.stream.provisioning.ConsumerDestination; +import org.springframework.integration.IntegrationMessageHeaderAccessor; +import org.springframework.messaging.MessageHandler; +import org.springframework.messaging.support.ErrorMessage; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * @author Gary Russell + * @since 3.0.6 + * + */ +public class RepublishUnitTests { + + @Test + public void testBadRepublishSetting() throws IOException { + ConnectionFactory cf = mock(ConnectionFactory.class); + Connection conn = mock(Connection.class); + given(cf.createConnection()).willReturn(conn); + Channel channel = mock(Channel.class); + given(channel.isOpen()).willReturn(true); + given(channel.exchangeDeclarePassive("DLX")).willThrow(new IOException()); + given(conn.createChannel(false)).willReturn(channel); + RabbitProperties props = new RabbitProperties(); + RabbitMessageChannelBinder binder = new RabbitMessageChannelBinder(cf, props, null); + RabbitConsumerProperties extension = new RabbitConsumerProperties(); + ExtendedConsumerProperties bindingProps = + new ExtendedConsumerProperties(extension); + MessageHandler handler = binder.getErrorMessageHandler(mock(ConsumerDestination.class), "foo", bindingProps); + ErrorMessage message = new ErrorMessage(new RuntimeException("test"), + Collections.singletonMap(IntegrationMessageHeaderAccessor.SOURCE_DATA, + new Message("foo".getBytes(), new MessageProperties()))); + handler.handleMessage(message); + handler.handleMessage(message); + verify(channel, times(1)).exchangeDeclarePassive("DLX"); + verify(channel, never()).basicPublish(any(), any(), eq(false), any(), any()); + } + +} diff --git a/r-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/integration/RabbitBinderModuleTests.java b/r-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/integration/RabbitBinderModuleTests.java new file mode 100644 index 000000000..e5814e042 --- /dev/null +++ b/r-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/integration/RabbitBinderModuleTests.java @@ -0,0 +1,444 @@ +/* + * Copyright 2015-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.stream.binder.rabbit.integration; + +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import com.rabbitmq.http.client.Client; +import com.rabbitmq.http.client.domain.BindingInfo; +import com.rabbitmq.http.client.domain.ExchangeInfo; +import com.rabbitmq.http.client.domain.QueueInfo; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.Mockito; + +import org.springframework.amqp.core.DeclarableCustomizer; +import org.springframework.amqp.core.ExchangeTypes; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.connection.ConnectionNameStrategy; +import org.springframework.amqp.rabbit.core.RabbitAdmin; +import org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer; +import org.springframework.amqp.rabbit.listener.MessageListenerContainer; +import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; +import org.springframework.amqp.utils.test.TestUtils; +import org.springframework.beans.DirectFieldAccessor; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.actuate.amqp.RabbitHealthIndicator; +import org.springframework.boot.actuate.health.CompositeHealthContributor; +import org.springframework.boot.actuate.health.Status; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.Cloud; +import org.springframework.cloud.stream.annotation.EnableBinding; +import org.springframework.cloud.stream.annotation.Input; +import org.springframework.cloud.stream.binder.Binder; +import org.springframework.cloud.stream.binder.BinderFactory; +import org.springframework.cloud.stream.binder.Binding; +import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; +import org.springframework.cloud.stream.binder.ExtendedProducerProperties; +import org.springframework.cloud.stream.binder.ExtendedPropertiesBinder; +import org.springframework.cloud.stream.binder.PollableMessageSource; +import org.springframework.cloud.stream.binder.rabbit.RabbitMessageChannelBinder; +import org.springframework.cloud.stream.binder.rabbit.properties.RabbitConsumerProperties; +import org.springframework.cloud.stream.binder.rabbit.properties.RabbitProducerProperties; +import org.springframework.cloud.stream.binder.test.junit.rabbit.RabbitTestSupport; +import org.springframework.cloud.stream.binding.BindingService; +import org.springframework.cloud.stream.config.ConsumerEndpointCustomizer; +import org.springframework.cloud.stream.config.ListenerContainerCustomizer; +import org.springframework.cloud.stream.config.MessageSourceCustomizer; +import org.springframework.cloud.stream.config.ProducerMessageHandlerCustomizer; +import org.springframework.cloud.stream.messaging.Processor; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.integration.amqp.inbound.AmqpInboundChannelAdapter; +import org.springframework.integration.amqp.inbound.AmqpMessageSource; +import org.springframework.integration.amqp.outbound.AmqpOutboundEndpoint; +import org.springframework.integration.channel.DirectChannel; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.support.GenericMessage; +import org.springframework.retry.backoff.ExponentialBackOffPolicy; +import org.springframework.retry.policy.SimpleRetryPolicy; +import org.springframework.retry.support.RetryTemplate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * @author Marius Bogoevici + * @author Gary Russell + * @author Artem Bilan + * @author Soby Chacko + */ +public class RabbitBinderModuleTests { + + @RegisterExtension + public static RabbitTestSupport rabbitTestSupport = new RabbitTestSupport(); + + private ConfigurableApplicationContext context; + + public static final ConnectionFactory MOCK_CONNECTION_FACTORY = mock( + ConnectionFactory.class, Mockito.RETURNS_MOCKS); + + @AfterEach + public void tearDown() { + if (context != null) { + context.close(); + context = null; + } + RabbitAdmin admin = new RabbitAdmin(rabbitTestSupport.getResource()); + admin.deleteExchange("input"); + admin.deleteExchange("output"); + } + + @Test + public void testParentConnectionFactoryInheritedByDefault() throws Exception { + context = new SpringApplicationBuilder(SimpleProcessor.class) + .web(WebApplicationType.NONE).run("--server.port=0", + "--spring.cloud.stream.rabbit.binder.connection-name-prefix=foo", + "--spring.cloud.stream.rabbit.bindings.input.consumer.single-active-consumer=true"); + BinderFactory binderFactory = context.getBean(BinderFactory.class); + Binder binder = binderFactory.getBinder(null, MessageChannel.class); + assertThat(binder).isInstanceOf(RabbitMessageChannelBinder.class); + DirectFieldAccessor binderFieldAccessor = new DirectFieldAccessor(binder); + CachingConnectionFactory binderConnectionFactory = (CachingConnectionFactory) binderFieldAccessor + .getPropertyValue("connectionFactory"); + assertThat(binderConnectionFactory).isInstanceOf(CachingConnectionFactory.class); + ConnectionFactory connectionFactory = context.getBean(ConnectionFactory.class); + assertThat(binderConnectionFactory).isSameAs(connectionFactory); + + CompositeHealthContributor bindersHealthIndicator = context + .getBean("bindersHealthContributor", CompositeHealthContributor.class); + + assertThat(bindersHealthIndicator).isNotNull(); + + RabbitHealthIndicator indicator = (RabbitHealthIndicator) bindersHealthIndicator.getContributor("rabbit"); + assertThat(indicator).isNotNull(); + assertThat(indicator.health().getStatus()) + .isEqualTo(Status.UP); + + ConnectionFactory publisherConnectionFactory = binderConnectionFactory + .getPublisherConnectionFactory(); + assertThat(TestUtils.getPropertyValue(publisherConnectionFactory, + "connection.target")).isNull(); + DirectChannel checkPf = new DirectChannel(); + Binding binding = ((RabbitMessageChannelBinder) binder) + .bindProducer("checkPF", checkPf, + new ExtendedProducerProperties<>(new RabbitProducerProperties())); + checkPf.send(new GenericMessage<>("foo".getBytes())); + binding.unbind(); + assertThat(TestUtils.getPropertyValue(publisherConnectionFactory, + "connection.target")).isNotNull(); + + CachingConnectionFactory cf = this.context + .getBean(CachingConnectionFactory.class); + ConnectionNameStrategy cns = TestUtils.getPropertyValue(cf, + "connectionNameStrategy", ConnectionNameStrategy.class); + assertThat(cns.obtainNewConnectionName(cf)).isEqualTo("foo#2"); + new RabbitAdmin(rabbitTestSupport.getResource()).deleteExchange("checkPF"); + checkCustomizedArgs(); + binderConnectionFactory.resetConnection(); + binderConnectionFactory.createConnection(); + checkCustomizedArgs(); + } + + private void checkCustomizedArgs() throws MalformedURLException, URISyntaxException, InterruptedException { + Client client = new Client("http://guest:guest@localhost:15672/api"); + List bindings = client.getBindingsBySource("/", "input"); + int n = 0; + while (n++ < 100 && bindings == null || bindings.size() < 1) { + Thread.sleep(100); + bindings = client.getBindingsBySource("/", "input"); + } + assertThat(bindings).isNotNull(); + assertThat(bindings.get(0).getArguments()).contains(entry("added.by", "customizer")); + ExchangeInfo exchange = client.getExchange("/", "input"); + assertThat(exchange.getArguments()).contains(entry("added.by", "customizer")); + QueueInfo queue = client.getQueue("/", bindings.get(0).getDestination()); + assertThat(queue.getArguments()).contains(entry("added.by", "customizer")); + assertThat(queue.getArguments()).contains(entry("x-single-active-consumer", Boolean.TRUE)); + } + + @Test + @SuppressWarnings("unchecked") + public void testParentConnectionFactoryInheritedByDefaultAndRabbitSettingsPropagated() { + context = new SpringApplicationBuilder(SimpleProcessor.class) + .web(WebApplicationType.NONE).run("--server.port=0", + "--spring.cloud.stream.bindings.source.group=someGroup", + "--spring.cloud.stream.bindings.input.group=someGroup", + "--spring.cloud.stream.rabbit.bindings.input.consumer.transacted=true", + "--spring.cloud.stream.rabbit.bindings.output.producer.transacted=true"); + BinderFactory binderFactory = context.getBean(BinderFactory.class); + Binder binder = binderFactory.getBinder(null, MessageChannel.class); + assertThat(binder).isInstanceOf(RabbitMessageChannelBinder.class); + BindingService bindingService = context.getBean(BindingService.class); + DirectFieldAccessor channelBindingServiceAccessor = new DirectFieldAccessor( + bindingService); + // @checkstyle:off + Map>> consumerBindings = (Map>>) channelBindingServiceAccessor + .getPropertyValue("consumerBindings"); + // @checkstyle:on + Binding inputBinding = consumerBindings.get("input").get(0); + assertThat(TestUtils.getPropertyValue(inputBinding, "lifecycle.beanName")) + .isEqualTo("setByCustomizer:someGroup"); + SimpleMessageListenerContainer container = TestUtils.getPropertyValue( + inputBinding, "lifecycle.messageListenerContainer", + SimpleMessageListenerContainer.class); + assertThat(TestUtils.getPropertyValue(container, "beanName")) + .isEqualTo("setByCustomizerForQueue:input.someGroup,andGroup:someGroup"); + assertThat(TestUtils.getPropertyValue(container, "transactional", Boolean.class)) + .isTrue(); + Map> producerBindings = (Map>) TestUtils + .getPropertyValue(bindingService, "producerBindings"); + Binding outputBinding = producerBindings.get("output"); + assertThat(TestUtils.getPropertyValue(outputBinding, + "lifecycle.amqpTemplate.transactional", Boolean.class)).isTrue(); + assertThat(TestUtils.getPropertyValue(outputBinding, "lifecycle.beanName")) + .isEqualTo("setByCustomizer:output"); + DirectFieldAccessor binderFieldAccessor = new DirectFieldAccessor(binder); + ConnectionFactory binderConnectionFactory = (ConnectionFactory) binderFieldAccessor + .getPropertyValue("connectionFactory"); + assertThat(binderConnectionFactory).isInstanceOf(CachingConnectionFactory.class); + ConnectionFactory connectionFactory = context.getBean(ConnectionFactory.class); + assertThat(binderConnectionFactory).isSameAs(connectionFactory); + CompositeHealthContributor bindersHealthIndicator = context + .getBean("bindersHealthContributor", CompositeHealthContributor.class); + + assertThat(bindersHealthIndicator).isNotNull(); + + RabbitHealthIndicator indicator = (RabbitHealthIndicator) bindersHealthIndicator.getContributor("rabbit"); + assertThat(indicator).isNotNull(); + assertThat(indicator.health().getStatus()) + .isEqualTo(Status.UP); + + CachingConnectionFactory cf = this.context + .getBean(CachingConnectionFactory.class); + ConnectionNameStrategy cns = TestUtils.getPropertyValue(cf, + "connectionNameStrategy", ConnectionNameStrategy.class); + assertThat(cns.obtainNewConnectionName(cf)).startsWith("rabbitConnectionFactory"); + assertThat(TestUtils.getPropertyValue(consumerBindings.get("source").get(0), + "target.source.h.advised.targetSource.target.beanName")) + .isEqualTo("setByCustomizer:someGroup"); + } + + @Test + public void testParentConnectionFactoryInheritedIfOverridden() { + context = new SpringApplicationBuilder(SimpleProcessor.class, + ConnectionFactoryConfiguration.class).web(WebApplicationType.NONE) + .run("--server.port=0"); + BinderFactory binderFactory = context.getBean(BinderFactory.class); + Binder binder = binderFactory.getBinder(null, MessageChannel.class); + assertThat(binder).isInstanceOf(RabbitMessageChannelBinder.class); + DirectFieldAccessor binderFieldAccessor = new DirectFieldAccessor(binder); + ConnectionFactory binderConnectionFactory = (ConnectionFactory) binderFieldAccessor + .getPropertyValue("connectionFactory"); + assertThat(binderConnectionFactory).isSameAs(MOCK_CONNECTION_FACTORY); + ConnectionFactory connectionFactory = context.getBean(ConnectionFactory.class); + assertThat(binderConnectionFactory).isSameAs(connectionFactory); + CompositeHealthContributor bindersHealthIndicator = context + .getBean("bindersHealthContributor", CompositeHealthContributor.class); + assertThat(bindersHealthIndicator).isNotNull(); + RabbitHealthIndicator indicator = (RabbitHealthIndicator) bindersHealthIndicator.getContributor("rabbit"); + assertThat(indicator).isNotNull(); + // mock connection factory behaves as if down + assertThat(indicator.health().getStatus()) + .isEqualTo(Status.DOWN); + } + + @Test + public void testParentConnectionFactoryNotInheritedByCustomizedBindersAndProducerRetryBootProperties() { + List params = new ArrayList<>(); + params.add("--spring.cloud.stream.input.binder=custom"); + params.add("--spring.cloud.stream.output.binder=custom"); + params.add("--spring.cloud.stream.binders.custom.type=rabbit"); + params.add("--spring.cloud.stream.binders.custom.environment.foo=bar"); + params.add("--server.port=0"); + params.add("--spring.rabbitmq.template.retry.enabled=true"); + params.add("--spring.rabbitmq.template.retry.maxAttempts=2"); + params.add("--spring.rabbitmq.template.retry.initial-interval=1000"); + params.add("--spring.rabbitmq.template.retry.multiplier=1.1"); + params.add("--spring.rabbitmq.template.retry.max-interval=3000"); + context = new SpringApplicationBuilder(SimpleProcessor.class) + .web(WebApplicationType.NONE) + .run(params.toArray(new String[params.size()])); + BinderFactory binderFactory = context.getBean(BinderFactory.class); + // @checkstyle:off + @SuppressWarnings("unchecked") + Binder, ExtendedProducerProperties> binder = (Binder, ExtendedProducerProperties>) binderFactory + .getBinder(null, MessageChannel.class); + // @checkstyle:on + assertThat(binder).isInstanceOf(RabbitMessageChannelBinder.class); + DirectFieldAccessor binderFieldAccessor = new DirectFieldAccessor(binder); + ConnectionFactory binderConnectionFactory = (ConnectionFactory) binderFieldAccessor + .getPropertyValue("connectionFactory"); + ConnectionFactory connectionFactory = context.getBean(ConnectionFactory.class); + assertThat(binderConnectionFactory).isNotSameAs(connectionFactory); + CompositeHealthContributor bindersHealthIndicator = context + .getBean("bindersHealthContributor", CompositeHealthContributor.class); + assertThat(bindersHealthIndicator); + + RabbitHealthIndicator indicator = (RabbitHealthIndicator) bindersHealthIndicator.getContributor("custom"); + assertThat(indicator).isNotNull(); + assertThat(indicator.health().getStatus()).isEqualTo(Status.UP); + String name = UUID.randomUUID().toString(); + Binding binding = binder.bindProducer(name, new DirectChannel(), + new ExtendedProducerProperties<>(new RabbitProducerProperties())); + RetryTemplate template = TestUtils.getPropertyValue(binding, + "lifecycle.amqpTemplate.retryTemplate", RetryTemplate.class); + assertThat(template).isNotNull(); + SimpleRetryPolicy retryPolicy = TestUtils.getPropertyValue(template, + "retryPolicy", SimpleRetryPolicy.class); + ExponentialBackOffPolicy backOff = TestUtils.getPropertyValue(template, + "backOffPolicy", ExponentialBackOffPolicy.class); + assertThat(retryPolicy.getMaxAttempts()).isEqualTo(2); + assertThat(backOff.getInitialInterval()).isEqualTo(1000L); + assertThat(backOff.getMultiplier()).isEqualTo(1.1); + assertThat(backOff.getMaxInterval()).isEqualTo(3000L); + binding.unbind(); + new RabbitAdmin(rabbitTestSupport.getResource()).deleteExchange(name); + context.close(); + } + + @Test + public void testCloudProfile() { + this.context = new SpringApplicationBuilder(SimpleProcessor.class, + MockCloudConfiguration.class).web(WebApplicationType.NONE) + .profiles("cloud").run(); + BinderFactory binderFactory = this.context.getBean(BinderFactory.class); + Binder binder = binderFactory.getBinder(null, MessageChannel.class); + assertThat(binder).isInstanceOf(RabbitMessageChannelBinder.class); + DirectFieldAccessor binderFieldAccessor = new DirectFieldAccessor(binder); + ConnectionFactory binderConnectionFactory = (ConnectionFactory) binderFieldAccessor + .getPropertyValue("connectionFactory"); + ConnectionFactory connectionFactory = this.context + .getBean(ConnectionFactory.class); + + assertThat(binderConnectionFactory).isNotSameAs(connectionFactory); + + assertThat(TestUtils.getPropertyValue(connectionFactory, "addresses")) + .isNotNull(); + assertThat(TestUtils.getPropertyValue(binderConnectionFactory, "addresses")) + .isNull(); + + Cloud cloud = this.context.getBean(Cloud.class); + + verify(cloud).getSingletonServiceConnector(ConnectionFactory.class, null); + } + + @Test + public void testExtendedProperties() { + context = new SpringApplicationBuilder(SimpleProcessor.class) + .web(WebApplicationType.NONE).run("--server.port=0", + "--spring.cloud.stream.rabbit.default.producer.routing-key-expression=fooRoutingKey", + "--spring.cloud.stream.rabbit.default.consumer.exchange-type=direct", + "--spring.cloud.stream.rabbit.bindings.output.producer.batch-size=512", + "--spring.cloud.stream.rabbit.default.consumer.max-concurrency=4", + "--spring.cloud.stream.rabbit.bindings.input.consumer.exchange-type=fanout"); + BinderFactory binderFactory = context.getBean(BinderFactory.class); + Binder rabbitBinder = binderFactory.getBinder(null, + MessageChannel.class); + + RabbitProducerProperties rabbitProducerProperties = (RabbitProducerProperties) ((ExtendedPropertiesBinder) rabbitBinder) + .getExtendedProducerProperties("output"); + + assertThat( + rabbitProducerProperties.getRoutingKeyExpression().getExpressionString()) + .isEqualTo("fooRoutingKey"); + assertThat(rabbitProducerProperties.getBatchSize()).isEqualTo(512); + + RabbitConsumerProperties rabbitConsumerProperties = (RabbitConsumerProperties) ((ExtendedPropertiesBinder) rabbitBinder) + .getExtendedConsumerProperties("input"); + + assertThat(rabbitConsumerProperties.getExchangeType()) + .isEqualTo(ExchangeTypes.FANOUT); + assertThat(rabbitConsumerProperties.getMaxConcurrency()).isEqualTo(4); + } + + @EnableBinding({ Processor.class, PMS.class }) + @SpringBootApplication + public static class SimpleProcessor { + + @Bean + public ListenerContainerCustomizer containerCustomizer() { + return (c, q, g) -> ((AbstractMessageListenerContainer) c).setBeanName( + "setByCustomizerForQueue:" + q + (g == null ? "" : ",andGroup:" + g)); + } + + @Bean + public MessageSourceCustomizer sourceCustomizer() { + return (s, q, g) -> s.setBeanName("setByCustomizer:" + g); + } + + @Bean + public ProducerMessageHandlerCustomizer messageHandlerCustomizer() { + return (handler, destinationName) -> handler.setBeanName("setByCustomizer:" + destinationName); + } + + @Bean + public ConsumerEndpointCustomizer adapterCustomizer() { + return (producer, dest, grp) -> producer.setBeanName("setByCustomizer:" + grp); + } + + @Bean + public DeclarableCustomizer customizer() { + return dec -> { + dec.addArgument("added.by", "customizer"); + return dec; + }; + } + } + + public static class ConnectionFactoryConfiguration { + + @Bean + public ConnectionFactory connectionFactory() { + return MOCK_CONNECTION_FACTORY; + } + + } + + public static class MockCloudConfiguration { + + @Bean + public Cloud cloud() { + Cloud cloud = mock(Cloud.class); + + willReturn(new CachingConnectionFactory("localhost")).given(cloud) + .getSingletonServiceConnector(ConnectionFactory.class, null); + + return cloud; + } + + } + + public interface PMS { + + @Input + PollableMessageSource source(); + + } + +} diff --git a/r-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/stream/AbstractIntegrationTests.java b/r-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/stream/AbstractIntegrationTests.java new file mode 100644 index 000000000..ebcb6d4f6 --- /dev/null +++ b/r-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/stream/AbstractIntegrationTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2021-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.stream.binder.rabbit.stream; + +import java.time.Duration; + +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.utility.DockerImageName; + +/** + * @author Gary Russell + * @since 3.2 + * + */ +public abstract class AbstractIntegrationTests { + + static final GenericContainer RABBITMQ; + + static { + if (System.getProperty("spring.rabbit.use.local.server") == null) { + String image = "pivotalrabbitmq/rabbitmq-stream"; + String cache = System.getenv().get("IMAGE_CACHE"); + if (cache != null) { + image = cache + image; + } + RABBITMQ = new GenericContainer<>(DockerImageName.parse(image)) + .withExposedPorts(5672, 15672, 5552) + .withStartupTimeout(Duration.ofMinutes(2)); + RABBITMQ.start(); + } + else { + RABBITMQ = null; + } + } + + static int amqpPort() { + return RABBITMQ != null ? RABBITMQ.getMappedPort(5672) : 5672; + } + + static int managementPort() { + return RABBITMQ != null ? RABBITMQ.getMappedPort(15672) : 15672; + } + + static int streamPort() { + return RABBITMQ != null ? RABBITMQ.getMappedPort(5552) : 5552; + } + +} diff --git a/r-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/stream/RabbitStreamBinderModuleTests.java b/r-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/stream/RabbitStreamBinderModuleTests.java new file mode 100644 index 000000000..4571e5f29 --- /dev/null +++ b/r-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/stream/RabbitStreamBinderModuleTests.java @@ -0,0 +1,150 @@ +/* + * Copyright 2021-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.stream.binder.rabbit.stream; + +import com.rabbitmq.stream.ConsumerBuilder; +import com.rabbitmq.stream.Environment; +import com.rabbitmq.stream.OffsetSpecification; +import com.rabbitmq.stream.ProducerBuilder; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.rabbit.listener.MessageListenerContainer; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.stream.binder.BinderFactory; +import org.springframework.cloud.stream.binder.Binding; +import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; +import org.springframework.cloud.stream.binder.ExtendedProducerProperties; +import org.springframework.cloud.stream.binder.rabbit.RabbitMessageChannelBinder; +import org.springframework.cloud.stream.binder.rabbit.RabbitStreamMessageHandler; +import org.springframework.cloud.stream.binder.rabbit.properties.RabbitConsumerProperties; +import org.springframework.cloud.stream.binder.rabbit.properties.RabbitConsumerProperties.ContainerType; +import org.springframework.cloud.stream.binder.rabbit.properties.RabbitProducerProperties; +import org.springframework.cloud.stream.binder.rabbit.properties.RabbitProducerProperties.ProducerType; +import org.springframework.cloud.stream.config.ListenerContainerCustomizer; +import org.springframework.cloud.stream.config.ProducerMessageHandlerCustomizer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.integration.channel.DirectChannel; +import org.springframework.integration.channel.QueueChannel; +import org.springframework.integration.test.util.TestUtils; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.MessageHandler; +import org.springframework.rabbit.stream.listener.StreamListenerContainer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * @author Gary Russell + */ +public class RabbitStreamBinderModuleTests { + + private ConfigurableApplicationContext context; + + @AfterEach + void tearDown() { + if (context != null) { + context.close(); + context = null; + } + } + + @Test + void testStreamContainer() { + context = new SpringApplicationBuilder(SimpleProcessor.class) + .web(WebApplicationType.NONE) + .run("--server.port=0"); + BinderFactory binderFactory = context.getBean(BinderFactory.class); + RabbitMessageChannelBinder rabbitBinder = (RabbitMessageChannelBinder) binderFactory.getBinder(null, + MessageChannel.class); + RabbitConsumerProperties rProps = new RabbitConsumerProperties(); + rProps.setContainerType(ContainerType.STREAM); + ExtendedConsumerProperties props = + new ExtendedConsumerProperties(rProps); + props.setAutoStartup(false); + Binding binding = rabbitBinder.bindConsumer("testStream", "grp", new QueueChannel(), props); + Object container = TestUtils.getPropertyValue(binding, "lifecycle.messageListenerContainer"); + assertThat(container).isInstanceOf(StreamListenerContainer.class); + ((StreamListenerContainer) container).start(); + verify(this.context.getBean(ConsumerBuilder.class)).offset(OffsetSpecification.first()); + ((StreamListenerContainer) container).stop(); + } + + @Test + void testStreamHandler() { + context = new SpringApplicationBuilder(SimpleProcessor.class) + .web(WebApplicationType.NONE) + .run("--server.port=0"); + BinderFactory binderFactory = context.getBean(BinderFactory.class); + RabbitMessageChannelBinder rabbitBinder = (RabbitMessageChannelBinder) binderFactory.getBinder(null, + MessageChannel.class); + RabbitProducerProperties rProps = new RabbitProducerProperties(); + rProps.setProducerType(ProducerType.STREAM_SYNC); + ExtendedProducerProperties props = + new ExtendedProducerProperties(rProps); + Binding binding = rabbitBinder.bindProducer("testStream", new DirectChannel(), props); + Object handler = TestUtils.getPropertyValue(binding, "lifecycle"); + assertThat(handler).isInstanceOf(RabbitStreamMessageHandler.class); + } + + @SpringBootApplication(proxyBeanMethods = false) + public static class SimpleProcessor { + + @Bean + ProducerMessageHandlerCustomizer handlerCustomizer() { + return (hand, dest) -> { + RabbitStreamMessageHandler handler = (RabbitStreamMessageHandler) hand; + handler.setConfirmTimeout(5000); + }; + } + + @Bean + ListenerContainerCustomizer containerCustomizer() { + return (cont, dest, group) -> { + StreamListenerContainer container = (StreamListenerContainer) cont; + container.setConsumerCustomizer((name, builder) -> { + builder.offset(OffsetSpecification.first()); + }); + }; + } + + @Bean + Environment env(ConsumerBuilder consumerBuilder, ProducerBuilder producerBuilder) { + Environment env = mock(Environment.class); + given(env.consumerBuilder()).willReturn(consumerBuilder); + given(env.producerBuilder()).willReturn(producerBuilder); + return env; + } + + @Bean + ConsumerBuilder consumerBuilder() { + return mock(ConsumerBuilder.class); + } + + @Bean + ProducerBuilder producerBuilder() { + return mock(ProducerBuilder.class); + } + + } + +} diff --git a/r-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/stream/RabbitStreamMessageHandlerTests.java b/r-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/stream/RabbitStreamMessageHandlerTests.java new file mode 100644 index 000000000..780ea5f4b --- /dev/null +++ b/r-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/stream/RabbitStreamMessageHandlerTests.java @@ -0,0 +1,116 @@ +/* + * Copyright 2021-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.stream.binder.rabbit.stream; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import com.rabbitmq.stream.Address; +import com.rabbitmq.stream.Consumer; +import com.rabbitmq.stream.Environment; +import com.rabbitmq.stream.OffsetSpecification; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.stream.binder.rabbit.RabbitStreamMessageHandler; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.rabbit.stream.producer.RabbitStreamTemplate; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Gary Russell + * @since 3.2 + * + */ +public class RabbitStreamMessageHandlerTests extends AbstractIntegrationTests { + + @Test + void convertAndSend() throws InterruptedException { + Environment env = Environment.builder() + .lazyInitialization(true) + .addressResolver(add -> new Address("localhost", streamPort())) + .build(); + try { + env.deleteStream("stream.stream"); + } + catch (Exception e) { + } + env.streamCreator().stream("stream.stream").create(); + RabbitStreamTemplate streamTemplate = new RabbitStreamTemplate(env, "stream.stream"); + RabbitStreamMessageHandler handler = new RabbitStreamMessageHandler(streamTemplate); + handler.setSync(true); + handler.handleMessage(MessageBuilder.withPayload("foo") + .setHeader("bar", "baz") + .build()); + CountDownLatch latch = new CountDownLatch(1); + AtomicReference received = new AtomicReference<>(); + Consumer consumer = env.consumerBuilder().stream("stream.stream") + .offset(OffsetSpecification.first()) + .messageHandler((context, msg) -> { + received.set(msg); + latch.countDown(); + }) + .build(); + assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(received.get()).isNotNull(); + assertThat(received.get().getBodyAsBinary()).isEqualTo("foo".getBytes()); + assertThat((String) received.get().getApplicationProperties().get("bar")).isEqualTo("baz"); + consumer.close(); + handler.stop(); + } + + @Test + @Disabled + void sendNative() throws InterruptedException { + Environment env = Environment.builder() + .lazyInitialization(true) + .build(); + try { + env.deleteStream("stream.stream"); + } + catch (Exception e) { + } + env.streamCreator().stream("stream.stream").create(); + RabbitStreamTemplate streamTemplate = new RabbitStreamTemplate(env, "stream.stream"); + RabbitStreamMessageHandler handler = new RabbitStreamMessageHandler(streamTemplate); + handler.setSync(true); + handler.handleMessage(MessageBuilder.withPayload(streamTemplate.messageBuilder() + .addData("foo".getBytes()) + .applicationProperties().entry("bar", "baz") + .messageBuilder() + .build()) + .build()); + CountDownLatch latch = new CountDownLatch(1); + AtomicReference received = new AtomicReference<>(); + Consumer consumer = env.consumerBuilder().stream("stream.stream") + .offset(OffsetSpecification.first()) + .messageHandler((context, msg) -> { + received.set(msg); + latch.countDown(); + }) + .build(); + assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(received.get()).isNotNull(); + assertThat(received.get().getBodyAsBinary()).isEqualTo("foo".getBytes()); + assertThat((String) received.get().getApplicationProperties().get("bar")).isEqualTo("baz"); + consumer.close(); + handler.stop(); + } + +} diff --git a/r-binder/spring-cloud-stream-binder-rabbit/src/test/resources/log4j.properties b/r-binder/spring-cloud-stream-binder-rabbit/src/test/resources/log4j.properties new file mode 100644 index 000000000..335956bc0 --- /dev/null +++ b/r-binder/spring-cloud-stream-binder-rabbit/src/test/resources/log4j.properties @@ -0,0 +1,8 @@ +log4j.rootCategory=DEBUG, stdout + +# standard logging including calling site +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %40.40c:%4L - %m%n + +log4j.category.org.springframework.cloud.binder.rabbit=DEBUG diff --git a/r-binder/src/checkstyle/checkstyle-suppressions.xml b/r-binder/src/checkstyle/checkstyle-suppressions.xml new file mode 100644 index 000000000..9d0be9549 --- /dev/null +++ b/r-binder/src/checkstyle/checkstyle-suppressions.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/r-binder/update-version.sh b/r-binder/update-version.sh new file mode 100755 index 000000000..bc0672b8e --- /dev/null +++ b/r-binder/update-version.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +#Execute this script from local checkout of spring cloud stream + +./mvnw versions:update-parent -DparentVersion=[0.0.1,$2] -Pspring -DgenerateBackupPoms=false -DallowSnapshots=true +./mvnw versions:set -DnewVersion=$1 -DgenerateBackupPoms=false + + + +lines=$(find . -name 'pom.xml' | xargs egrep "SNAPSHOT|M[0-9]|RC[0-9]" | grep -v regex | wc -l) +if [ $lines -eq 0 ]; then + echo "No snapshots found" +else + echo "Snapshots found." +fi + +lines=$(find . -name 'pom.xml' | xargs egrep "M[0-9]" | grep -v regex | wc -l) +if [ $lines -eq 0 ]; then + echo "No milestones found" +else + echo "Milestones found." +fi + +lines=$(find . -name 'pom.xml' | xargs egrep "RC[0-9]" | grep -v regex | wc -l) +if [ $lines -eq 0 ]; then + echo "No release candidates found" +else + echo "Release candidates found." +fi