diff --git a/.github/workflows/maven.yaml b/.github/workflows/maven.yaml
index a55f1443..859d820a 100644
--- a/.github/workflows/maven.yaml
+++ b/.github/workflows/maven.yaml
@@ -4,21 +4,21 @@
name: Build
on:
push:
- branches: [ main, 3.1.x ]
+ branches: [ main, 4.1.x, 4.0.x, 3.1.x ]
pull_request:
- branches: [ main, 3.1.x ]
+ branches: [ main, 4.1.x, 4.0.x, 3.1.x ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- name: Set up JDK
- uses: actions/setup-java@v2
+ uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Cache local Maven repository
- uses: actions/cache@v2
+ uses: actions/cache@v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
@@ -27,12 +27,12 @@ jobs:
- name: Build with Maven
run: ./mvnw -s .settings.xml clean org.jacoco:jacoco-maven-plugin:prepare-agent install -U -P sonar -nsu --batch-mode -Dmaven.test.redirectTestOutputToFile=true -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn
- name: Publish Test Report
- uses: mikepenz/action-junit-report@v2
+ uses: mikepenz/action-junit-report@v5
if: always() # always run even if the previous step fails
with:
report_paths: '**/surefire-reports/TEST-*.xml'
- name: Archive code coverage results
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v4
with:
name: surefire-reports
path: '**/surefire-reports/*'
diff --git a/docs/modules/ROOT/pages/quickstart.adoc b/docs/modules/ROOT/pages/quickstart.adoc
index 98a17bfc..effd3f09 100644
--- a/docs/modules/ROOT/pages/quickstart.adoc
+++ b/docs/modules/ROOT/pages/quickstart.adoc
@@ -20,11 +20,11 @@ spring:
----
The bus currently supports sending messages to all nodes listening or all nodes for a
-particular service (as defined by Eureka). The `/bus/*` actuator namespace has some HTTP
-endpoints. Currently, two are implemented. The first, `/bus/env`, sends key/value pairs to
-update each node's Spring Environment. The second, `/bus/refresh`, reloads each
+particular service (as defined by Eureka). The `/bus*` actuator namespace has some HTTP
+endpoints. Currently, three are implemented. The first, `/busenv`, sends key/value pairs to
+update each node's Spring Environment. The second, `/busrefresh`, reloads each
application's configuration, as though they had all been pinged on their `/refresh`
-endpoint.
+endpoint. The third `/busshutdown` sends a shutdown event to gracefully shutdown the application instance(s).
NOTE: The Spring Cloud Bus starters cover Rabbit and Kafka, because those are the two most
common implementations. However, Spring Cloud Stream is quite flexible, and the binder
diff --git a/docs/modules/ROOT/pages/spring-cloud-bus/bus-endpoints.adoc b/docs/modules/ROOT/pages/spring-cloud-bus/bus-endpoints.adoc
index 76ff8853..51089b92 100644
--- a/docs/modules/ROOT/pages/spring-cloud-bus/bus-endpoints.adoc
+++ b/docs/modules/ROOT/pages/spring-cloud-bus/bus-endpoints.adoc
@@ -2,9 +2,9 @@
= Bus Endpoints
:page-section-summary-toc: 1
-Spring Cloud Bus provides two endpoints, `/actuator/busrefresh` and `/actuator/busenv`
+Spring Cloud Bus provides three endpoints, `/actuator/busrefresh`, `/actutator/busshutdown` and `/actuator/busenv`
that correspond to individual actuator endpoints in Spring Cloud Commons,
-`/actuator/refresh` and `/actuator/env` respectively.
+`/actuator/refresh`, `/actuator/shutdown`, and `/actuator/env` respectively.
[[bus-refresh-endpoint]]
== Bus Refresh Endpoint
@@ -41,4 +41,33 @@ The `/actuator/busenv` endpoint accepts `POST` requests with the following shape
"name": "key1",
"value": "value1"
}
-----
\ No newline at end of file
+----
+
+[[bus-shutdown-endpoint]]
+== Bus Shutdown Endpoint
+The `/actuator/busshutdown` shuts down the application https://docs.spring.io/spring-boot/reference/web/graceful-shutdown.html[gracefully].
+
+To expose the `/actuator/busshutdown` endpoint, you need to add following configuration to your
+application:
+
+[source,properties]
+----
+management.endpoints.web.exposure.include=busshutdown
+----
+
+You can make a request to the `busshutdown` endpoint by issuing a `POST` request.
+
+If you would like to target a specific application you can issue a `POST` request to `/busshutdown` and optionally
+specify the bus id:
+
+[source,bash]
+----
+$ curl -X POST http://localhost:8080/actuator/busshutdown
+----
+
+You can also target a specific application instance by specifying the bus id:
+
+[source,bash]
+----
+$ curl -X POST http://localhost:8080/actuator/busshutdown/busid:123
+----
diff --git a/docs/modules/ROOT/partials/_configprops.adoc b/docs/modules/ROOT/partials/_configprops.adoc
index 3c5e4634..39eb4f16 100644
--- a/docs/modules/ROOT/partials/_configprops.adoc
+++ b/docs/modules/ROOT/partials/_configprops.adoc
@@ -9,6 +9,7 @@
|spring.cloud.bus.env.enabled | `+++true+++` | Flag to switch off environment change events (default on).
|spring.cloud.bus.id | `+++application+++` | The identifier for this application instance.
|spring.cloud.bus.refresh.enabled | `+++true+++` | Flag to switch off refresh events (default on).
+|spring.cloud.bus.shutdown.enabled | `+++true+++` | Flag to switch off shutdown events (default on).
|spring.cloud.bus.trace.enabled | `+++false+++` | Flag to switch on tracing of acks (default off).
|===
\ No newline at end of file
diff --git a/spring-cloud-bus-rsocket/src/main/java/org/springframework/cloud/bus/rsocket/BusRSocketAutoConfiguration.java b/spring-cloud-bus-rsocket/src/main/java/org/springframework/cloud/bus/rsocket/BusRSocketAutoConfiguration.java
index 264173a2..79f2fb40 100644
--- a/spring-cloud-bus-rsocket/src/main/java/org/springframework/cloud/bus/rsocket/BusRSocketAutoConfiguration.java
+++ b/spring-cloud-bus-rsocket/src/main/java/org/springframework/cloud/bus/rsocket/BusRSocketAutoConfiguration.java
@@ -26,6 +26,7 @@
import org.springframework.cloud.bus.BusAutoConfiguration;
import org.springframework.cloud.bus.BusProperties;
import org.springframework.cloud.bus.BusRefreshAutoConfiguration;
+import org.springframework.cloud.bus.BusShutdownAutoConfiguration;
import org.springframework.cloud.bus.ConditionalOnBusEnabled;
import org.springframework.cloud.bus.PathServiceMatcherAutoConfiguration;
import org.springframework.context.annotation.Bean;
@@ -39,7 +40,7 @@
@EnableConfigurationProperties(BusRSocketProperties.class)
@ConditionalOnClass({ RSocket.class, RoutingRSocketRequester.class })
@AutoConfigureBefore({ BusAutoConfiguration.class, BusRefreshAutoConfiguration.class,
- PathServiceMatcherAutoConfiguration.class })
+ PathServiceMatcherAutoConfiguration.class, BusShutdownAutoConfiguration.class })
public class BusRSocketAutoConfiguration {
@Bean
diff --git a/spring-cloud-bus-tests/pom.xml b/spring-cloud-bus-tests/pom.xml
index f2ed0b35..5d68fb9d 100644
--- a/spring-cloud-bus-tests/pom.xml
+++ b/spring-cloud-bus-tests/pom.xml
@@ -17,10 +17,6 @@
..
-
- 1.17.6
-
-
org.springframework.boot
@@ -47,16 +43,19 @@
spring-boot-starter-test
test
+
+ org.springframework.boot
+ spring-boot-testcontainers
+ test
+
org.testcontainers
junit-jupiter
- ${testcontainers.version}
test
org.testcontainers
rabbitmq
- ${testcontainers.version}
test
diff --git a/spring-cloud-bus-tests/src/test/java/org/springframework/cloud/bus/ShutdownListenerIntegrationTests.java b/spring-cloud-bus-tests/src/test/java/org/springframework/cloud/bus/ShutdownListenerIntegrationTests.java
new file mode 100644
index 00000000..84ec29a0
--- /dev/null
+++ b/spring-cloud-bus-tests/src/test/java/org/springframework/cloud/bus/ShutdownListenerIntegrationTests.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2012-2024 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.bus;
+
+import java.util.concurrent.CountDownLatch;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.testcontainers.containers.RabbitMQContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.SpringBootConfiguration;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
+import org.springframework.cloud.bus.event.EnvironmentChangeRemoteApplicationEvent;
+import org.springframework.context.ApplicationListener;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.test.web.reactive.server.WebTestClient;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
+
+/**
+ * @author Ryan Baxter
+ */
+@SpringBootTest(webEnvironment = RANDOM_PORT,
+ properties = { "management.endpoints.web.exposure.include=*",
+ "spring.cloud.stream.bindings.springCloudBusOutput.producer.errorChannelEnabled=true",
+ "logging.level.org.springframework.cloud.bus=TRACE", "spring.cloud.bus.id=app:1" })
+@Testcontainers
+public class ShutdownListenerIntegrationTests {
+
+ private static ConfigurableApplicationContext context;
+
+ @Container
+ @ServiceConnection
+ private static final RabbitMQContainer rabbitMQContainer = new RabbitMQContainer("rabbitmq:4.0-management");
+
+ @BeforeAll
+ static void before() {
+ context = new SpringApplicationBuilder(TestConfig.class)
+ .properties("server.port=0", "spring.rabbitmq.host=" + rabbitMQContainer.getHost(),
+ "spring.rabbitmq.port=" + rabbitMQContainer.getAmqpPort(),
+ "management.endpoints.web.exposure.include=*", "spring.cloud.bus.id=app:2", "debug=true")
+ .run();
+ }
+
+ @AfterAll
+ static void after() {
+ if (context != null) {
+ context.close();
+ }
+ }
+
+ @Test
+ void testShutdown(@Autowired WebTestClient client) {
+ assertThat(rabbitMQContainer.isRunning());
+ client.post().uri("/actuator/busshutdown/app:2").exchange().expectStatus().is2xxSuccessful();
+ assertThat(context.isClosed());
+ }
+
+ @SpringBootConfiguration
+ @EnableAutoConfiguration
+ static class TestConfig implements ApplicationListener {
+
+ CountDownLatch latch = new CountDownLatch(1);
+
+ @Override
+ public void onApplicationEvent(EnvironmentChangeRemoteApplicationEvent event) {
+ latch.countDown();
+ }
+
+ }
+
+}
diff --git a/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/BusShutdownAutoConfiguration.java b/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/BusShutdownAutoConfiguration.java
new file mode 100644
index 00000000..1167b0fd
--- /dev/null
+++ b/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/BusShutdownAutoConfiguration.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2012-2024 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.bus;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.cloud.bus.endpoint.ShutdownBusEndpoint;
+import org.springframework.cloud.bus.event.Destination;
+import org.springframework.cloud.bus.event.ShutdownListener;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @author Ryan Baxter
+ */
+@Configuration(proxyBeanMethods = false)
+@ConditionalOnBusEnabled
+public class BusShutdownAutoConfiguration {
+
+ @Bean
+ @ConditionalOnProperty(value = "spring.cloud.bus.shutdown.enabled", matchIfMissing = true)
+ @ConditionalOnMissingBean
+ public ShutdownListener shutdownListener(ServiceMatcher serviceMatcher) {
+ return new ShutdownListener(serviceMatcher);
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ @ConditionalOnClass(name = { "org.springframework.boot.actuate.endpoint.annotation.Endpoint" })
+ protected static class BusShutdownEndpointConfiguration {
+
+ @Bean
+ public ShutdownBusEndpoint shutdownBusEndpoint(ApplicationEventPublisher publisher, BusProperties bus,
+ Destination.Factory destinationFactory) {
+ return new ShutdownBusEndpoint(publisher, bus.getId(), destinationFactory);
+ }
+
+ }
+
+}
diff --git a/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/endpoint/ShutdownBusEndpoint.java b/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/endpoint/ShutdownBusEndpoint.java
new file mode 100644
index 00000000..5b755539
--- /dev/null
+++ b/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/endpoint/ShutdownBusEndpoint.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2012-2024 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.bus.endpoint;
+
+import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
+import org.springframework.boot.actuate.endpoint.annotation.Selector;
+import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
+import org.springframework.cloud.bus.event.Destination;
+import org.springframework.cloud.bus.event.ShutdownRemoteApplicationEvent;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.util.StringUtils;
+
+/**
+ * @author Ryan Baxter
+ */
+@Endpoint(id = "busshutdown")
+public class ShutdownBusEndpoint extends AbstractBusEndpoint {
+
+ public ShutdownBusEndpoint(ApplicationEventPublisher publisher, String id, Destination.Factory destinationFactory) {
+ super(publisher, id, destinationFactory);
+ }
+
+ @WriteOperation
+ public void busShutdownWithDestination(@Selector(match = Selector.Match.ALL_REMAINING) String[] destinations) {
+ String destination = StringUtils.arrayToDelimitedString(destinations, ":");
+ publish(new ShutdownRemoteApplicationEvent(this, getInstanceId(), getDestination(destination)));
+ }
+
+ @WriteOperation
+ public void busShutdown() {
+ publish(new ShutdownRemoteApplicationEvent(this, getInstanceId(), getDestination(null)));
+ }
+
+}
diff --git a/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/event/ShutdownListener.java b/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/event/ShutdownListener.java
new file mode 100644
index 00000000..528f5dc9
--- /dev/null
+++ b/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/event/ShutdownListener.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2012-2024 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.bus.event;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.beans.BeansException;
+import org.springframework.boot.SpringApplication;
+import org.springframework.cloud.bus.ServiceMatcher;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.context.ApplicationListener;
+
+/**
+ * @author Ryan Baxter
+ */
+public class ShutdownListener implements ApplicationListener, ApplicationContextAware {
+
+ private static final Log LOG = LogFactory.getLog(ShutdownListener.class);
+
+ private ApplicationContext context;
+
+ private ServiceMatcher serviceMatcher;
+
+ public ShutdownListener(ServiceMatcher serviceMatcher) {
+ this.serviceMatcher = serviceMatcher;
+ }
+
+ @Override
+ public void onApplicationEvent(ShutdownRemoteApplicationEvent event) {
+ if (serviceMatcher.isForSelf(event)) {
+ LOG.warn("Received remote shutdown request from " + event.getOriginService() + ". Shutting down.");
+ shutdown();
+ }
+ else {
+ LOG.info("Shutdown not performed, the event was targeting " + event.getDestinationService());
+ }
+
+ }
+
+ protected int shutdown() {
+ return SpringApplication.exit(context, () -> 0);
+ }
+
+ @Override
+ public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+ this.context = applicationContext;
+ }
+
+}
diff --git a/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/event/ShutdownRemoteApplicationEvent.java b/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/event/ShutdownRemoteApplicationEvent.java
new file mode 100644
index 00000000..2283224d
--- /dev/null
+++ b/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/event/ShutdownRemoteApplicationEvent.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2012-2024 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.bus.event;
+
+/**
+ * Event which indicates the application should be shutdown.
+ *
+ * @author Ryan Baxter
+ */
+public class ShutdownRemoteApplicationEvent extends RemoteApplicationEvent {
+
+ @SuppressWarnings("unused")
+ private ShutdownRemoteApplicationEvent() {
+ // for serializers
+ }
+
+ public ShutdownRemoteApplicationEvent(Object source, String originService, Destination destination) {
+ super(source, originService, destination);
+ }
+
+}
diff --git a/spring-cloud-bus/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-bus/src/main/resources/META-INF/additional-spring-configuration-metadata.json
index 06714087..7e8b5dce 100644
--- a/spring-cloud-bus/src/main/resources/META-INF/additional-spring-configuration-metadata.json
+++ b/spring-cloud-bus/src/main/resources/META-INF/additional-spring-configuration-metadata.json
@@ -12,6 +12,12 @@
"description": "Flag to switch off refresh events (default on).",
"defaultValue": true
},
+ {
+ "name": "spring.cloud.bus.shutdown.enabled",
+ "type": "java.lang.Boolean",
+ "description": "Flag to switch off shutdown events (default on).",
+ "defaultValue": true
+ },
{
"name": "spring.cloud.bus.trace.enabled",
"type": "java.lang.Boolean",
diff --git a/spring-cloud-bus/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-cloud-bus/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
index ac7da219..cd6dc306 100644
--- a/spring-cloud-bus/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
+++ b/spring-cloud-bus/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -1,5 +1,6 @@
org.springframework.cloud.bus.PathServiceMatcherAutoConfiguration
org.springframework.cloud.bus.BusAutoConfiguration
org.springframework.cloud.bus.BusRefreshAutoConfiguration
+org.springframework.cloud.bus.BusShutdownAutoConfiguration
org.springframework.cloud.bus.BusStreamAutoConfiguration
-org.springframework.cloud.bus.jackson.BusJacksonAutoConfiguration
\ No newline at end of file
+org.springframework.cloud.bus.jackson.BusJacksonAutoConfiguration
diff --git a/spring-cloud-bus/src/test/java/org/springframework/cloud/bus/BusAutoConfigurationTests.java b/spring-cloud-bus/src/test/java/org/springframework/cloud/bus/BusAutoConfigurationTests.java
index ef7d5580..f0d61d81 100644
--- a/spring-cloud-bus/src/test/java/org/springframework/cloud/bus/BusAutoConfigurationTests.java
+++ b/spring-cloud-bus/src/test/java/org/springframework/cloud/bus/BusAutoConfigurationTests.java
@@ -32,11 +32,12 @@
import org.springframework.cloud.bus.event.RefreshRemoteApplicationEvent;
import org.springframework.cloud.bus.event.RemoteApplicationEvent;
import org.springframework.cloud.bus.event.SentApplicationEvent;
+import org.springframework.cloud.bus.event.ShutdownListener;
+import org.springframework.cloud.bus.event.ShutdownRemoteApplicationEvent;
import org.springframework.cloud.bus.event.UnknownRemoteApplicationEvent;
import org.springframework.cloud.context.refresh.ContextRefresher;
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
import org.springframework.cloud.stream.config.BindingProperties;
-import org.springframework.cloud.stream.config.BindingServiceProperties;
import org.springframework.cloud.stream.function.StreamBridge;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
@@ -48,7 +49,6 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
public class BusAutoConfigurationTests {
@@ -74,28 +74,61 @@ public void inboundNotForSelf() {
this.context = SpringApplication.run(InboundMessageHandlerConfiguration.class, "--spring.cloud.bus.id=foo",
"--server.port=0");
this.context.getBean(BusConstants.INPUT, MessageChannel.class)
- .send(new GenericMessage<>(new RefreshRemoteApplicationEvent(this, "bar", "bar")));
+ .send(new GenericMessage<>(new RefreshRemoteApplicationEvent(this, "bar",
+ new PathDestinationFactory().getDestination("bar"))));
assertThat(this.context.getBean(InboundMessageHandlerConfiguration.class).refresh).isNull();
}
+ @Test
+ public void shutdownInboundNotForSelf() {
+ this.context = SpringApplication.run(ShutdownInboundMessageHandlerConfiguration.class,
+ "--spring.cloud.bus.id=foo", "--server.port=0");
+ this.context.getBean(BusConstants.INPUT, MessageChannel.class)
+ .send(new GenericMessage<>(new ShutdownRemoteApplicationEvent(this, "bar",
+ new PathDestinationFactory().getDestination("bar"))));
+ assertThat(this.context.getBean(ShutdownInboundMessageHandlerConfiguration.class).shutdown).isNull();
+ }
+
@Test
public void inboundFromSelf() {
this.context = SpringApplication.run(InboundMessageHandlerConfiguration.class, "--spring.cloud.bus.id=foo",
"--server.port=0");
this.context.getBean(BusConstants.INPUT, MessageChannel.class)
- .send(new GenericMessage<>(new RefreshRemoteApplicationEvent(this, "foo", (String) null)));
+ .send(new GenericMessage<>(
+ new RefreshRemoteApplicationEvent(this, "foo", new PathDestinationFactory().getDestination(null))));
assertThat(this.context.getBean(InboundMessageHandlerConfiguration.class).refresh).isNull();
}
+ @Test
+ public void shutdownInboundFromSelf() {
+ this.context = SpringApplication.run(ShutdownInboundMessageHandlerConfiguration.class,
+ "--spring.cloud.bus.id=foo", "--server.port=0");
+ this.context.getBean(BusConstants.INPUT, MessageChannel.class)
+ .send(new GenericMessage<>(new ShutdownRemoteApplicationEvent(this, "foo",
+ new PathDestinationFactory().getDestination(null))));
+ assertThat(this.context.getBean(ShutdownInboundMessageHandlerConfiguration.class).shutdown).isNull();
+ }
+
@Test
public void inboundNotFromSelf() {
this.context = SpringApplication.run(InboundMessageHandlerConfiguration.class, "--spring.cloud.bus.id=bar",
"--server.port=0");
this.context.getBean(BusConstants.INPUT, MessageChannel.class)
- .send(new GenericMessage<>(new RefreshRemoteApplicationEvent(this, "foo", (String) null)));
+ .send(new GenericMessage<>(
+ new RefreshRemoteApplicationEvent(this, "foo", new PathDestinationFactory().getDestination(null))));
assertThat(this.context.getBean(InboundMessageHandlerConfiguration.class).refresh).isNotNull();
}
+ @Test
+ public void shutdownInboundNotFromSelf() {
+ this.context = SpringApplication.run(ShutdownInboundMessageHandlerConfiguration.class,
+ "--spring.cloud.bus.id=bar", "--server.port=0");
+ this.context.getBean(BusConstants.INPUT, MessageChannel.class)
+ .send(new GenericMessage<>(new ShutdownRemoteApplicationEvent(this, "foo",
+ new PathDestinationFactory().getDestination(null))));
+ assertThat(this.context.getBean(ShutdownInboundMessageHandlerConfiguration.class).shutdown).isNotNull();
+ }
+
@Test
public void inboundNotFromSelfWithAck() throws Exception {
this.context = SpringApplication.run(
@@ -121,7 +154,8 @@ public void inboundNotFromSelfWithTrace() {
SentMessageConfiguration.class },
new String[] { "--spring.cloud.bus.trace.enabled=true", "--spring.cloud.bus.id=bar",
"--server.port=0" });
- this.context.getBean(BusConsumer.class).accept(new RefreshRemoteApplicationEvent(this, "foo", (String) null));
+ this.context.getBean(BusConsumer.class)
+ .accept(new RefreshRemoteApplicationEvent(this, "foo", new PathDestinationFactory().getDestination(null)));
RefreshRemoteApplicationEvent refresh = this.context.getBean(InboundMessageHandlerConfiguration.class).refresh;
assertThat(refresh).isNotNull();
SentMessageConfiguration sent = this.context.getBean(SentMessageConfiguration.class);
@@ -149,7 +183,19 @@ public void inboundAckWithTrace() throws InterruptedException {
public void outboundFromSelf() throws Exception {
this.context = SpringApplication.run(OutboundMessageHandlerConfiguration.class, "--debug=true",
"--spring.cloud.bus.id=foo", "--server.port=0");
- this.context.publishEvent(new RefreshRemoteApplicationEvent(this, "foo", (String) null));
+ this.context.publishEvent(
+ new RefreshRemoteApplicationEvent(this, "foo", new PathDestinationFactory().getDestination(null)));
+ TestStreamBusBridge busBridge = this.context.getBean(TestStreamBusBridge.class);
+ busBridge.latch.await(2, TimeUnit.SECONDS);
+ assertThat(busBridge.message).as("message was null").isNotNull();
+ }
+
+ @Test
+ public void shutdownOutboundFromSelf() throws Exception {
+ this.context = SpringApplication.run(OutboundMessageHandlerConfiguration.class, "--debug=true",
+ "--spring.cloud.bus.id=foo", "--server.port=0");
+ this.context.publishEvent(
+ new ShutdownRemoteApplicationEvent(this, "foo", new PathDestinationFactory().getDestination(null)));
TestStreamBusBridge busBridge = this.context.getBean(TestStreamBusBridge.class);
busBridge.latch.await(2, TimeUnit.SECONDS);
assertThat(busBridge.message).as("message was null").isNotNull();
@@ -159,7 +205,17 @@ public void outboundFromSelf() throws Exception {
public void outboundNotFromSelf() {
this.context = SpringApplication.run(OutboundMessageHandlerConfiguration.class, "--spring.cloud.bus.id=bar",
"--server.port=0");
- this.context.publishEvent(new RefreshRemoteApplicationEvent(this, "foo", (String) null));
+ this.context.publishEvent(
+ new RefreshRemoteApplicationEvent(this, "foo", new PathDestinationFactory().getDestination(null)));
+ assertThat(this.context.getBean(TestStreamBusBridge.class).message).isNull();
+ }
+
+ @Test
+ public void shutdownOutboundNotFromSelf() {
+ this.context = SpringApplication.run(OutboundMessageHandlerConfiguration.class, "--spring.cloud.bus.id=bar",
+ "--server.port=0");
+ this.context.publishEvent(
+ new ShutdownRemoteApplicationEvent(this, "foo", new PathDestinationFactory().getDestination(null)));
assertThat(this.context.getBean(TestStreamBusBridge.class).message).isNull();
}
@@ -168,28 +224,61 @@ public void inboundNotFromSelfPathPattern() {
this.context = SpringApplication.run(InboundMessageHandlerConfiguration.class, "--spring.cloud.bus.id=bar:1000",
"--server.port=0");
this.context.getBean(BusConstants.INPUT, MessageChannel.class)
- .send(new GenericMessage<>(new RefreshRemoteApplicationEvent(this, "foo", "bar:*")));
+ .send(new GenericMessage<>(new RefreshRemoteApplicationEvent(this, "foo",
+ new PathDestinationFactory().getDestination("bar:*"))));
assertThat(this.context.getBean(InboundMessageHandlerConfiguration.class).refresh).isNotNull();
}
+ @Test
+ public void shutdownInboundNotFromSelfPathPattern() {
+ this.context = SpringApplication.run(ShutdownInboundMessageHandlerConfiguration.class,
+ "--spring.cloud.bus.id=bar:1000", "--server.port=0");
+ this.context.getBean(BusConstants.INPUT, MessageChannel.class)
+ .send(new GenericMessage<>(new ShutdownRemoteApplicationEvent(this, "foo",
+ new PathDestinationFactory().getDestination("bar:*"))));
+ assertThat(this.context.getBean(ShutdownInboundMessageHandlerConfiguration.class).shutdown).isNotNull();
+ }
+
@Test
public void inboundNotFromSelfDeepPathPattern() {
this.context = SpringApplication.run(InboundMessageHandlerConfiguration.class,
"--spring.cloud.bus.id=bar:test:1000", "--server.port=0");
this.context.getBean(BusConstants.INPUT, MessageChannel.class)
- .send(new GenericMessage<>(new RefreshRemoteApplicationEvent(this, "foo", "bar:**")));
+ .send(new GenericMessage<>(new RefreshRemoteApplicationEvent(this, "foo",
+ new PathDestinationFactory().getDestination("bar:**"))));
assertThat(this.context.getBean(InboundMessageHandlerConfiguration.class).refresh).isNotNull();
}
+ @Test
+ public void shutdownInboundNotFromSelfDeepPathPattern() {
+ this.context = SpringApplication.run(ShutdownInboundMessageHandlerConfiguration.class,
+ "--spring.cloud.bus.id=bar:test:1000", "--server.port=0");
+ this.context.getBean(BusConstants.INPUT, MessageChannel.class)
+ .send(new GenericMessage<>(new ShutdownRemoteApplicationEvent(this, "foo",
+ new PathDestinationFactory().getDestination("bar:**"))));
+ assertThat(this.context.getBean(ShutdownInboundMessageHandlerConfiguration.class).shutdown).isNotNull();
+ }
+
@Test
public void inboundNotFromSelfFlatPattern() {
this.context = SpringApplication.run(InboundMessageHandlerConfiguration.class, "--spring.cloud.bus.id=bar",
"--server.port=0");
this.context.getBean(BusConstants.INPUT, MessageChannel.class)
- .send(new GenericMessage<>(new RefreshRemoteApplicationEvent(this, "foo", "bar*")));
+ .send(new GenericMessage<>(new RefreshRemoteApplicationEvent(this, "foo",
+ new PathDestinationFactory().getDestination("bar*"))));
assertThat(this.context.getBean(InboundMessageHandlerConfiguration.class).refresh).isNotNull();
}
+ @Test
+ public void shutdownInboundNotFromSelfFlatPattern() {
+ this.context = SpringApplication.run(ShutdownInboundMessageHandlerConfiguration.class,
+ "--spring.cloud.bus.id=bar", "--server.port=0");
+ this.context.getBean(BusConstants.INPUT, MessageChannel.class)
+ .send(new GenericMessage<>(new ShutdownRemoteApplicationEvent(this, "foo",
+ new PathDestinationFactory().getDestination("bar*"))));
+ assertThat(this.context.getBean(ShutdownInboundMessageHandlerConfiguration.class).shutdown).isNotNull();
+ }
+
// see https://github.com/spring-cloud/spring-cloud-bus/issues/74
@Test
public void inboundNotFromSelfUnknown() {
@@ -210,8 +299,6 @@ public void initDoesNotOverrideCustomDestination() {
output.setDestination("mydestination");
properties.put(BusConstants.OUTPUT, output);
- setupBusAutoConfig(properties);
-
BindingProperties inputProps = properties.get(BusConstants.INPUT);
assertThat(inputProps.getDestination()).isEqualTo("mydestination");
@@ -219,15 +306,6 @@ public void initDoesNotOverrideCustomDestination() {
assertThat(outputProps.getDestination()).isEqualTo("mydestination");
}
- private BusProperties setupBusAutoConfig(HashMap properties) {
- BindingServiceProperties serviceProperties = mock(BindingServiceProperties.class);
- when(serviceProperties.getBindings()).thenReturn(properties);
-
- BusProperties bus = new BusProperties();
- BusAutoConfiguration configuration = new BusAutoConfiguration();
- return bus;
- }
-
// see https://github.com/spring-cloud/spring-cloud-bus/issues/101
@Test
public void serviceMatcherIdIsConstantAfterRefresh() {
@@ -251,6 +329,11 @@ protected static class RefreshConfig {
PropertyPlaceholderAutoConfiguration.class })
protected static class OutboundMessageHandlerConfiguration {
+ @Bean
+ ShutdownListener shutdownListener() {
+ return mock(ShutdownListener.class);
+ }
+
@Bean
@Primary
StreamBusBridge testStreamBusBridge(StreamBridge streamBridge, BusProperties properties) {
@@ -294,6 +377,28 @@ public void onApplicationEvent(RefreshRemoteApplicationEvent event) {
}
+ @Configuration(proxyBeanMethods = false)
+ @EnableAutoConfiguration
+ @ImportAutoConfiguration({ BusAutoConfiguration.class, TestChannelBinderConfiguration.class,
+ PropertyPlaceholderAutoConfiguration.class })
+ protected static class ShutdownInboundMessageHandlerConfiguration
+ implements ApplicationListener {
+
+ private ShutdownRemoteApplicationEvent shutdown;
+
+ @Bean
+ // Mock the shutdown listener so we don't try and shutdown the application
+ ShutdownListener customShutdownListener() {
+ return mock(ShutdownListener.class);
+ }
+
+ @Override
+ public void onApplicationEvent(ShutdownRemoteApplicationEvent event) {
+ this.shutdown = event;
+ }
+
+ }
+
@Configuration(proxyBeanMethods = false)
protected static class SentMessageConfiguration implements ApplicationListener {
diff --git a/spring-cloud-bus/src/test/java/org/springframework/cloud/bus/ConditionalOnBusEnabledTests.java b/spring-cloud-bus/src/test/java/org/springframework/cloud/bus/ConditionalOnBusEnabledTests.java
index 84a7926c..1e0d5112 100644
--- a/spring-cloud-bus/src/test/java/org/springframework/cloud/bus/ConditionalOnBusEnabledTests.java
+++ b/spring-cloud-bus/src/test/java/org/springframework/cloud/bus/ConditionalOnBusEnabledTests.java
@@ -33,6 +33,9 @@
*/
public class ConditionalOnBusEnabledTests {
+ /**
+ * {@link ExpectedException} rule for verifying thrown exceptions.
+ */
@Rule
public ExpectedException thrown = ExpectedException.none();
diff --git a/spring-cloud-bus/src/test/java/org/springframework/cloud/bus/PathServiceMatcherTests.java b/spring-cloud-bus/src/test/java/org/springframework/cloud/bus/PathServiceMatcherTests.java
index e7350598..17a8b023 100644
--- a/spring-cloud-bus/src/test/java/org/springframework/cloud/bus/PathServiceMatcherTests.java
+++ b/spring-cloud-bus/src/test/java/org/springframework/cloud/bus/PathServiceMatcherTests.java
@@ -136,7 +136,7 @@ public void notFromSelf() {
}
/**
- * see gh-678
+ * see gh-678.
*/
@Test
public void forSelfWithMultipleProfiles() {
@@ -147,7 +147,7 @@ public void forSelfWithMultipleProfiles() {
}
/**
- * see gh-678
+ * see gh-678.
*/
@Test
public void notForSelfWithMultipleProfiles() {
@@ -158,7 +158,7 @@ public void notForSelfWithMultipleProfiles() {
}
/**
- * see gh-678
+ * see gh-678.
*/
@Test
public void notForSelfWithMultipleProfilesDifferentPort() {
diff --git a/spring-cloud-bus/src/test/java/org/springframework/cloud/bus/jackson/RemoteApplicationEventScanTests.java b/spring-cloud-bus/src/test/java/org/springframework/cloud/bus/jackson/RemoteApplicationEventScanTests.java
index b11cb995..44185253 100644
--- a/spring-cloud-bus/src/test/java/org/springframework/cloud/bus/jackson/RemoteApplicationEventScanTests.java
+++ b/spring-cloud-bus/src/test/java/org/springframework/cloud/bus/jackson/RemoteApplicationEventScanTests.java
@@ -32,6 +32,7 @@
import org.springframework.cloud.bus.event.AckRemoteApplicationEvent;
import org.springframework.cloud.bus.event.EnvironmentChangeRemoteApplicationEvent;
import org.springframework.cloud.bus.event.RefreshRemoteApplicationEvent;
+import org.springframework.cloud.bus.event.ShutdownRemoteApplicationEvent;
import org.springframework.cloud.bus.event.UnknownRemoteApplicationEvent;
import org.springframework.cloud.bus.event.test.TestRemoteApplicationEvent;
import org.springframework.cloud.bus.event.test.TypedRemoteApplicationEvent;
@@ -123,6 +124,7 @@ private void addStandardSpringCloudEventBusEvents(final List> expectedR
expectedRegisterdClassesAsList.add(EnvironmentChangeRemoteApplicationEvent.class);
expectedRegisterdClassesAsList.add(RefreshRemoteApplicationEvent.class);
expectedRegisterdClassesAsList.add(UnknownRemoteApplicationEvent.class);
+ expectedRegisterdClassesAsList.add(ShutdownRemoteApplicationEvent.class);
}
@Configuration(proxyBeanMethods = false)
diff --git a/spring-cloud-bus/src/test/java/org/springframework/cloud/bus/jackson/SubtypeModuleTests.java b/spring-cloud-bus/src/test/java/org/springframework/cloud/bus/jackson/SubtypeModuleTests.java
index 97d77ca6..d255e361 100644
--- a/spring-cloud-bus/src/test/java/org/springframework/cloud/bus/jackson/SubtypeModuleTests.java
+++ b/spring-cloud-bus/src/test/java/org/springframework/cloud/bus/jackson/SubtypeModuleTests.java
@@ -107,7 +107,7 @@ public void testDeserializeJsonTypeWithMessageConverter() throws Exception {
}
/**
- * see https://github.com/spring-cloud/spring-cloud-bus/issues/74
+ * see https://github.com/spring-cloud/spring-cloud-bus/issues/74.
*/
@Test
public void testDeserializeAckRemoteApplicationEventWithKnownType() throws Exception {
@@ -124,7 +124,7 @@ public void testDeserializeAckRemoteApplicationEventWithKnownType() throws Excep
}
/**
- * see https://github.com/spring-cloud/spring-cloud-bus/issues/74
+ * see https://github.com/spring-cloud/spring-cloud-bus/issues/74.
*/
@Test
public void testDeserializeAckRemoteApplicationEventWithUnknownType() throws Exception {