Repository navigation
fix(server): resolve Hazelcast notification trigger independent of auto-configuration order - #5726
YadavKshitiz wants to merge 4 commits into
Conversation
|
Hello @YadavKshitiz. Below my configuration: @Configuration(proxyBeanMethods = false)
public class NotificationConfig {
// See de.codecentric.boot.admin.server.config.AdminServerNotifierAutoConfiguration.MailNotifierConfiguration.mailNotifier
static final String MAIL_CONFIG_PROPS_PREFIX = "spring.boot.admin.notify.mail";
// See de.codecentric.boot.admin.server.config.AdminServerNotifierAutoConfiguration.MailNotifierConfiguration.mailNotifierTemplateEngine
private static final String TEMPLATE_ENGINE_BEAN_NAME = "mailNotifierTemplateEngine";
@Bean
@Validated
@ConfigurationProperties(MAIL_CONFIG_PROPS_PREFIX)
public DelayingMailNotifier mailNotifier(JavaMailSender mailSender, InstanceRepository repository, @Qualifier(TEMPLATE_ENGINE_BEAN_NAME) TemplateEngine templateEngine) {
return new DelayingMailNotifier(mailSender, repository, templateEngine);
}
@Bean
@Primary
@ConditionalOnBean(DelayingMailNotifier.class)
public FilteringNotifier filteringNotifier(DelayingMailNotifier mailNotifier, InstanceRepository instanceRepository) {
return new FilteringNotifier(mailNotifier, instanceRepository);
}
} |
Thanks for sharing your config, that helps a lot! Since your notifiers are declared as your own @bean, I think you were probably not hit by the A few questions so we can narrow it down:
Thanks again! |
Hello @YadavKshitiz
|
Thanks, that's really helpful! Your setup looks fine to me, and since your notifiers are @beans I don't think my PR (which only affects notifiers configured via properties) is what you're hitting. I've changed it to "Related to" so this issue stays open. Your DelayingMailNotifier is my main suspect now: only the node that "wins" an event in Hazelcast calls notify(), so the delayed job (and the cancel on recovery) only exists on that node. A recovery handled by the other node wouldn't cancel it. Could you help me with two things?
When you redeploy, DEBUG logging for de.codecentric.boot.admin.server.notify on both nodes would be great, plus a note of whether each node logs "Triggering notifiers for ..." for the same event. |
|
Hello @YadavKshitiz. Below the implementation of the notifier: import de.codecentric.boot.admin.server.domain.entities.Instance;
import de.codecentric.boot.admin.server.domain.entities.InstanceRepository;
import de.codecentric.boot.admin.server.domain.events.InstanceEvent;
import de.codecentric.boot.admin.server.domain.values.InstanceId;
import de.codecentric.boot.admin.server.notify.MailNotifier;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.validation.constraints.NotNull;
import java.time.Duration;
import java.time.LocalDate;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.hibernate.validator.constraints.time.DurationMin;
import org.jspecify.annotations.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.format.datetime.standard.DurationFormatterUtils;
import org.springframework.mail.javamail.JavaMailSender;
import org.thymeleaf.TemplateEngine;
import reactor.core.Disposable;
import reactor.core.publisher.Mono;
import static java.time.DayOfWeek.FRIDAY;
import static java.time.DayOfWeek.MONDAY;
import static java.time.LocalDate.now;
import static com.sixgroup.cit.fmd.zookeepers.monitoring.notification.DelayingMailNotifier.DayWindow.EVERYDAY;
import static de.codecentric.boot.admin.server.domain.values.StatusInfo.STATUS_OFFLINE;
import static de.codecentric.boot.admin.server.domain.values.StatusInfo.STATUS_RESTRICTED;
import static de.codecentric.boot.admin.server.domain.values.StatusInfo.STATUS_UNKNOWN;
import static de.codecentric.boot.admin.server.domain.values.StatusInfo.STATUS_UP;
import static org.springframework.format.annotation.DurationFormat.Style.COMPOSITE;
import static reactor.core.publisher.Mono.empty;
import static reactor.core.scheduler.Schedulers.boundedElastic;
public class DelayingMailNotifier extends MailNotifier {
private static final Logger log = LoggerFactory.getLogger(DelayingMailNotifier.class);
private static final Set<String> OK_STATUSES = Set.of(STATUS_UP, STATUS_UNKNOWN, STATUS_OFFLINE, STATUS_RESTRICTED);
private static final String DELAY_PROPERTY_KEY = "delay";
private static final String OK_STATUSES_PROPERTY_KEY = "okStatuses";
private final InstanceRepository repository;
private final Map<InstanceId, Disposable> pendingInstanceNotifications;
@NotNull private DayWindow dayWindow;
@NotNull @DurationMin(seconds = 1) private Duration delay;
public DelayingMailNotifier(JavaMailSender mailSender, InstanceRepository repository, TemplateEngine templateEngine) {
super(mailSender, repository, templateEngine);
this.repository = repository;
this.dayWindow = EVERYDAY;
this.delay = Duration.ofMinutes(5);
this.pendingInstanceNotifications = new ConcurrentHashMap<>();
}
@PostConstruct
void propagateAdditionalProperties() {
Map<String, Object> additionalProperties = getAdditionalProperties();
additionalProperties.put(DELAY_PROPERTY_KEY, DurationFormatterUtils.print(delay, COMPOSITE));
additionalProperties.put(OK_STATUSES_PROPERTY_KEY, OK_STATUSES);
}
@PreDestroy
void disposePendingNotifications() {
pendingInstanceNotifications.keySet().forEach(this::cancelPendingNotification);
pendingInstanceNotifications.clear();
}
// Visible for testing
Map<InstanceId, Disposable> getPendingInstanceNotifications() {
return pendingInstanceNotifications;
}
public Duration getDelay() {
return delay;
}
public void setDelay(Duration delay) {
this.delay = delay;
}
public DayWindow getDayWindow() {
return dayWindow;
}
public void setDayWindow(DayWindow dayWindow) {
this.dayWindow = dayWindow;
}
@Override
protected boolean shouldNotify(@NonNull InstanceEvent event, @NonNull Instance instance) {
return super.shouldNotify(event, instance) && dayWindow.isWithinWindow(now());
}
@NonNull
@Override
protected Mono<Void> doNotify(@NonNull InstanceEvent event, @NonNull Instance instance) {
return isEnabled() ?
cancelPendingNotification(event).then(scheduleDelayedNotification(event)) :
Mono.empty();
}
private Mono<Void> scheduleDelayedNotification(InstanceEvent event) {
return Mono.delay(delay)
.doOnSubscribe(s -> {
log.info("Scheduling delayed notification for instance {} with a delay of {}", event.getInstance(), delay);
// Update the last status before scheduling the notification so that the next run sees the pending status and it doesn't reschedule the notification again
updateLastStatus(event);
pendingInstanceNotifications.put(event.getInstance(), s::cancel);
})
.flatMap(_ -> doNotifyIfNotRecovered(event))
.doFinally(_ -> pendingInstanceNotifications.remove(event.getInstance()))
.then();
}
private Mono<Instance> getInstanceIfNotRecovered(InstanceId id) {
return repository
.find(id)
.filter(instance -> !OK_STATUSES.contains(instance.getStatusInfo().getStatus()));
}
private Mono<Void> doNotifyIfNotRecovered(InstanceEvent event) {
return getInstanceIfNotRecovered(event.getInstance())
.switchIfEmpty(signalRecovery(event).then(empty()))
.flatMap(i -> sendNotification(event, i));
}
private Mono<Void> sendNotification(InstanceEvent event, Instance instance) {
return super
.doNotify(event, instance)
.subscribeOn(boundedElastic())
.doOnSubscribe(_ -> log.info("Performing delayed notification for instance {} since it didn't recover after {}", event.getInstance(), delay));
}
private Mono<Void> signalRecovery(InstanceEvent event) {
return cancelPendingNotification(event).doOnSubscribe(_ -> log.info("Instance {} has recovered before the delay period. Cancelling the pending notification.", event.getInstance()));
}
private Mono<Void> cancelPendingNotification(InstanceEvent event) {
return Mono.fromRunnable(() -> cancelPendingNotification(event.getInstance()));
}
private void cancelPendingNotification(InstanceId id) {
Disposable existingJob = pendingInstanceNotifications.remove(id);
if(existingJob != null && !existingJob.isDisposed()) {
existingJob.dispose();
}
}
public enum DayWindow {
EVERYDAY {
@Override
boolean isWithinWindow(LocalDate date) {
return true;
}
}, WEEKDAYS {
private static final int START_VALUE = MONDAY.getValue();
private static final int END_VALUE = FRIDAY.getValue();
@Override
boolean isWithinWindow(LocalDate date) {
int dayOfWeekValue = date.getDayOfWeek().getValue();
return dayOfWeekValue >= START_VALUE && dayOfWeekValue <= END_VALUE;
}
};
abstract boolean isWithinWindow(LocalDate date);
}
}
|
Hi @cdprete, thanks for the code and the details, that helped a lot! I have a theory but no proof yet, so I'd really appreciate it if you could test my branch once. The Hazelcast check decides which node handles each event, but the "last known status" is kept per node. If two events for the same outage get claimed by different nodes, the second node doesn't know the first one already saw DOWN, so it schedules its own delayed job. Both jobs then fire ~5 minutes later and send a mail at the same time. My PR shares that last Branch: https://github.com/YadavKshitiz/spring-boot-admin/tree/fix/5716-hazelcast-notification-dedup Could you try it on both nodes and let me know?
Either result is useful, so no worries if it doesn't fix it. It will tell us where to look next. |
|
I would need time to test this but, if I understood you correctly, you're
going back to my initial analysis results.
Il mer 30 set 2026, 09:08 Kshitiz Yadav ***@***.***> ha
scritto:
… *YadavKshitiz* left a comment (codecentric/spring-boot-admin#5726)
<#5726 (comment)>
Hello @YadavKshitiz <https://github.com/YadavKshitiz>.
Below the implementation of the notifier:
import de.codecentric.boot.admin.server.domain.entities.Instance;import de.codecentric.boot.admin.server.domain.entities.InstanceRepository;import de.codecentric.boot.admin.server.domain.events.InstanceEvent;import de.codecentric.boot.admin.server.domain.values.InstanceId;import de.codecentric.boot.admin.server.notify.MailNotifier;import jakarta.annotation.PostConstruct;import jakarta.annotation.PreDestroy;import jakarta.validation.constraints.NotNull;import java.time.Duration;import java.time.LocalDate;import java.util.Map;import java.util.Set;import java.util.concurrent.ConcurrentHashMap;import org.hibernate.validator.constraints.time.DurationMin;import org.jspecify.annotations.NonNull;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.format.datetime.standard.DurationFormatterUtils;import org.springframework.mail.javamail.JavaMailSender;import org.thymeleaf.TemplateEngine;import reactor.core.Disposable;import reactor.core.publisher.Mono;
import static java.time.DayOfWeek.FRIDAY;import static java.time.DayOfWeek.MONDAY;import static java.time.LocalDate.now;
import static com.sixgroup.cit.fmd.zookeepers.monitoring.notification.DelayingMailNotifier.DayWindow.EVERYDAY;import static de.codecentric.boot.admin.server.domain.values.StatusInfo.STATUS_OFFLINE;import static de.codecentric.boot.admin.server.domain.values.StatusInfo.STATUS_RESTRICTED;import static de.codecentric.boot.admin.server.domain.values.StatusInfo.STATUS_UNKNOWN;import static de.codecentric.boot.admin.server.domain.values.StatusInfo.STATUS_UP;import static org.springframework.format.annotation.DurationFormat.Style.COMPOSITE;import static reactor.core.publisher.Mono.empty;import static reactor.core.scheduler.Schedulers.boundedElastic;
public class DelayingMailNotifier extends MailNotifier {
private static final Logger log = LoggerFactory.getLogger(DelayingMailNotifier.class);
private static final Set<String> OK_STATUSES = Set.of(STATUS_UP, STATUS_UNKNOWN, STATUS_OFFLINE, STATUS_RESTRICTED);
private static final String DELAY_PROPERTY_KEY = "delay";
private static final String OK_STATUSES_PROPERTY_KEY = "okStatuses";
private final InstanceRepository repository;
private final Map<InstanceId, Disposable> pendingInstanceNotifications;
@NotNull private DayWindow dayWindow;
@NotNull @DurationMin(seconds = 1) private Duration delay;
public DelayingMailNotifier(JavaMailSender mailSender, InstanceRepository repository, TemplateEngine templateEngine) {
super(mailSender, repository, templateEngine);
this.repository = repository;
this.dayWindow = EVERYDAY;
this.delay = Duration.ofMinutes(5);
this.pendingInstanceNotifications = new ConcurrentHashMap<>();
}
@PostConstruct
void propagateAdditionalProperties() {
Map<String, Object> additionalProperties = getAdditionalProperties();
additionalProperties.put(DELAY_PROPERTY_KEY, DurationFormatterUtils.print(delay, COMPOSITE));
additionalProperties.put(OK_STATUSES_PROPERTY_KEY, OK_STATUSES);
}
@PreDestroy
void disposePendingNotifications() {
pendingInstanceNotifications.keySet().forEach(this::cancelPendingNotification);
pendingInstanceNotifications.clear();
}
// Visible for testing
Map<InstanceId, Disposable> getPendingInstanceNotifications() {
return pendingInstanceNotifications;
}
public Duration getDelay() {
return delay;
}
public void setDelay(Duration delay) {
this.delay = delay;
}
public DayWindow getDayWindow() {
return dayWindow;
}
public void setDayWindow(DayWindow dayWindow) {
this.dayWindow = dayWindow;
}
@OverRide
protected boolean ***@***.*** InstanceEvent event, @nonnull Instance instance) {
return super.shouldNotify(event, instance) && dayWindow.isWithinWindow(now());
}
@nonnull
@OverRide
protected Mono<Void> ***@***.*** InstanceEvent event, @nonnull Instance instance) {
return isEnabled() ?
cancelPendingNotification(event).then(scheduleDelayedNotification(event)) :
Mono.empty();
}
private Mono<Void> scheduleDelayedNotification(InstanceEvent event) {
return Mono.delay(delay)
.doOnSubscribe(s -> {
log.info("Scheduling delayed notification for instance {} with a delay of {}", event.getInstance(), delay);
// Update the last status before scheduling the notification so that the next run sees the pending status and it doesn't reschedule the notification again
updateLastStatus(event);
pendingInstanceNotifications.put(event.getInstance(), s::cancel);
})
.flatMap(_ -> doNotifyIfNotRecovered(event))
.doFinally(_ -> pendingInstanceNotifications.remove(event.getInstance()))
.then();
}
private Mono<Instance> getInstanceIfNotRecovered(InstanceId id) {
return repository
.find(id)
.filter(instance -> !OK_STATUSES.contains(instance.getStatusInfo().getStatus()));
}
private Mono<Void> doNotifyIfNotRecovered(InstanceEvent event) {
return getInstanceIfNotRecovered(event.getInstance())
.switchIfEmpty(signalRecovery(event).then(empty()))
.flatMap(i -> sendNotification(event, i));
}
private Mono<Void> sendNotification(InstanceEvent event, Instance instance) {
return super
.doNotify(event, instance)
.subscribeOn(boundedElastic())
.doOnSubscribe(_ -> log.info("Performing delayed notification for instance {} since it didn't recover after {}", event.getInstance(), delay));
}
private Mono<Void> signalRecovery(InstanceEvent event) {
return cancelPendingNotification(event).doOnSubscribe(_ -> log.info("Instance {} has recovered before the delay period. Cancelling the pending notification.", event.getInstance()));
}
private Mono<Void> cancelPendingNotification(InstanceEvent event) {
return Mono.fromRunnable(() -> cancelPendingNotification(event.getInstance()));
}
private void cancelPendingNotification(InstanceId id) {
Disposable existingJob = pendingInstanceNotifications.remove(id);
if(existingJob != null && !existingJob.isDisposed()) {
existingJob.dispose();
}
}
public enum DayWindow {
EVERYDAY {
@OverRide
boolean isWithinWindow(LocalDate date) {
return true;
}
}, WEEKDAYS {
private static final int START_VALUE = MONDAY.getValue();
private static final int END_VALUE = FRIDAY.getValue();
@OverRide
boolean isWithinWindow(LocalDate date) {
int dayOfWeekValue = date.getDayOfWeek().getValue();
return dayOfWeekValue >= START_VALUE && dayOfWeekValue <= END_VALUE;
}
};
abstract boolean isWithinWindow(LocalDate date);
}
}
* the notifications are sent after 5 minutes, but I receive both the duplicates at the same time.
* see code above
Hi @cdprete <https://github.com/cdprete>, thanks for the code and the
details, that helped a lot!
I have a theory but no proof yet, so I'd really appreciate it if you could
test my branch once. The Hazelcast check decides which node handles each
*event*, but the "last known status" is kept per node. If two events for
the same outage get claimed by different nodes, the second node doesn't
know the first one already saw DOWN, so it schedules its own delayed job.
Both jobs then fire ~5 minutes later and send a mail at the same time. My
PR shares that last
status across nodes, which should stop the second job.
Branch:
https://github.com/YadavKshitiz/spring-boot-admin/tree/fix/5716-hazelcast-notification-dedup
Could you try it on both nodes and let me know?
1. Build the server from that branch (./mvnw -DskipTests install in
the repo root) and use that version on both nodes. Your Hazelcast config
from the docs works unchanged; the new map
spring-boot-admin-last-statuses is created automatically.
2. Trigger a status change (for example stop a client) and check
whether you still get duplicate mails.
3. If you still get duplicates, DEBUG logging for
de.codecentric.boot.admin.server.notify on both nodes would help a
lot. In particular the "Triggering notifiers for ..." and "Scheduling
delayed notification ..." lines, with the timestamps and which node logged
each.
Either result is useful, so no worries if it doesn't fix it. It will tell
us where to look next.
Thanks a lot for your time!
—
Reply to this email directly, view it on GitHub
<#5726?email_source=notifications&email_token=ACAZGWF4EW2TBB5LBPFZDNT5RSWN7A5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKOJQGYYDINBXG43KM4TFMFZW63VHNVSW45DJN5XKKZLWMVXHJLDGN5XXIZLSL5RWY2LDNM#issuecomment-5906044776>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ACAZGWHA4SEOREA4S52DHM35RSWN7AVCNFSNUABEKJSXA33TNF2G64TZHMZDEMRVGQ4DKNR3JFZXG5LFHM2TMMRZGMYTCNZQGCQXMAQ>
.
Triage notifications, keep track of coding agent tasks and review pull
requests on the go with GitHub Mobile for iOS
<https://github.com/notifications/mobile/ios/ACAZGWANCOJ7HBGDF627MIT5RSWN7A5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKOJQGYYDINBXG43KM4TFMFZW63VHNVSW45DJN5XKKZLWMVXHJKTGN5XXIZLSL5UW64Y>
and Android
<https://github.com/notifications/mobile/android/ACAZGWCSBCMEUYO73BGHEPT5RSWN7A5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKOJQGYYDINBXG43KM4TFMFZW63VHNVSW45DJN5XKKZLWMVXHJLTGN5XXIZLSL5QW4ZDSN5UWI>.
Download it today!
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
|
Yes, you're right, and thanks for pointing that out and no rush on testing, take the time you need. |
|
I will check again |
Thanks for pointing this out, and I really appreciate your feedback on my code. Here's what I looked into. I read through #5737 and I like the approach. You're right that my version is heavier than it needs to be. What I'm thinking of changing: AbstractStatusChangeNotifier would just take a plain ConcurrentMap<InstanceId, String>, with a normal ConcurrentHashMap as the default, the same way #5737 does it. The Hazelcast config would then hand each notifier its own IMap, named after the notifier bean (something like "spring-boot-admin-last-statuses-"). That lets me drop the LastStatusStore wrapper, the key prefix and the extra property. I'd keep one map per notifier because otherwise two notifiers (say Slack and Mail) would overwrite each other's last status. Does that match what you had in mind? And when you said the map handling felt like overkill, did you |
|
Please align with the team about the issue you mention it's fixed with your first commit and, in general, on how to proceed. For example @SteKoe or @ulischulte. To me, as a consumer, given that each notifier should keep its state, I think it would be enough that the notifiers expose their |
|
Hi @SteKoe, @ulischulte, I could use your help deciding how to move forward with this PR. It currently mixes two separate things, and I'd like to untangle them before changing anything else.
Which would you prefer? I'm happy to go with whatever fits the project best. One more thing, and I may well be wrong since I haven't run it: #5737 seems to create its Thanks a lot for your time! |
|
Hi @YadavKshitiz, thanks for the detailed analysis! I checked both points against current
We'd like to split this up into two PRs: 1. Notifier wiring with Hazelcast
2. Shared last status
Please drop We'll keep #5716 open until @cdprete can confirm whether this fixes his duplicates. Thanks again! |
@SteKoe for safety and consistency, I would rather go with constructor injection rather than the setter one. |
4ddacb6 to
0bdb820
Compare
|
Hi @SteKoe, I've pushed the changes for the wiring PR. Summary:
Is that what you had in mind, or did you want a FilteringNotifier still created automatically? For the second PR (shared last status), I'll make lastStatuses a ConcurrentMap with ConcurrentHashMap as the default and pass it through a constructor, as @cdprete suggested. AbstractStatusChangeNotifier has about 15 subclasses, including MailNotifier, which @cdprete extends. Should I add a map constructor to every notifier, or only to the abstract class plus the commonly extended ones such as MailNotifier? And do you want a setter as well? Thanks again for the guidance! |
|
@SteKoe Please Tell, so i can continue my work |
| @Bean(name = SENT_NOTIFICATIONS_BEAN_NAME) | ||
| @Lazy | ||
| @ConditionalOnMissingBean(name = SENT_NOTIFICATIONS_BEAN_NAME) | ||
| public IMap<InstanceId, Long> sentNotificationsMap(HazelcastInstance hazelcastInstance, |
There was a problem hiding this comment.
Like for the other maps already present, I don't think it's necessary to have them declared as beans explicitly.
|
Thanks @YadavKshitiz, this looks good. Decisions on the open points:
Map beans: We keep Please address these before merge:
This PR stays "Related to #5716". We keep the issue open until @cdprete confirms the duplicates are gone, which depends on the shared last status in the follow-up PR. |
@SteKoe I don't have currently the time to properly test this. If it looks ok to you, feel free to merge it and release it. I'll report a new bug ticket if the issue then still occurs with the new released version. |
… FilteringNotifier
…ifier circularity
0bdb820 to
b2a9b29
Compare
…nfigured notifier
Thanks @SteKoe, both points are addressed and pushed:
I updated the PR title and description to the new scope, with the release-notes entry in it. I saw there's an upgrading section under docs/11-upgrading; do you want an entry there too? For the follow-up PR (shared last status), could you confirm: a map constructor on every notifier, or only on the abstract class plus commonly extended ones like MailNotifier, and do you still want a setter as well? I'll also need the |
Related to #5716
Summary
With Hazelcast, notification deduplication across cluster nodes did not work when the notifiers were
configured through
spring.boot.admin.notify.*properties. Also, the (unreleased) change from #5737 made theapplication fail to start with a user-defined
@Beannotifier. This PR fixes both.Problems
HazelcastNotificationTriggerwas not created for property-configured notifiers (Slack, Mail, Teams, ...).AdminServerHazelcastAutoConfigurationis ordered beforeAdminServerNotifierAutoConfiguration, so its@ConditionalOnBean(Notifier.class)was evaluated before those notifiers existed. A plainNotificationTriggerwas created instead, with no cross-node deduplication. The existing
hazelcastConfig()test only passed becauseit declares the notifier as a user bean.
FilteringNotifieris itself aNotifier, so theCompositeNotifiercollects it and the context fails with a
BeanCurrentlyInCreationException(filteringNotifier->compositeNotifier->filteringNotifier). With property-only notifiers noFilteringNotifierwas created.Changes
AdminServerHazelcastAutoConfigurationexposes two lazy beans:sentNotificationsMapandnotificationFiltersMap.AdminServerNotifierAutoConfigurationgets the sent-notifications map as a plainConcurrentMapthrough anObjectProvider, resolved lazily when the trigger is created, and createsHazelcastNotificationTriggerwhen itis present. It does not reference Hazelcast classes, so Hazelcast stays optional, and the result no longer depends
on auto-configuration order.
NotifierTriggerConfigurationand the auto-createdFilteringNotifier. To share filters in acluster, declare your own
FilteringNotifierand pass thenotificationFiltersMapbean.CompositeNotifier.Tests
AdminServerHazelcastAutoConfigurationTest: single@Beannotifier, notifiers configured via properties,multiple property notifiers, Hazelcast disabled, and a user
FilteringNotifierusing the shared filters map.The property-notifier tests fail on master (plain
NotificationTrigger) and pass with this change.AdminServerAutoConfigurationTest: the runner now includesAdminServerNotifierAutoConfiguration.spring-boot-admin-servermodule run green locally (504 tests); Checkstyle and javaformat pass.About #5716
The reporter's notifiers are user-declared
@Beans, so the wiring problem above most likely did not affect him.His suspicion about the per-node last status in
AbstractStatusChangeNotifierremains the likeliest cause of hisduplicates. That is handled in a follow-up PR (map injection), so this PR only references the issue. Nothing was
tested on two live nodes.
Release notes
AdminServerHazelcastAutoConfiguration.NotifierTriggerConfiguration(internalauto-configuration detail). The Hazelcast trigger is now created by
AdminServerNotifierAutoConfiguration.FilteringNotifierfrom fix: share notification filters across Hazelcast cluster nodes #5737 (unreleased) is removed. Declare your ownFilteringNotifierandpass
notificationFiltersMapto share filters (see the clustering docs).