Skip to content

Commit 38452e4

Browse files
committed
Fix Reminding Notifier
1 parent 3cc312b commit 38452e4

File tree

5 files changed

+129
-40
lines changed

5 files changed

+129
-40
lines changed

spring-boot-admin-samples/spring-boot-admin-sample-reactive/src/main/java/de/codecentric/boot/admin/SpringBootAdminApplication.java

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
import org.springframework.context.annotation.Configuration;
3131
import org.springframework.context.annotation.Primary;
3232
import org.springframework.context.annotation.Profile;
33-
import org.springframework.scheduling.annotation.Scheduled;
3433
import org.springframework.security.config.web.server.ServerHttpSecurity;
3534
import org.springframework.security.web.server.SecurityWebFilterChain;
3635

@@ -81,19 +80,15 @@ public NotifierConfig(InstanceRepository repository) {
8180
this.repository = repository;
8281
}
8382

84-
@Bean
8583
@Primary
84+
@Bean(initMethod = "start", destroyMethod = "stop")
8685
public RemindingNotifier remindingNotifier() {
8786
RemindingNotifier notifier = new RemindingNotifier(filteringNotifier(), repository);
8887
notifier.setReminderPeriod(Duration.ofMinutes(10));
88+
notifier.setCheckReminderInverval(Duration.ofSeconds(10));
8989
return notifier;
9090
}
9191

92-
@Scheduled(fixedRate = 1_000L)
93-
public void remind() {
94-
remindingNotifier().sendReminders();
95-
}
96-
9792
@Bean
9893
public FilteringNotifier filteringNotifier() {
9994
return new FilteringNotifier(loggerNotifier(), repository);

spring-boot-admin-samples/spring-boot-admin-sample-servlet/src/main/java/de/codecentric/boot/admin/SpringBootAdminApplication.java

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
import org.springframework.context.annotation.Primary;
3535
import org.springframework.context.annotation.Profile;
3636
import org.springframework.http.HttpMethod;
37-
import org.springframework.scheduling.annotation.Scheduled;
3837
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
3938
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
4039
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
@@ -108,19 +107,15 @@ public NotifierConfig(InstanceRepository repository) {
108107
this.repository = repository;
109108
}
110109

111-
@Bean
112110
@Primary
111+
@Bean(initMethod = "start", destroyMethod = "stop")
113112
public RemindingNotifier remindingNotifier() {
114113
RemindingNotifier notifier = new RemindingNotifier(filteringNotifier(), repository);
115114
notifier.setReminderPeriod(Duration.ofMinutes(10));
115+
notifier.setCheckReminderInverval(Duration.ofSeconds(10));
116116
return notifier;
117117
}
118118

119-
@Scheduled(fixedRate = 1_000L)
120-
public void remind() {
121-
remindingNotifier().sendReminders();
122-
}
123-
124119
@Bean
125120
public FilteringNotifier filteringNotifier() {
126121
return new FilteringNotifier(loggerNotifier(), repository);

spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/notify/RemindingNotifier.java

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,19 @@
2222
import de.codecentric.boot.admin.server.domain.events.InstanceEvent;
2323
import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent;
2424
import de.codecentric.boot.admin.server.domain.values.InstanceId;
25+
import reactor.core.Disposable;
26+
import reactor.core.publisher.Flux;
2527
import reactor.core.publisher.Mono;
28+
import reactor.core.scheduler.Schedulers;
29+
import reactor.retry.Retry;
2630

2731
import java.time.Duration;
2832
import java.time.Instant;
29-
import java.util.ArrayList;
3033
import java.util.Arrays;
3134
import java.util.concurrent.ConcurrentHashMap;
35+
import java.util.logging.Level;
36+
import org.slf4j.Logger;
37+
import org.slf4j.LoggerFactory;
3238
import org.springframework.util.Assert;
3339

3440
/**
@@ -37,10 +43,13 @@
3743
* @author Johannes Edmeier
3844
*/
3945
public class RemindingNotifier extends AbstractEventNotifier {
46+
private static final Logger log = LoggerFactory.getLogger(RemindingNotifier.class);
4047
private final ConcurrentHashMap<InstanceId, Reminder> reminders = new ConcurrentHashMap<>();
48+
private final Notifier delegate;
49+
private Duration checkReminderInverval = Duration.ofSeconds(10);
4150
private Duration reminderPeriod = Duration.ofMinutes(10);
4251
private String[] reminderStatuses = {"DOWN", "OFFLINE"};
43-
private final Notifier delegate;
52+
private Disposable subscription;
4453

4554
public RemindingNotifier(Notifier delegate, InstanceRepository repository) {
4655
super(repository);
@@ -50,7 +59,7 @@ public RemindingNotifier(Notifier delegate, InstanceRepository repository) {
5059

5160
@Override
5261
public Mono<Void> doNotify(InstanceEvent event, Instance instance) {
53-
return delegate.notify(event).then(Mono.fromRunnable(() -> {
62+
return delegate.notify(event).onErrorResume(error -> Mono.empty()).then(Mono.fromRunnable(() -> {
5463
if (shouldEndReminder(event)) {
5564
reminders.remove(event.getInstance());
5665
} else if (shouldStartReminder(event)) {
@@ -59,16 +68,37 @@ public Mono<Void> doNotify(InstanceEvent event, Instance instance) {
5968
}));
6069
}
6170

62-
public void sendReminders() {
63-
Instant now = Instant.now();
64-
for (Reminder reminder : new ArrayList<>(reminders.values())) {
65-
if (reminder.getLastNotification().plus(reminderPeriod).isBefore(now)) {
66-
reminder.setLastNotification(now);
67-
delegate.notify(reminder.getEvent());
68-
}
71+
72+
public void start() {
73+
this.subscription = Flux.interval(this.checkReminderInverval, Schedulers.newSingle("reminders"))
74+
.log(log.getName(), Level.FINEST)
75+
.doOnSubscribe(subscription -> log.debug("Started reminders"))
76+
.flatMap(i -> this.sendReminders())
77+
.retryWhen(Retry.any()
78+
.retryMax(Integer.MAX_VALUE)
79+
.doOnRetry(
80+
ctx -> log.error("Resubscribing for reminders after uncaught error",
81+
ctx.exception())))
82+
.subscribe();
83+
}
84+
85+
public void stop() {
86+
if (this.subscription != null && !this.subscription.isDisposed()) {
87+
log.debug("stopped reminders");
88+
this.subscription.dispose();
6989
}
7090
}
7191

92+
protected Mono<Void> sendReminders() {
93+
Instant now = Instant.now();
94+
95+
return Flux.fromIterable(this.reminders.values())
96+
.filter(reminder -> reminder.getLastNotification().plus(reminderPeriod).isBefore(now))
97+
.flatMap(reminder -> delegate.notify(reminder.getEvent())
98+
.doOnSuccess(signal -> reminder.setLastNotification(now)))
99+
.then();
100+
}
101+
72102
protected boolean shouldStartReminder(InstanceEvent event) {
73103
if (event instanceof InstanceStatusChangedEvent) {
74104
return Arrays.binarySearch(reminderStatuses,
@@ -98,6 +128,10 @@ public void setReminderStatuses(String[] reminderStatuses) {
98128
this.reminderStatuses = copy;
99129
}
100130

131+
public void setCheckReminderInverval(Duration checkReminderInverval) {
132+
this.checkReminderInverval = checkReminderInverval;
133+
}
134+
101135
private static class Reminder {
102136
private final InstanceEvent event;
103137
private Instant lastNotification;

spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/config/AdminServerNotifierAutoConfigurationTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public void test_notifierListener() throws InterruptedException {
7373
.expectNext(APP_DOWN)
7474
.thenCancel()
7575
.verify();
76-
Thread.sleep(50); //wait for the notifications in different thread
76+
Thread.sleep(100); //wait for the notifications in different thread
7777
assertThat(context.getBean(TestNotifier.class).getEvents()).containsOnly(APP_DOWN);
7878
}
7979

spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/notify/RemindingNotifierTest.java

Lines changed: 80 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,26 @@
1818

1919
import de.codecentric.boot.admin.server.domain.entities.Instance;
2020
import de.codecentric.boot.admin.server.domain.entities.InstanceRepository;
21+
import de.codecentric.boot.admin.server.domain.events.InstanceDeregisteredEvent;
22+
import de.codecentric.boot.admin.server.domain.events.InstanceEndpointsDetectedEvent;
2123
import de.codecentric.boot.admin.server.domain.events.InstanceEvent;
2224
import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent;
25+
import de.codecentric.boot.admin.server.domain.values.Endpoints;
2326
import de.codecentric.boot.admin.server.domain.values.InstanceId;
2427
import de.codecentric.boot.admin.server.domain.values.Registration;
2528
import de.codecentric.boot.admin.server.domain.values.StatusInfo;
29+
import de.codecentric.boot.admin.server.eventstore.InstanceEventPublisher;
2630
import reactor.core.publisher.Mono;
2731
import reactor.test.StepVerifier;
2832

2933
import java.time.Duration;
34+
import java.util.Collections;
3035
import org.junit.Before;
3136
import org.junit.Test;
3237

3338
import static org.assertj.core.api.Assertions.assertThat;
3439
import static org.assertj.core.api.Assertions.assertThatThrownBy;
40+
import static org.mockito.ArgumentMatchers.any;
3541
import static org.mockito.Mockito.mock;
3642
import static org.mockito.Mockito.when;
3743

@@ -42,63 +48,122 @@ public class RemindingNotifierTest {
4248
private final Instance instance2 = Instance.create(InstanceId.of("id-2"))
4349
.register(Registration.create("App", "http://health").build())
4450
.withStatusInfo(StatusInfo.ofDown());
45-
private final InstanceEvent appDown = new InstanceStatusChangedEvent(instance1.getId(), instance1.getVersion(),
46-
StatusInfo.ofDown());
47-
private final InstanceEvent appUp = new InstanceStatusChangedEvent(instance1.getId(), instance1.getVersion(),
48-
StatusInfo.ofUp());
51+
private final InstanceEvent appDown = new InstanceStatusChangedEvent(instance1.getId(), 0L, StatusInfo.ofDown());
52+
private final InstanceEvent appUp = new InstanceStatusChangedEvent(instance1.getId(), 0L, StatusInfo.ofUp());
53+
private final InstanceEvent appEndpointsDiscovered = new InstanceEndpointsDetectedEvent(instance1.getId(), 0L,
54+
Endpoints.empty());
55+
private final InstanceEvent appDeregister = new InstanceDeregisteredEvent(instance1.getId(), 0L);
4956
private final InstanceEvent otherAppUp = new InstanceStatusChangedEvent(instance2.getId(), 0L, StatusInfo.ofUp());
5057
private InstanceRepository repository;
5158

5259
@Before
5360
public void setUp() {
5461
repository = mock(InstanceRepository.class);
62+
when(repository.find(any())).thenReturn(Mono.empty());
5563
when(repository.find(instance1.getId())).thenReturn(Mono.just(instance1));
5664
when(repository.find(instance2.getId())).thenReturn(Mono.just(instance2));
5765
}
5866

5967
@Test
60-
public void test_ctor_assert() {
68+
public void should_throw_on_invalid_ctor() {
6169
assertThatThrownBy(() -> new CompositeNotifier(null)).isInstanceOf(IllegalArgumentException.class);
6270
}
6371

6472
@Test
65-
public void test_remind() {
73+
public void should_remind_only_down_events() throws InterruptedException {
6674
TestNotifier notifier = new TestNotifier();
6775
RemindingNotifier reminder = new RemindingNotifier(notifier, repository);
6876
reminder.setReminderPeriod(Duration.ZERO);
6977

7078
StepVerifier.create(reminder.notify(appDown)).verifyComplete();
79+
StepVerifier.create(reminder.notify(appEndpointsDiscovered)).verifyComplete();
7180
StepVerifier.create(reminder.notify(otherAppUp)).verifyComplete();
72-
reminder.sendReminders();
73-
reminder.sendReminders();
81+
Thread.sleep(10);
82+
StepVerifier.create(reminder.sendReminders()).verifyComplete();
83+
Thread.sleep(10);
84+
StepVerifier.create(reminder.sendReminders()).verifyComplete();
7485

75-
assertThat(notifier.getEvents()).containsOnly(appDown, otherAppUp, appDown, appDown);
86+
assertThat(notifier.getEvents()).containsExactlyInAnyOrder(appDown, appEndpointsDiscovered, otherAppUp, appDown,
87+
appDown);
7688

7789
}
7890

7991
@Test
80-
public void test_no_remind_after_up() {
92+
public void should_not_remind_remind_after_up() {
8193
TestNotifier notifier = new TestNotifier();
8294
RemindingNotifier reminder = new RemindingNotifier(notifier, repository);
8395
reminder.setReminderPeriod(Duration.ZERO);
8496

8597
StepVerifier.create(reminder.notify(appDown)).verifyComplete();
8698
StepVerifier.create(reminder.notify(appUp)).verifyComplete();
87-
reminder.sendReminders();
99+
StepVerifier.create(reminder.sendReminders()).verifyComplete();
88100

89-
assertThat(notifier.getEvents()).containsOnly(appDown, appUp);
101+
assertThat(notifier.getEvents()).containsExactlyInAnyOrder(appDown, appUp);
102+
}
103+
104+
105+
@Test
106+
public void should_not_remind_remind_after_deregister() {
107+
TestNotifier notifier = new TestNotifier();
108+
RemindingNotifier reminder = new RemindingNotifier(notifier, repository);
109+
reminder.setReminderPeriod(Duration.ZERO);
110+
111+
StepVerifier.create(reminder.notify(appDown)).verifyComplete();
112+
StepVerifier.create(reminder.notify(appDeregister)).verifyComplete();
113+
StepVerifier.create(reminder.sendReminders()).verifyComplete();
114+
115+
assertThat(notifier.getEvents()).containsExactlyInAnyOrder(appDown, appDeregister);
90116
}
91117

92118
@Test
93-
public void test_no_remind_before_end() {
119+
public void should_not_remind_remind_before_period_ends() {
94120
TestNotifier notifier = new TestNotifier();
95121
RemindingNotifier reminder = new RemindingNotifier(notifier, repository);
96122
reminder.setReminderPeriod(Duration.ofHours(24));
97123

98124
StepVerifier.create(reminder.notify(appDown)).verifyComplete();
99-
reminder.sendReminders();
125+
StepVerifier.create(reminder.sendReminders()).verifyComplete();
126+
127+
assertThat(notifier.getEvents()).containsExactlyInAnyOrder(appDown);
128+
}
129+
130+
@Test
131+
public void should_resubscribe_after_error() {
132+
FluxNotifier notifier = new FluxNotifier();
133+
134+
RemindingNotifier reminder = new RemindingNotifier(notifier, repository);
135+
reminder.setCheckReminderInverval(Duration.ofMillis(1));
136+
reminder.setReminderPeriod(Duration.ofMillis(1));
137+
reminder.start();
138+
139+
StepVerifier.create(notifier)
140+
.expectSubscription()
141+
.then(() -> StepVerifier.create(reminder.notify(appDown)).verifyComplete())
142+
.expectNext(appDown)
143+
.expectNext(appDown)
144+
.then(() -> StepVerifier.create(
145+
reminder.notify(new InstanceDeregisteredEvent(InstanceId.of("ERROR"), 0L))).verifyComplete())
146+
.expectNext(appDown)
147+
.expectNext(appDown)
148+
.then(reminder::stop)
149+
.expectNoEvent(Duration.ofMillis(10))
150+
.thenCancel()
151+
.verify();
152+
153+
reminder.stop();
154+
}
155+
156+
157+
private static class FluxNotifier extends InstanceEventPublisher implements Notifier {
100158

101-
assertThat(notifier.getEvents()).containsOnly(appDown);
159+
@Override
160+
public Mono<Void> notify(InstanceEvent event) {
161+
if (event.getInstance().getValue().equals("ERROR")) {
162+
throw new IllegalArgumentException("TEST-ERROR");
163+
}
164+
this.publish(Collections.singletonList(event));
165+
return Mono.empty();
166+
}
102167
}
103168

104169
}

0 commit comments

Comments
 (0)