Skip to content

Commit a1cffbc

Browse files
authored
Merge pull request #277 from ryanjbaxter/add-shutdow-event
Add a shutdown event, endpoint, and listener
2 parents 78ae184 + 815b5db commit a1cffbc

File tree

18 files changed

+493
-49
lines changed

18 files changed

+493
-49
lines changed

.github/workflows/maven.yaml

+7-7
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,21 @@
44
name: Build
55
on:
66
push:
7-
branches: [ main, 3.1.x ]
7+
branches: [ main, 4.1.x, 4.0.x, 3.1.x ]
88
pull_request:
9-
branches: [ main, 3.1.x ]
9+
branches: [ main, 4.1.x, 4.0.x, 3.1.x ]
1010
jobs:
1111
build:
1212
runs-on: ubuntu-latest
1313
steps:
14-
- uses: actions/checkout@v2
14+
- uses: actions/checkout@v4
1515
- name: Set up JDK
16-
uses: actions/setup-java@v2
16+
uses: actions/setup-java@v4
1717
with:
1818
distribution: 'temurin'
1919
java-version: '17'
2020
- name: Cache local Maven repository
21-
uses: actions/cache@v2
21+
uses: actions/cache@v4
2222
with:
2323
path: ~/.m2/repository
2424
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
@@ -27,12 +27,12 @@ jobs:
2727
- name: Build with Maven
2828
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
2929
- name: Publish Test Report
30-
uses: mikepenz/action-junit-report@v2
30+
uses: mikepenz/action-junit-report@v5
3131
if: always() # always run even if the previous step fails
3232
with:
3333
report_paths: '**/surefire-reports/TEST-*.xml'
3434
- name: Archive code coverage results
35-
uses: actions/upload-artifact@v2
35+
uses: actions/upload-artifact@v4
3636
with:
3737
name: surefire-reports
3838
path: '**/surefire-reports/*'

docs/modules/ROOT/pages/quickstart.adoc

+4-4
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ spring:
2020
----
2121

2222
The bus currently supports sending messages to all nodes listening or all nodes for a
23-
particular service (as defined by Eureka). The `/bus/*` actuator namespace has some HTTP
24-
endpoints. Currently, two are implemented. The first, `/bus/env`, sends key/value pairs to
25-
update each node's Spring Environment. The second, `/bus/refresh`, reloads each
23+
particular service (as defined by Eureka). The `/bus*` actuator namespace has some HTTP
24+
endpoints. Currently, three are implemented. The first, `/busenv`, sends key/value pairs to
25+
update each node's Spring Environment. The second, `/busrefresh`, reloads each
2626
application's configuration, as though they had all been pinged on their `/refresh`
27-
endpoint.
27+
endpoint. The third `/busshutdown` sends a shutdown event to gracefully shutdown the application instance(s).
2828

2929
NOTE: The Spring Cloud Bus starters cover Rabbit and Kafka, because those are the two most
3030
common implementations. However, Spring Cloud Stream is quite flexible, and the binder

docs/modules/ROOT/pages/spring-cloud-bus/bus-endpoints.adoc

+32-3
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
= Bus Endpoints
33
:page-section-summary-toc: 1
44

5-
Spring Cloud Bus provides two endpoints, `/actuator/busrefresh` and `/actuator/busenv`
5+
Spring Cloud Bus provides three endpoints, `/actuator/busrefresh`, `/actutator/busshutdown` and `/actuator/busenv`
66
that correspond to individual actuator endpoints in Spring Cloud Commons,
7-
`/actuator/refresh` and `/actuator/env` respectively.
7+
`/actuator/refresh`, `/actuator/shutdown`, and `/actuator/env` respectively.
88

99
[[bus-refresh-endpoint]]
1010
== Bus Refresh Endpoint
@@ -41,4 +41,33 @@ The `/actuator/busenv` endpoint accepts `POST` requests with the following shape
4141
"name": "key1",
4242
"value": "value1"
4343
}
44-
----
44+
----
45+
46+
[[bus-shutdown-endpoint]]
47+
== Bus Shutdown Endpoint
48+
The `/actuator/busshutdown` shuts down the application https://docs.spring.io/spring-boot/reference/web/graceful-shutdown.html[gracefully].
49+
50+
To expose the `/actuator/busshutdown` endpoint, you need to add following configuration to your
51+
application:
52+
53+
[source,properties]
54+
----
55+
management.endpoints.web.exposure.include=busshutdown
56+
----
57+
58+
You can make a request to the `busshutdown` endpoint by issuing a `POST` request.
59+
60+
If you would like to target a specific application you can issue a `POST` request to `/busshutdown` and optionally
61+
specify the bus id:
62+
63+
[source,bash]
64+
----
65+
$ curl -X POST http://localhost:8080/actuator/busshutdown
66+
----
67+
68+
You can also target a specific application instance by specifying the bus id:
69+
70+
[source,bash]
71+
----
72+
$ curl -X POST http://localhost:8080/actuator/busshutdown/busid:123
73+
----

docs/modules/ROOT/partials/_configprops.adoc

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
|spring.cloud.bus.env.enabled | `+++true+++` | Flag to switch off environment change events (default on).
1010
|spring.cloud.bus.id | `+++application+++` | The identifier for this application instance.
1111
|spring.cloud.bus.refresh.enabled | `+++true+++` | Flag to switch off refresh events (default on).
12+
|spring.cloud.bus.shutdown.enabled | `+++true+++` | Flag to switch off shutdown events (default on).
1213
|spring.cloud.bus.trace.enabled | `+++false+++` | Flag to switch on tracing of acks (default off).
1314

1415
|===

spring-cloud-bus-rsocket/src/main/java/org/springframework/cloud/bus/rsocket/BusRSocketAutoConfiguration.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.springframework.cloud.bus.BusAutoConfiguration;
2727
import org.springframework.cloud.bus.BusProperties;
2828
import org.springframework.cloud.bus.BusRefreshAutoConfiguration;
29+
import org.springframework.cloud.bus.BusShutdownAutoConfiguration;
2930
import org.springframework.cloud.bus.ConditionalOnBusEnabled;
3031
import org.springframework.cloud.bus.PathServiceMatcherAutoConfiguration;
3132
import org.springframework.context.annotation.Bean;
@@ -39,7 +40,7 @@
3940
@EnableConfigurationProperties(BusRSocketProperties.class)
4041
@ConditionalOnClass({ RSocket.class, RoutingRSocketRequester.class })
4142
@AutoConfigureBefore({ BusAutoConfiguration.class, BusRefreshAutoConfiguration.class,
42-
PathServiceMatcherAutoConfiguration.class })
43+
PathServiceMatcherAutoConfiguration.class, BusShutdownAutoConfiguration.class })
4344
public class BusRSocketAutoConfiguration {
4445

4546
@Bean

spring-cloud-bus-tests/pom.xml

+5-6
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,6 @@
1717
<relativePath>..</relativePath> <!-- lookup parent from repository -->
1818
</parent>
1919

20-
<properties>
21-
<testcontainers.version>1.17.6</testcontainers.version>
22-
</properties>
23-
2420
<dependencies>
2521
<dependency>
2622
<groupId>org.springframework.boot</groupId>
@@ -47,16 +43,19 @@
4743
<artifactId>spring-boot-starter-test</artifactId>
4844
<scope>test</scope>
4945
</dependency>
46+
<dependency>
47+
<groupId>org.springframework.boot</groupId>
48+
<artifactId>spring-boot-testcontainers</artifactId>
49+
<scope>test</scope>
50+
</dependency>
5051
<dependency>
5152
<groupId>org.testcontainers</groupId>
5253
<artifactId>junit-jupiter</artifactId>
53-
<version>${testcontainers.version}</version>
5454
<scope>test</scope>
5555
</dependency>
5656
<dependency>
5757
<groupId>org.testcontainers</groupId>
5858
<artifactId>rabbitmq</artifactId>
59-
<version>${testcontainers.version}</version>
6059
<scope>test</scope>
6160
</dependency>
6261
</dependencies>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Copyright 2012-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.bus;
18+
19+
import java.util.concurrent.CountDownLatch;
20+
21+
import org.junit.jupiter.api.AfterAll;
22+
import org.junit.jupiter.api.BeforeAll;
23+
import org.junit.jupiter.api.Test;
24+
import org.testcontainers.containers.RabbitMQContainer;
25+
import org.testcontainers.junit.jupiter.Container;
26+
import org.testcontainers.junit.jupiter.Testcontainers;
27+
28+
import org.springframework.beans.factory.annotation.Autowired;
29+
import org.springframework.boot.SpringBootConfiguration;
30+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
31+
import org.springframework.boot.builder.SpringApplicationBuilder;
32+
import org.springframework.boot.test.context.SpringBootTest;
33+
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
34+
import org.springframework.cloud.bus.event.EnvironmentChangeRemoteApplicationEvent;
35+
import org.springframework.context.ApplicationListener;
36+
import org.springframework.context.ConfigurableApplicationContext;
37+
import org.springframework.test.web.reactive.server.WebTestClient;
38+
39+
import static org.assertj.core.api.Assertions.assertThat;
40+
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
41+
42+
/**
43+
* @author Ryan Baxter
44+
*/
45+
@SpringBootTest(webEnvironment = RANDOM_PORT,
46+
properties = { "management.endpoints.web.exposure.include=*",
47+
"spring.cloud.stream.bindings.springCloudBusOutput.producer.errorChannelEnabled=true",
48+
"logging.level.org.springframework.cloud.bus=TRACE", "spring.cloud.bus.id=app:1" })
49+
@Testcontainers
50+
public class ShutdownListenerIntegrationTests {
51+
52+
private static ConfigurableApplicationContext context;
53+
54+
@Container
55+
@ServiceConnection
56+
private static final RabbitMQContainer rabbitMQContainer = new RabbitMQContainer("rabbitmq:4.0-management");
57+
58+
@BeforeAll
59+
static void before() {
60+
context = new SpringApplicationBuilder(TestConfig.class)
61+
.properties("server.port=0", "spring.rabbitmq.host=" + rabbitMQContainer.getHost(),
62+
"spring.rabbitmq.port=" + rabbitMQContainer.getAmqpPort(),
63+
"management.endpoints.web.exposure.include=*", "spring.cloud.bus.id=app:2", "debug=true")
64+
.run();
65+
}
66+
67+
@AfterAll
68+
static void after() {
69+
if (context != null) {
70+
context.close();
71+
}
72+
}
73+
74+
@Test
75+
void testShutdown(@Autowired WebTestClient client) {
76+
assertThat(rabbitMQContainer.isRunning());
77+
client.post().uri("/actuator/busshutdown/app:2").exchange().expectStatus().is2xxSuccessful();
78+
assertThat(context.isClosed());
79+
}
80+
81+
@SpringBootConfiguration
82+
@EnableAutoConfiguration
83+
static class TestConfig implements ApplicationListener<EnvironmentChangeRemoteApplicationEvent> {
84+
85+
CountDownLatch latch = new CountDownLatch(1);
86+
87+
@Override
88+
public void onApplicationEvent(EnvironmentChangeRemoteApplicationEvent event) {
89+
latch.countDown();
90+
}
91+
92+
}
93+
94+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2012-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.bus;
18+
19+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
20+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
21+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
22+
import org.springframework.cloud.bus.endpoint.ShutdownBusEndpoint;
23+
import org.springframework.cloud.bus.event.Destination;
24+
import org.springframework.cloud.bus.event.ShutdownListener;
25+
import org.springframework.context.ApplicationEventPublisher;
26+
import org.springframework.context.annotation.Bean;
27+
import org.springframework.context.annotation.Configuration;
28+
29+
/**
30+
* @author Ryan Baxter
31+
*/
32+
@Configuration(proxyBeanMethods = false)
33+
@ConditionalOnBusEnabled
34+
public class BusShutdownAutoConfiguration {
35+
36+
@Bean
37+
@ConditionalOnProperty(value = "spring.cloud.bus.shutdown.enabled", matchIfMissing = true)
38+
@ConditionalOnMissingBean
39+
public ShutdownListener shutdownListener(ServiceMatcher serviceMatcher) {
40+
return new ShutdownListener(serviceMatcher);
41+
}
42+
43+
@Configuration(proxyBeanMethods = false)
44+
@ConditionalOnClass(name = { "org.springframework.boot.actuate.endpoint.annotation.Endpoint" })
45+
protected static class BusShutdownEndpointConfiguration {
46+
47+
@Bean
48+
public ShutdownBusEndpoint shutdownBusEndpoint(ApplicationEventPublisher publisher, BusProperties bus,
49+
Destination.Factory destinationFactory) {
50+
return new ShutdownBusEndpoint(publisher, bus.getId(), destinationFactory);
51+
}
52+
53+
}
54+
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2012-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.bus.endpoint;
18+
19+
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
20+
import org.springframework.boot.actuate.endpoint.annotation.Selector;
21+
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
22+
import org.springframework.cloud.bus.event.Destination;
23+
import org.springframework.cloud.bus.event.ShutdownRemoteApplicationEvent;
24+
import org.springframework.context.ApplicationEventPublisher;
25+
import org.springframework.util.StringUtils;
26+
27+
/**
28+
* @author Ryan Baxter
29+
*/
30+
@Endpoint(id = "busshutdown")
31+
public class ShutdownBusEndpoint extends AbstractBusEndpoint {
32+
33+
public ShutdownBusEndpoint(ApplicationEventPublisher publisher, String id, Destination.Factory destinationFactory) {
34+
super(publisher, id, destinationFactory);
35+
}
36+
37+
@WriteOperation
38+
public void busShutdownWithDestination(@Selector(match = Selector.Match.ALL_REMAINING) String[] destinations) {
39+
String destination = StringUtils.arrayToDelimitedString(destinations, ":");
40+
publish(new ShutdownRemoteApplicationEvent(this, getInstanceId(), getDestination(destination)));
41+
}
42+
43+
@WriteOperation
44+
public void busShutdown() {
45+
publish(new ShutdownRemoteApplicationEvent(this, getInstanceId(), getDestination(null)));
46+
}
47+
48+
}

0 commit comments

Comments
 (0)