Repository: axonframework/axonframework
Issue: Duplicate initialization of TrackingEventProcessor worker threads

<!-- Please use markdown (https://guides.github.com/features/mastering-markdown/) semantics throughout the bug description. -->

### Basic information

* Axon Framework version: 4.7.1
* JDK version:  Amazon Coretto 17

### Steps to reproduce

We build our applications using Spring Boot and we recently introduced a spring actuator `HealthIndicator` to monitor the health of all axon event processors. The health indicator starts as a spring component and injects the axon `EventProcessingConfiguration` to access the `.eventProcessors()` method to loop over all event processors checking their status.

After the introduction of our new `HealthIndicator` we started seeing random occurrences of duplicated events in our event processors. We spent the last few days tracking down the error and finally found a race condition in the initialization of the axon event processors. After looking at the axon source code we found that calling the `.eventProcessors()` was not a simple get operation (as we expected) but it actually initialized the event processors. In some rare occasions the `HealthIndicator` will call the `.eventProcessors()` method at the same time as it is being initialized by axon during startup. Since the `EventProcessingModule.initializeProcessors()` is not thread safe this will causes duplicate event processor threads being created for the same event processor and segment.

### Expected behaviour

The `.eventProcessors()` method looks like a simple getter, and should not have side effects. If the initialization side effects are unavoidable it should be thread safe.

### Actual behaviour

Race condition when calling `.eventProcessors()` from multiple locations simultaneously during startup.


Changed files:
config/src/main/java/org/axonframework/config/EventProcessingModule.java

diff --git a/config/src/main/java/org/axonframework/config/EventProcessingModule.java b/config/src/main/java/org/axonframework/config/EventProcessingModule.java
index 092a993e6..535b70ac5 100644
--- a/config/src/main/java/org/axonframework/config/EventProcessingModule.java
+++ b/config/src/main/java/org/axonframework/config/EventProcessingModule.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010-2022. Axon Framework
+ * Copyright (c) 2010-2023. Axon Framework
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -65,6 +65,7 @@
 import java.util.Optional;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.BiFunction;
 import java.util.function.Consumer;
 import java.util.function.Function;
@@ -129,6 +130,7 @@
     protected final Map<String, PooledStreamingProcessorConfiguration> psepConfigs = new HashMap<>();
     protected final Map<String, DeadLetteringInvokerConfiguration> deadLetteringInvokerConfigs = new HashMap<>();
 
+    private final AtomicBoolean initialized = new AtomicBoolean(false);
     private Configuration configuration;
 
     private final Component<ListenerInvocationErrorHandler> defaultListenerInvocationErrorHandler = new Component<>(
@@ -209,7 +211,15 @@ public void initialize(Configuration configuration) {
      * processors have already been initialized, this method does nothing.
      */
     private void initializeProcessors() {
-        if (eventProcessors.isEmpty()) {
+        if (initialized.get()) {
+            return;
+        }
+
+        synchronized (initialized) {
+            if (initialized.get()) {
+                return;
+            }
+
             instanceSelectors.sort(comparing(InstanceProcessingGroupSelector::getPriority).reversed());
 
             Map<String, List<Function<Configuration, EventHandlerInvoker>>> handlerInvokers = new HashMap<>();
@@ -217,12 +227,14 @@ private void initializeProcessors() {
             registerSagaManagers(handlerInvokers);
 
             handlerInvokers.forEach((processorName, invokers) -> {
-                Component<EventProcessor> eventProcessorComponent =
-                        new Component<>(configuration, processorName, c -> buildEventProcessor(invokers, processorName));
+                Component<EventProcessor> eventProcessorComponent = new Component<>(
+                        configuration, processorName, c -> buildEventProcessor(invokers, processorName)
+                );
                 eventProcessors.put(processorName, eventProcessorComponent);
             });
 
             eventProcessors.values().forEach(Component::get);
+            initialized.set(true);
         }
     }