Skip to content

Commit ee348f4

Browse files
committed
add tests
Signed-off-by: wind57 <[email protected]>
1 parent c7a7328 commit ee348f4

File tree

17 files changed

+504
-61
lines changed

17 files changed

+504
-61
lines changed

docs/modules/ROOT/pages/leader-election.adoc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,8 @@ You might have to give proper RBAC to be able to use this functionality, for exa
107107
[source]
108108
----
109109
- apiGroups: [ "coordination.k8s.io" ]
110-
resources: [ "leases" ]
111-
verbs: [ "get", "update", "create"]
110+
resources: [ "leases", "configmaps" ]
111+
verbs: [ "get", "update", "create", "patch"]
112112
----
113113

114114

spring-cloud-kubernetes-fabric8-leader/src/main/java/org/springframework/cloud/kubernetes/fabric8/leader/election/Fabric8LeaderElectionAutoConfiguration.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,8 @@ Lock lock(KubernetesClient fabric8KubernetesClient, LeaderElectionProperties pro
109109

110110
if (leaseSupported) {
111111
if (properties.useConfigMapAsLock()) {
112-
LOG.info(() -> "leases are supported on the cluster, but config map will be used " +
113-
"(because 'spring.cloud.kubernetes.leader.election.use-config-map-as-lock=true')");
112+
LOG.info(() -> "leases are supported on the cluster, but config map will be used "
113+
+ "(because 'spring.cloud.kubernetes.leader.election.use-config-map-as-lock=true')");
114114
return new ConfigMapLock(properties.lockNamespace(), properties.lockName(), holderIdentity);
115115
}
116116
else {

spring-cloud-kubernetes-fabric8-leader/src/main/java/org/springframework/cloud/kubernetes/fabric8/leader/election/Fabric8LeaderElectionInitiator.java

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,7 @@ final class Fabric8LeaderElectionInitiator {
6161

6262
private final AtomicReference<CompletableFuture<?>> leaderFutureReference = new AtomicReference<>();
6363

64-
// not private for testing
65-
volatile boolean destroyCalled = false;
64+
private volatile boolean destroyCalled = false;
6665

6766
Fabric8LeaderElectionInitiator(String holderIdentity, String podNamespace, KubernetesClient fabric8KubernetesClient,
6867
LeaderElectionConfig leaderElectionConfig, LeaderElectionProperties leaderElectionProperties) {
@@ -74,19 +73,19 @@ final class Fabric8LeaderElectionInitiator {
7473
}
7574

7675
/**
77-
* in a CachedSingleThreadScheduler start pod readiness and keep it running 'forever',
78-
* until it is successful or failed. That is run in a daemon thread.
76+
* in a CachedSingleThreadScheduler start pod readiness and keep it running 'forever',
77+
* until it is successful or failed. That is run in a daemon thread.
7978
*
80-
* In a different pool ('executorService'), block until the above CompletableFuture is done.
81-
* Only when it's done, start the leader election process.
82-
* If pod readiness fails, leader election is not started.
79+
* In a different pool ('executorService'), block until the above CompletableFuture is
80+
* done. Only when it's done, start the leader election process. If pod readiness
81+
* fails, leader election is not started.
8382
*
84-
*/
83+
*/
8584
@PostConstruct
8685
void postConstruct() {
8786
LOG.info(() -> "starting leader initiator : " + holderIdentity);
88-
executorService.set(Executors.newSingleThreadExecutor(
89-
r -> new Thread(r, "Fabric8LeaderElectionInitiator-" + holderIdentity)));
87+
executorService.set(Executors
88+
.newSingleThreadExecutor(r -> new Thread(r, "Fabric8LeaderElectionInitiator-" + holderIdentity)));
9089
CompletableFuture<Void> podReadyFuture = new CompletableFuture<>();
9190

9291
// wait until pod is ready
@@ -104,8 +103,8 @@ void postConstruct() {
104103
podReadyFuture.complete(null);
105104
}
106105
else {
107-
LOG.info(() -> "Pod : " + holderIdentity + " in namespace : " + podNamespace + " is not ready, "
108-
+ "will retry in one second");
106+
LOG.debug(() -> "Pod : " + holderIdentity + " in namespace : " + podNamespace
107+
+ " is not ready, " + "will retry in one second");
109108
}
110109
}
111110
catch (Exception e) {
@@ -121,19 +120,18 @@ void postConstruct() {
121120
// and in the same thread start the leader election
122121
executorService.get().submit(() -> {
123122
if (leaderElectionProperties.waitForPodReady()) {
124-
CompletableFuture<?> ready = podReadyFuture
125-
.whenComplete((ok, error) -> {
126-
if (error != null) {
127-
LOG.error(() -> "readiness failed for : " + holderIdentity);
128-
LOG.error(() -> "leader election for : " + holderIdentity + " will not start");
129-
}
130-
else {
131-
LOG.info(() -> holderIdentity + " is ready");
132-
}
133-
// we cancel the future that checks readiness of the pod
134-
// and thus also close the pool that was running it.
135-
podReadyTask.get().cancel(true);
136-
});
123+
CompletableFuture<?> ready = podReadyFuture.whenComplete((ok, error) -> {
124+
if (error != null) {
125+
LOG.error(() -> "readiness failed for : " + holderIdentity);
126+
LOG.error(() -> "leader election for : " + holderIdentity + " will not start");
127+
}
128+
else {
129+
LOG.info(() -> holderIdentity + " is ready");
130+
}
131+
// we cancel the future that checks readiness of the pod
132+
// and thus also close the pool that was running it.
133+
podReadyTask.get().cancel(true);
134+
});
137135
try {
138136
ready.get();
139137
}
@@ -195,8 +193,8 @@ private void startLeaderElection() {
195193

196194
if (error instanceof CancellationException) {
197195
if (!destroyCalled) {
198-
LOG.warn(() -> "renewal failed for : " + holderIdentity + ", will re-start it after : " +
199-
leaderElectionProperties.waitAfterRenewalFailure().toSeconds() + " seconds");
196+
LOG.warn(() -> "renewal failed for : " + holderIdentity + ", will re-start it after : "
197+
+ leaderElectionProperties.waitAfterRenewalFailure().toSeconds() + " seconds");
200198
try {
201199
TimeUnit.SECONDS.sleep(leaderElectionProperties.waitAfterRenewalFailure().toSeconds());
202200
}

spring-cloud-kubernetes-fabric8-leader/src/test/java/org/springframework/cloud/kubernetes/fabric8/leader/election/Fabric8LeaderElectionConcurrentITTest.java

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@
4545
class Fabric8LeaderElectionConcurrentITTest {
4646

4747
private static final LeaderElectionProperties PROPERTIES = new LeaderElectionProperties(false, false,
48-
Duration.ofSeconds(15), "default", "lease-lock", Duration.ofSeconds(5), Duration.ofSeconds(2),
49-
Duration.ofSeconds(5), false);
48+
Duration.ofSeconds(15), "default", "lease-lock", Duration.ofSeconds(5), Duration.ofSeconds(2),
49+
Duration.ofSeconds(5), false);
5050

5151
private static K3sContainer container;
5252

@@ -113,42 +113,37 @@ void test(CapturedOutput output) {
113113
failLeaderRenewal(leader, one, two);
114114

115115
/*
116-
* we simulated above that renewal failed and leader future was canceled.
117-
* In this case, the 'notLeader' picks up the leadership, the 'leader'
118-
* is now a "follower", it re-tries to take leadership.
116+
* we simulated above that renewal failed and leader future was canceled. In this
117+
* case, the 'notLeader' picks up the leadership, the 'leader' is now a
118+
* "follower", it re-tries to take leadership.
119119
*/
120-
awaitForMessageFromPosition(output, beforeRelease,
121-
"id : " + follower + " is the new leader");
120+
awaitForMessageFromPosition(output, beforeRelease, "id : " + follower + " is the new leader");
122121
awaitForMessageFromPosition(output, beforeRelease,
123122
"Attempting to renew leader lease 'LeaseLock: " + "default - lease-lock (" + follower + ")'...");
124123
awaitForMessageFromPosition(output, beforeRelease,
125-
"Acquired lease 'LeaseLock: default - lease-lock (" + follower + ")'");
124+
"Acquired lease 'LeaseLock: default - lease-lock (" + follower + ")'");
126125

127126
// proves that the canceled leader tries to acquire again the leadership
128127
awaitForMessageFromPosition(output, beforeRelease,
129-
"Attempting to acquire leader lease 'LeaseLock: default - lease-lock (" + leader + ")'...");
130-
awaitForMessageFromPosition(output, beforeRelease,
131-
"Lock is held by " + follower + " and has not yet expired");
128+
"Attempting to acquire leader lease 'LeaseLock: default - lease-lock (" + leader + ")'...");
129+
awaitForMessageFromPosition(output, beforeRelease, "Lock is held by " + follower + " and has not yet expired");
132130

133131
/*
134-
* we simulate the renewal failure one more time.
135-
* we know that leader = 'follower'
132+
* we simulate the renewal failure one more time. we know that leader = 'follower'
136133
*/
137134
beforeRelease = output.getOut().length();
138135
failLeaderRenewal(follower, one, two);
139136

137+
awaitForMessageFromPosition(output, beforeRelease, "id : " + leader + " is the new leader");
140138
awaitForMessageFromPosition(output, beforeRelease,
141-
"id : " + leader + " is the new leader");
142-
awaitForMessageFromPosition(output, beforeRelease,
143-
"Attempting to renew leader lease 'LeaseLock: " + "default - lease-lock (" + leader + ")'...");
139+
"Attempting to renew leader lease 'LeaseLock: " + "default - lease-lock (" + leader + ")'...");
144140
awaitForMessageFromPosition(output, beforeRelease,
145-
"Acquired lease 'LeaseLock: default - lease-lock (" + leader + ")'");
141+
"Acquired lease 'LeaseLock: default - lease-lock (" + leader + ")'");
146142

147143
// proves that the canceled leader tries to acquire again the leadership
148144
awaitForMessageFromPosition(output, beforeRelease,
149-
"Attempting to acquire leader lease 'LeaseLock: default - lease-lock (" + follower + ")'...");
150-
awaitForMessageFromPosition(output, beforeRelease, "Lock is held by " + leader +
151-
" and has not yet expired");
145+
"Attempting to acquire leader lease 'LeaseLock: default - lease-lock (" + follower + ")'...");
146+
awaitForMessageFromPosition(output, beforeRelease, "Lock is held by " + leader + " and has not yet expired");
152147

153148
}
154149

@@ -208,10 +203,12 @@ private void awaitForMessageFromPosition(CapturedOutput output, int from, String
208203
.until(() -> output.getOut().substring(from).contains(message));
209204
}
210205

211-
private LeaderAndFollower leaderAndFollower(
212-
LeaderElectionConfig leaderElectionConfig, KubernetesClient kubernetesClient) {
206+
private LeaderAndFollower leaderAndFollower(LeaderElectionConfig leaderElectionConfig,
207+
KubernetesClient kubernetesClient) {
213208
boolean oneIsLeader = leaderElectionConfig.getLock()
214-
.get(kubernetesClient).getHolderIdentity().equals(HOLDER_IDENTITY_ONE);
209+
.get(kubernetesClient)
210+
.getHolderIdentity()
211+
.equals(HOLDER_IDENTITY_ONE);
215212

216213
if (oneIsLeader) {
217214
return new LeaderAndFollower("one", "two");

spring-cloud-kubernetes-integration-tests/pom.xml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,5 +108,8 @@
108108
<!-- config watcher reload, using rabbitmq and secret -->
109109
<module>spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload</module>
110110

111-
</modules>
111+
<!-- fabric8 leader election -->
112+
<module>spring-cloud-kubernetes-fabric8-client-leader-election</module>
113+
114+
</modules>
112115
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
<parent>
7+
<groupId>org.springframework.cloud</groupId>
8+
<artifactId>spring-cloud-kubernetes-fabric8-client-leader-election</artifactId>
9+
<version>3.3.1-SNAPSHOT</version>
10+
</parent>
11+
12+
<artifactId>fabric8-leader-app</artifactId>
13+
14+
<dependencies>
15+
<dependency>
16+
<groupId>org.springframework.boot</groupId>
17+
<artifactId>spring-boot-starter-web</artifactId>
18+
</dependency>
19+
<dependency>
20+
<groupId>org.springframework.cloud</groupId>
21+
<artifactId>spring-cloud-kubernetes-fabric8-leader</artifactId>
22+
</dependency>
23+
<dependency>
24+
<groupId>org.springframework.boot</groupId>
25+
<artifactId>spring-boot-starter-actuator</artifactId>
26+
</dependency>
27+
</dependencies>
28+
29+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2013-2025 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.kubernetes.fabric8.client.leader.election;
18+
19+
import org.springframework.boot.SpringApplication;
20+
import org.springframework.boot.autoconfigure.SpringBootApplication;
21+
22+
/**
23+
* @author wind57
24+
*/
25+
@SpringBootApplication
26+
public class AppA {
27+
28+
public static void main(String[] args) {
29+
SpringApplication.run(AppA.class, args);
30+
}
31+
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
logging:
2+
level:
3+
org:
4+
springframework:
5+
cloud:
6+
kubernetes:
7+
fabric8:
8+
leader: DEBUG
9+
10+
spring:
11+
application:
12+
name: fabric8-leader-app-a
13+
cloud:
14+
kubernetes:
15+
leader:
16+
election:
17+
enabled: true
18+
management:
19+
endpoint:
20+
info:
21+
access: unrestricted
22+
health:
23+
probes:
24+
enabled: true
25+
endpoints:
26+
web:
27+
exposure:
28+
include: health, info
29+
server:
30+
port: 8080
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
<parent>
7+
<groupId>org.springframework.cloud</groupId>
8+
<artifactId>spring-cloud-kubernetes-fabric8-client-leader-election</artifactId>
9+
<version>3.3.1-SNAPSHOT</version>
10+
</parent>
11+
12+
<artifactId>fabric8-leader-test</artifactId>
13+
14+
<properties>
15+
<spring-boot.repackage.skip>true</spring-boot.repackage.skip>
16+
<spring-boot.build-image.skip>true</spring-boot.build-image.skip>
17+
</properties>
18+
19+
<dependencies>
20+
<dependency>
21+
<groupId>org.springframework.boot</groupId>
22+
<artifactId>spring-boot-starter-test</artifactId>
23+
</dependency>
24+
<dependency>
25+
<groupId>org.springframework.boot</groupId>
26+
<artifactId>spring-boot-starter-webflux</artifactId>
27+
</dependency>
28+
<dependency>
29+
<groupId>org.springframework.cloud</groupId>
30+
<artifactId>spring-cloud-kubernetes-test-support</artifactId>
31+
</dependency>
32+
33+
</dependencies>
34+
35+
<build>
36+
<resources>
37+
<resource>
38+
<directory>../../src/main/resources</directory>
39+
<filtering>true</filtering>
40+
</resource>
41+
</resources>
42+
</build>
43+
44+
</project>

0 commit comments

Comments
 (0)