Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ jobs:
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v7
with:
node-version-file: 'docs/.nvmrc'
- name: Install dependencies
Expand All @@ -31,7 +31,7 @@ jobs:
run: npm run build
working-directory: docs
- name: Upload artifact
uses: actions/upload-pages-artifact@v4
uses: actions/upload-pages-artifact@v5
with:
path: 'docs/build/'
- name: Deploy to GitHub Pages
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ jobs:
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- name: Verify Conventional Commits
uses: amannn/action-semantic-pull-request@v5
if: github.event_name == 'pull_request' || github.event_name == 'pull_request_target'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Set up NodeJS
if: github.ref == 'refs/heads/master'
uses: actions/setup-node@v6
uses: actions/setup-node@v7
with:
node-version-file: '.nvmrc'
- name: Set up Semantic Release
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- name: Set env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/v}" >> $GITHUB_ENV
- name: Upgrade Gradle.properties
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/updater.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- name: Set up JDK 25
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: 25
- name: Check for updates
run: ./gradlew internalTask --action UPDATE_DEPENDENCIES
- name: Gradle Wrapper
run: ./gradlew wrapper
- name: Check for changes
id: git_changes
run: |
Expand Down
2 changes: 1 addition & 1 deletion async/async-commons/async-commons.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ dependencies {
compileOnly 'io.projectreactor:reactor-core'
api 'tools.jackson.core:jackson-databind'
implementation 'commons-io:commons-io:2.22.0'
implementation 'io.cloudevents:cloudevents-core:4.1.1'
implementation 'io.cloudevents:cloudevents-core:5.0.0'

testImplementation 'io.projectreactor:reactor-test'
}
2 changes: 1 addition & 1 deletion async/async-kafka/async-kafka.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ dependencies {
api project(':async-commons')
api project(':cloudevents-json-jackson')
api 'io.projectreactor.kafka:reactor-kafka:1.3.25'
implementation 'io.cloudevents:cloudevents-core:4.1.1'
implementation 'io.cloudevents:cloudevents-core:5.0.0'
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ public ApplicationEventListener(ReactiveMessageListener receiver,
int retryDelay,
DiscardNotifier discardNotifier,
CustomReporter errorReporter,
String appName) {
String groupId) {
super(receiver, withDLQRetry, createTopology, maxRetries, retryDelay, discardNotifier,
"event", errorReporter, appName + "-events", resolver.getEventNames());
"event", errorReporter, groupId, resolver.getEventNames());
this.resolver = resolver;
this.messageConverter = messageConverter;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@
import org.reactivecommons.async.commons.communications.Message;
import org.reactivecommons.async.commons.converters.MessageConverter;
import org.reactivecommons.async.kafka.communications.ReactiveMessageListener;
import org.reactivecommons.async.kafka.communications.topology.TopologyCreator;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
Expand Down Expand Up @@ -52,7 +55,7 @@ void setup() {
1000,
null,
null,
"testApp"
"testApp-events"
);

}
Expand Down Expand Up @@ -96,4 +99,30 @@ void shouldHandleRawMessageSuccessfullyWhenCloudEvent() {
verify(resolver, times(1)).getEventListener(anyString());
verify(messageConverter, times(1)).readCloudEvent(any(Message.class));
}

@Test
void shouldUseTheGivenGroupId() {
ApplicationEventListener listener = buildListener("dummy.consumer-group");
when(receiver.getMaxConcurrency()).thenReturn(1);
when(receiver.listen(anyString(), any())).thenReturn(Flux.never());

listener.startListener(mock(TopologyCreator.class));

verify(receiver, times(1)).listen(eq("dummy.consumer-group"), any());
}

private ApplicationEventListener buildListener(String groupId) {
return new ApplicationEventListener(
receiver,
resolver,
messageConverter,
true,
false,
3,
1000,
null,
null,
groupId
);
}
}
2 changes: 1 addition & 1 deletion async/async-rabbit/async-rabbit.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ dependencies {
api 'com.rabbitmq:amqp-client'
api 'tools.jackson.core:jackson-databind'

implementation 'io.cloudevents:cloudevents-core:4.1.1'
implementation 'io.cloudevents:cloudevents-core:5.0.0'
testImplementation 'io.projectreactor:reactor-test'
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ ext {

dependencies {
api 'tools.jackson.core:jackson-databind'
implementation 'io.cloudevents:cloudevents-api:4.1.1'
implementation 'io.cloudevents:cloudevents-core:4.1.1'
implementation 'io.cloudevents:cloudevents-api:5.0.0'
implementation 'io.cloudevents:cloudevents-core:5.0.0'
}
94 changes: 48 additions & 46 deletions docs/docs/reactive-commons/configuration_properties/1-rabbitmq.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ sidebar_position: 1

# RabbitMQ Configuration

This page describes how to configure RabbitMQ connection and messaging properties for each **domain** in
Reactive Commons. A domain represents an independent connection to a RabbitMQ broker. Your application can work
with a single domain (one broker) or multiple domains (several independent brokers), each with its own properties.
See [Communication Scenarios](/reactive-commons-java/docs/category/communication-scenarios) for guidance on when
to use multiple domains.
This page describes how to configure RabbitMQ connection and messaging properties for each **domain** in Reactive
Commons. A domain represents an independent connection to a RabbitMQ broker. Your application can work with a single
domain (one broker) or multiple domains (several independent brokers), each with its own properties.
See [Communication Scenarios](/reactive-commons-java/docs/category/communication-scenarios) for guidance on when to use
multiple domains.

All available properties are defined in the
[AsyncProps](https://github.com/reactive-commons/reactive-commons-java/blob/master/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/props/AsyncProps.java)
class. There are two ways to provide these values via `application.yaml` or a combination of YAML and
programmatic configuration, as described in the [Configuration approaches](#configuration-approaches) section below.
class. There are two ways to provide these values via `application.yaml` or a combination of YAML and programmatic
configuration, as described in the [Configuration approaches](#configuration-approaches) section below.

```yaml title="application.yaml"
app:
Expand Down Expand Up @@ -79,12 +79,12 @@ properties do not depend on runtime values such as secrets.

### Approach 2: Hybrid YAML + `RabbitPropsCustomizer`

Use this approach when you want to define the domain structure in YAML (topology, retry settings, etc.) but need to
set some properties at runtime for example, loading connection credentials from a secrets manager.
Use this approach when you want to define the domain structure in YAML (topology, retry settings, etc.) but need to set
some properties at runtime for example, loading connection credentials from a secrets manager.

Declare your domains in `application.yaml` as usual, then define a `RabbitPropsCustomizer` bean to override specific
properties after the YAML is loaded. The customizer receives the full map of configured domains and can modify
any property on any domain.
properties after the YAML is loaded. The customizer receives the full map of configured domains and can modify any
property on any domain.

:::caution[YAML domains are optional]
The `RabbitPropsCustomizer` can work with or without pre-existing YAML domains. If no domains are defined in your
Expand All @@ -95,7 +95,7 @@ executes, otherwise an `InvalidConfigurationException` is thrown.

You have two options:

**Option A: Define domains in YAML, then override with customizer**
**Option A: Define domains in YAML, then merge overrides with the customizer**

Declare your domains in `application.yaml` as usual, then use the customizer to override or extend them.

Expand All @@ -114,7 +114,6 @@ app:
package sample;

import org.reactivecommons.async.rabbit.config.RabbitProperties;
import org.reactivecommons.async.rabbit.config.props.AsyncProps;
import org.reactivecommons.async.rabbit.config.props.AsyncPropsDomain;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand All @@ -132,27 +131,31 @@ public class RabbitMQConfig {
@Bean
public AsyncPropsDomain.RabbitPropsCustomizer rabbitPropsCustomizer() {
return domainProperties -> {
// Customize the "app" domain — overrides take precedence over YAML values
AsyncProps app = domainProperties.get("app");
if (app != null) {
app.setConnectionProperties(loadFromSecret("secret-app-rabbit"));
}
// Customize the "app" domain — YAML values are kept, only these fields are overridden
domainProperties.customize("app", app -> app.setConnectionProperties(
loadFromSecret("secret-app-rabbit")
));

// Customize the "accounts" domain independently
AsyncProps accounts = domainProperties.get("accounts");
if (accounts != null) {
accounts.setConnectionProperties(loadFromSecret("secret-accounts-rabbit"));
}
domainProperties.customize("accounts", accounts -> accounts.setConnectionProperties(
loadFromSecret("secret-accounts-rabbit")
));
};
}
}
```

:::danger[`put` replaces the domain, `customize` merges it]
`domainProperties` is a map of domains, so `domainProperties.put("app", AsyncProps.builder()...build())`
**replaces the whole domain** and every value bound from `application.yaml` for that domain is lost, going back to its
defaults. Use `customize(domain, props -> ...)` to merge, and `put(domain, props)` only when you intend to define the
whole domain programmatically.
:::

**Option B: Define all domains in the customizer (no YAML domains)**

If you prefer full programmatic control, **omit the `app.async` section entirely from your `application.yaml`** and
define all domains
inside the customizer:
define all domains inside the customizer:

```java
package sample;
Expand Down Expand Up @@ -194,21 +197,25 @@ public class RabbitMQConfig {
**Key rules for the hybrid approach:**

- Properties set in the customizer **take precedence** over YAML values.
- YAML values not touched by the customizer are **preserved**.
- YAML values not touched by the customizer are **preserved**, as long as you use
`domainProperties.customize("domain", props -> ...)` or mutate the instance returned by
`domainProperties.get("domain")`.
- `domainProperties.put("domain", props)` **replaces** the whole domain: use it to define domains programmatically, not
to override a few properties of a domain declared in YAML.
- The customizer can also **add new domains** by calling `domainProperties.put("newDomain", asyncProps)`.

## Loading properties from a secret

:::danger[Deprecated]
Using `AsyncPropsDomain.RabbitSecretFiller` to load secrets is **deprecated** and will be removed in a future version.
Use **[Approach 2: Hybrid YAML + `RabbitPropsCustomizer`](#approach-2-hybrid-yaml--rabbitpropscustomizer)** instead,
which provides full control over all domain properties at runtime and is the recommended way to integrate with a
secrets manager.
which provides full control over all domain properties at runtime and is the recommended way to integrate with a secrets
manager.
:::

The recommended way to load connection properties from a secrets manager is to use the `RabbitPropsCustomizer` (see
[Approach 2](#approach-2-hybrid-yaml--rabbitpropscustomizer)). This gives you full control over all domain properties
at runtime. The example below uses the [Secrets Manager](https://github.com/bancolombia/secrets-manager) library.
[Approach 2](#approach-2-hybrid-yaml--rabbitpropscustomizer)). This gives you full control over all domain properties at
runtime. The example below uses the [Secrets Manager](https://github.com/bancolombia/secrets-manager) library.

1. Create a `@ConfigurationProperties` record to map the secret fields:

Expand Down Expand Up @@ -330,8 +337,8 @@ public class RabbitMQConfig {
## Customizing the connection

For advanced control over the RabbitMQ connection, you can define a `ConnectionFactoryCustomizer` bean. This allows you
to configure options that are not exposed through standard properties, such as custom timeouts, SSL/TLS settings,
or automatic recovery strategies:
to configure options that are not exposed through standard properties, such as custom timeouts, SSL/TLS settings, or
automatic recovery strategies:

```java

Expand Down Expand Up @@ -373,7 +380,7 @@ two brokers, which means the described scenarios are limited to a maximum of two
### 1. Sending messages (single domain)

> In this scenario we only use annotations to enable message sending only, along with different configurations for the
`listenReplies` property:
> `listenReplies` property:

| Enabled annotations | listenReplies | Broker | Connections | Channels |
|----------------------------|---------------|------------|-------------|----------|
Expand All @@ -383,7 +390,7 @@ two brokers, which means the described scenarios are limited to a maximum of two
### 2. Sending messages (multiple domains)

> In this scenario, we only send messages to two brokers, using one or all of the annotations and configurations for the
`listenReplies` property:
> `listenReplies` property:

| Enabled annotations | listenReplies | Broker | Connections | Channels |
|----------------------------|---------------|-------------------|-------------|----------|
Expand Down Expand Up @@ -527,9 +534,8 @@ app:
```

Now we configure the return handler to manage messages that could not be delivered correctly. By default, these messages
are displayed in a log.
To customize this behavior, a class that implements the `UnroutableMessageHandler` interface is created and registered
as a Spring bean:
are displayed in a log. To customize this behavior, a class that implements the `UnroutableMessageHandler` interface is
created and registered as a Spring bean:

```java
package sample;
Expand Down Expand Up @@ -574,8 +580,8 @@ To send the unrouted message to a queue, we use the `@EnableDomainEventBus` anno
for [commands](/reactive-commons-java/docs/reactive-commons/sending-a-command) and
[asynchronous queries](/reactive-commons-java/docs/reactive-commons/making-an-async-query), as appropriate.

It is important to ensure that the queue exists before sending the message, as it will otherwise be lost.
Therefore, it is recommended to verify or create the queue beforehand to ensure successful delivery.
It is important to ensure that the queue exists before sending the message, as it will otherwise be lost. Therefore, it
is recommended to verify or create the queue beforehand to ensure successful delivery.

```java
package sample;
Expand Down Expand Up @@ -730,8 +736,7 @@ public class RabbitMQConfig {
### PRECONDITION_FAILED - inequivalent arg 'x-dead-letter-exchange'

This error occurs when there is a mismatch between the queue properties defined in your application and the properties
of the queue that already exists in the RabbitMQ broker.
It commonly happens when you try to:
of the queue that already exists in the RabbitMQ broker. It commonly happens when you try to:

- Change the name of a domain.
- Enable or disable DLQ (Dead Letter Queue) functionality for a queue that has already been created.
Expand All @@ -748,13 +753,10 @@ of type 'longstr', class-id=50, method-id=10)
**Cause:**

RabbitMQ does not allow changing certain durable properties of a queue after it has been declared, such as the
`x-dead-letter-exchange` argument.
When your application starts, it tries to declare the queue with the new properties, but the broker rejects the
declaration because it conflicts
with the existing queue.
`x-dead-letter-exchange` argument. When your application starts, it tries to declare the queue with the new properties,
but the broker rejects the declaration because it conflicts with the existing queue.

**Solution:**

To resolve this issue, you must manually delete the conflicting queues from the RabbitMQ broker. Once the queues are
deleted,
you can restart the microservice to recreate them with the correct, updated properties.
deleted, you can restart the microservice to recreate them with the correct, updated properties.
Loading
Loading