Skip to content

Commit c4ccc21

Browse files
improve bean instantiations check
1 parent 03e4ab7 commit c4ccc21

5 files changed

Lines changed: 220 additions & 42 deletions

File tree

.github/workflows/bean-instantiations-startup.yml

Lines changed: 155 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ jobs:
3030

3131
- name: Start Spring Boot app
3232
run: |
33-
PROFILES=dev,localci,lti,aeolus,theia,iris,localvc,artemis,scheduling,buildagent,core,ldap,no-liquibase
33+
PROFILES=dev,localci,lti,aeolus,theia,iris,localvc,artemis,scheduling,buildagent,core,ldap
3434
JAR=$(ls build/libs/Artemis*.jar | head -n1)
3535
nohup java -jar $JAR \
36-
--spring.profiles.active=dev,localci,lti,aeolus,theia,iris,localvc,artemis,scheduling,buildagent,core,ldap,no-liquibase \
36+
--spring.profiles.active=$PROFILES \
3737
--artemis.user-management.passkey.enabled=true \
3838
--artemis.user-management.use-external=false \
3939
--artemis.iris.url=http://iris.fake \
@@ -49,58 +49,179 @@ jobs:
4949
5050
- name: Wait for the application to start
5151
run: |
52+
# Configuration variables
5253
RUNNING_MESSAGE="'Artemis is running!'"
53-
echo "Waiting up to 60s for $RUNNING_MESSAGE to appear in app.log..."
54+
STARTUP_TIMEOUT_ATTEMPTS=30
55+
STARTUP_RETRY_INTERVAL=2
56+
STARTUP_TOTAL_TIMEOUT=$((STARTUP_TIMEOUT_ATTEMPTS * STARTUP_RETRY_INTERVAL))
57+
LOG_FILE="app.log"
58+
59+
echo "Waiting up to ${STARTUP_TOTAL_TIMEOUT}s for $RUNNING_MESSAGE to appear in $LOG_FILE..."
5460
5561
isAppRunning=false
56-
for i in {1..30}; do
57-
if grep -q $RUNNING_MESSAGE app.log; then
62+
for i in $(seq 1 $STARTUP_TIMEOUT_ATTEMPTS); do
63+
if grep -q $RUNNING_MESSAGE $LOG_FILE; then
5864
isAppRunning=true
59-
echo "✅ Found $RUNNING_MESSAGE in app.log after $i attempts"
65+
echo "✅ Found $RUNNING_MESSAGE in $LOG_FILE after $i attempts"
6066
break
6167
fi
62-
echo " attempt $i/30: not found yet"
63-
sleep 2
68+
echo " attempt $i/$STARTUP_TIMEOUT_ATTEMPTS: not found yet"
69+
sleep $STARTUP_RETRY_INTERVAL
6470
done
6571
6672
if [ "$isAppRunning" = false ]; then
67-
echo "❌ Timeout: $RUNNING_MESSAGE not found in app.log"
68-
cat app.log
73+
echo "❌ Timeout: $RUNNING_MESSAGE not found in $LOG_FILE"
74+
cat $LOG_FILE
75+
exit 1
76+
fi
77+
78+
- name: Extract and validate startup bean instantiation metrics
79+
run: |
80+
# Configuration variables
81+
STARTUP_LOG_PATTERN="Bean instantiation graph exported to startupBeans\.dot \(([0-9]+) edges, longest dependency chain: \[([^\]]*)\]\)"
82+
MIN_INSTANTIATED_BEANS=20
83+
MAX_INSTANTIATED_BEANS=94
84+
MAX_STARTUP_DEPENDENCY_CHAIN_LENGTH=9
85+
LOG_FILE="app.log"
86+
87+
# Extract search text from pattern (everything before opening parenthesis)
88+
STARTUP_SEARCH=$(echo "$STARTUP_LOG_PATTERN" | sed 's/ \\(.*$//')
89+
90+
LINE=$(grep -E "$STARTUP_SEARCH" $LOG_FILE) \
91+
|| { echo "❌ No startup metrics line found"; cat $LOG_FILE; exit 1; }
92+
93+
if [[ "$LINE" =~ $STARTUP_LOG_PATTERN ]]; then
94+
INSTANTIATED_BEANS=${BASH_REMATCH[1]}
95+
LONGEST_CHAIN_CONTENT=${BASH_REMATCH[2]}
96+
# Count chain length by counting commas and adding 1 (if not empty)
97+
if [ -n "$LONGEST_CHAIN_CONTENT" ]; then
98+
ACTUAL_MAX_CHAIN_LENGTH=$(echo "$LONGEST_CHAIN_CONTENT" | tr ',' '\n' | wc -l)
99+
else
100+
ACTUAL_MAX_CHAIN_LENGTH=0
101+
fi
102+
else
103+
echo "❌ Failed to parse startup metrics from: $LINE"
104+
exit 1
105+
fi
106+
107+
echo "• Number of instantiated beans = $INSTANTIATED_BEANS"
108+
echo "• Longest dependency chain length = $ACTUAL_MAX_CHAIN_LENGTH"
109+
echo "• Longest chain: [$LONGEST_CHAIN_CONTENT]"
110+
111+
echo "Validating against thresholds: MIN_INSTANTIATED_BEANS=$MIN_INSTANTIATED_BEANS (expected ≥ $MIN_INSTANTIATED_BEANS), MAX_INSTANTIATED_BEANS=$MAX_INSTANTIATED_BEANS (expected ≤ $MAX_INSTANTIATED_BEANS), MAX_STARTUP_DEPENDENCY_CHAIN_LENGTH=$MAX_STARTUP_DEPENDENCY_CHAIN_LENGTH (expected ≤ $MAX_STARTUP_DEPENDENCY_CHAIN_LENGTH)"
112+
113+
if (( INSTANTIATED_BEANS < MIN_INSTANTIATED_BEANS )); then
114+
echo "❌ $INSTANTIATED_BEANS < $MIN_INSTANTIATED_BEANS beans. Something seems to be wrong, as usually more beans are instantiated."
115+
exit 1
116+
fi
117+
118+
if (( INSTANTIATED_BEANS > MAX_INSTANTIATED_BEANS )); then
119+
echo "❌ $INSTANTIATED_BEANS > $MAX_INSTANTIATED_BEANS threshold"
120+
exit 1
121+
fi
122+
123+
if (( ACTUAL_MAX_CHAIN_LENGTH > MAX_STARTUP_DEPENDENCY_CHAIN_LENGTH )); then
124+
echo "❌ Longest dependency chain length $ACTUAL_MAX_CHAIN_LENGTH > $MAX_STARTUP_DEPENDENCY_CHAIN_LENGTH threshold"
125+
exit 1
126+
fi
127+
128+
echo "✅ Final values → Beans: $INSTANTIATED_BEANS; Longest dependency chain length: $ACTUAL_MAX_CHAIN_LENGTH"
129+
echo "🎉 Startup bean instantiation metrics within expected ranges"
130+
131+
- name: Check for startup dependency chains exceeding threshold
132+
run: |
133+
# Configuration variables
134+
STARTUP_DEEP_CHAIN_PATTERN="Startup long bean instantiation chain"
135+
MAX_STARTUP_DEPENDENCY_CHAIN_LENGTH=9
136+
LOG_FILE="app.log"
137+
138+
echo "Checking for startup dependency chains exceeding threshold..."
139+
140+
if (( ACTUAL_MAX_CHAIN_LENGTH > MAX_STARTUP_DEPENDENCY_CHAIN_LENGTH )); then
141+
echo "❌ Startup dependency chain length $ACTUAL_MAX_CHAIN_LENGTH > $MAX_STARTUP_DEPENDENCY_CHAIN_LENGTH threshold"
142+
echo "Chains exceeding threshold:"
143+
grep -E "$STARTUP_DEEP_CHAIN_PATTERN" $LOG_FILE || echo " (No detailed chains found in log)"
144+
echo "🔧 These chains violate the threshold MAX_STARTUP_DEPENDENCY_CHAIN_LENGTH. Please refactor them to break these chains."
69145
exit 1
146+
else
147+
echo "✅ No startup dependency chains exceed the threshold of $MAX_STARTUP_DEPENDENCY_CHAIN_LENGTH"
70148
fi
71149
72-
- name: Extract and validate bean instantiation metrics
150+
- name: Extract and validate deferred eager bean chain length
73151
run: |
74-
LINE=$(grep 'Bean instantiation graph exported' app.log) \
75-
|| { echo "❌ No metrics line"; cat app.log; exit 1; }
76-
if [[ "$LINE" =~ \(([0-9]+)\ edges,\ max\ call\ stack\ size:\ ([0-9]+)\) ]]; then
77-
EDGES=${BASH_REMATCH[1]}
78-
MAXSTACK=${BASH_REMATCH[2]}
152+
DEFERRED_BEAN_PATTERN="Maximum dependency chain length during deferred eager init: ([0-9]+)"
153+
DEFERRED_TIMEOUT_ATTEMPTS=10
154+
DEFERRED_RETRY_INTERVAL=3
155+
DEFERRED_TOTAL_TIMEOUT=$((DEFERRED_TIMEOUT_ATTEMPTS * DEFERRED_RETRY_INTERVAL))
156+
MIN_DEFERRED_CHAIN_LENGTH=1
157+
MAX_DEFERRED_CHAIN_LENGTH=15
158+
LOG_FILE="app.log"
159+
LOG_TAIL_LINES=50
160+
161+
# Extract search text from pattern (everything before :)
162+
DEFERRED_BEAN_SEARCH=$(echo "$DEFERRED_BEAN_PATTERN" | sed 's/:.*$//')
163+
164+
echo "Waiting for deferred eager bean initialization to complete..."
165+
166+
DEFERRED_LINE=""
167+
for i in $(seq 1 $DEFERRED_TIMEOUT_ATTEMPTS); do
168+
DEFERRED_LINE=$(grep -E "$DEFERRED_BEAN_SEARCH" $LOG_FILE 2>/dev/null || true)
169+
if [ -n "$DEFERRED_LINE" ]; then
170+
echo "✅ Found deferred eager bean initialization log after $i attempts"
171+
break
172+
fi
173+
echo " attempt $i/$DEFERRED_TIMEOUT_ATTEMPTS: deferred eager bean initialization not completed yet"
174+
sleep $DEFERRED_RETRY_INTERVAL
175+
done
176+
177+
if [ -z "$DEFERRED_LINE" ]; then
178+
echo "❌ No deferred eager bean chain length line found after ${DEFERRED_TOTAL_TIMEOUT}s"
179+
echo "Last $LOG_TAIL_LINES lines of $LOG_FILE:"
180+
tail -$LOG_TAIL_LINES $LOG_FILE
181+
exit 1
182+
fi
183+
184+
if [[ "$DEFERRED_LINE" =~ $DEFERRED_BEAN_PATTERN ]]; then
185+
DEFERRED_CHAIN_LENGTH=${BASH_REMATCH[1]}
79186
else
80-
echo "❌ Failed to parse numbers"; exit 1
187+
echo "❌ Failed to parse deferred eager bean chain length from: $DEFERRED_LINE"
188+
exit 1
81189
fi
82-
echo "• Number of edges = $EDGES"
83-
echo "• Max call stack size = $MAXSTACK"
84-
MIN_EDGES=20
85-
MAX_EDGES=94
86-
MAX_STACK_SIZE_THRESHOLD=9
87-
echo "Validating against thresholds: MIN_EDGES=$MIN_EDGES, MAX_EDGES=$MAX_EDGES, MAX_STACK_SIZE_THRESHOLD=$MAX_STACK_SIZE_THRESHOLD"
88-
if (( EDGES < MIN_EDGES )); then
89-
echo "❌ < $MIN_EDGES edges. Something seems to be wrong, as usually more beans are instantiated."
90-
exit 1
190+
191+
echo "• Maximum dependency chain length during deferred eager bean initialization = $DEFERRED_CHAIN_LENGTH"
192+
echo "Validating against thresholds: MIN_DEFERRED_CHAIN_LENGTH=$MIN_DEFERRED_CHAIN_LENGTH (expected ≥ $MIN_DEFERRED_CHAIN_LENGTH), MAX_DEFERRED_CHAIN_LENGTH=$MAX_DEFERRED_CHAIN_LENGTH (expected ≤ $MAX_DEFERRED_CHAIN_LENGTH)"
193+
194+
if (( DEFERRED_CHAIN_LENGTH < MIN_DEFERRED_CHAIN_LENGTH )); then
195+
echo "❌ Deferred chain length $DEFERRED_CHAIN_LENGTH < $MIN_DEFERRED_CHAIN_LENGTH. Something seems to be wrong, as usually some deferred beans are instantiated."
196+
exit 1
91197
fi
92198
93-
if (( EDGES > MAX_EDGES )); then
94-
echo "❌ > $MAX_EDGES edges"
95-
exit 1
199+
if (( DEFERRED_CHAIN_LENGTH > MAX_DEFERRED_CHAIN_LENGTH )); then
200+
echo "❌ Deferred chain length $DEFERRED_CHAIN_LENGTH > $MAX_DEFERRED_CHAIN_LENGTH threshold"
201+
exit 1
96202
fi
97203
98-
if (( MAXSTACK > MAX_STACK_SIZE_THRESHOLD )); then
99-
echo "❌ stack > $MAX_STACK_SIZE_THRESHOLD"
100-
exit 1
204+
echo "✅ Deferred eager bean chain length: $DEFERRED_CHAIN_LENGTH"
205+
echo "🎉 Deferred eager bean dependency chain length within expected ranges"
206+
207+
- name: Check for deferred dependency chains exceeding threshold
208+
run: |
209+
# Configuration variables
210+
DEFERRED_DEEP_CHAIN_PATTERN="Deferred long bean instantiation chain"
211+
MAX_DEFERRED_DEPENDENCY_CHAIN_LENGTH=15
212+
LOG_FILE="app.log"
213+
214+
echo "Checking for deferred dependency chains exceeding threshold..."
215+
216+
if (( DEFERRED_CHAIN_LENGTH > MAX_DEFERRED_DEPENDENCY_CHAIN_LENGTH )); then
217+
echo "❌ Deferred dependency chain length $DEFERRED_CHAIN_LENGTH > $MAX_DEFERRED_DEPENDENCY_CHAIN_LENGTH threshold"
218+
echo "Chains exceeding threshold:"
219+
grep -E "$DEFERRED_DEEP_CHAIN_PATTERN" $LOG_FILE || echo " (No detailed chains found in log)"
220+
echo "🔧 These chains violate the threshold MAX_DEFERRED_DEPENDENCY_CHAIN_LENGTH. Please refactor them to break these chains."
221+
exit 1
222+
else
223+
echo "✅ No deferred dependency chains exceed the threshold of $MAX_DEFERRED_DEPENDENCY_CHAIN_LENGTH"
101224
fi
102-
echo "✅ Final values → Edges: $EDGES; Max call stack size: $MAXSTACK"
103-
echo "🎉 Number of instantiated beans on startup within expected ranges"
104225
105226
- name: Stop application
106227
if: always()
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package de.tum.cit.aet.artemis.core;
2+
3+
public record DeferredEagerBeanInitializationCompletedEvent() {
4+
}

src/main/java/de/tum/cit/aet/artemis/core/config/BeanInstantiationTracer.java

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@
55
import java.io.IOException;
66
import java.io.PrintWriter;
77
import java.util.ArrayDeque;
8+
import java.util.ArrayList;
9+
import java.util.Collections;
810
import java.util.Deque;
11+
import java.util.List;
912
import java.util.Queue;
1013
import java.util.concurrent.ConcurrentLinkedQueue;
1114
import java.util.concurrent.atomic.AtomicInteger;
15+
import java.util.concurrent.atomic.AtomicReference;
1216

1317
import org.slf4j.Logger;
1418
import org.slf4j.LoggerFactory;
@@ -18,6 +22,7 @@
1822
import org.springframework.context.event.EventListener;
1923
import org.springframework.stereotype.Component;
2024

25+
import de.tum.cit.aet.artemis.core.DeferredEagerBeanInitializationCompletedEvent;
2126
import de.tum.cit.aet.artemis.core.PrintStartupBeansEvent;
2227
import de.tum.cit.aet.artemis.core.util.Pair;
2328

@@ -36,6 +41,16 @@ public class BeanInstantiationTracer implements InstantiationAwareBeanPostProces
3641

3742
private final AtomicInteger maxCallStackSize = new AtomicInteger(0);
3843

44+
private final AtomicReference<List<String>> longestChain = new AtomicReference<>(new ArrayList<>());
45+
46+
private static final int STARTUP_MAX_DEPENDENCY_CHAIN_THRESHOLD = 9;
47+
48+
private static final int DEFERRED_INIT_MAX_DEPENDENCY_CHAIN_THRESHOLD = 15;
49+
50+
private final Queue<List<String>> exceedingThresholdChains = new ConcurrentLinkedQueue<>();
51+
52+
private final Queue<List<String>> deferredInstantiationExceedingThresholdChains = new ConcurrentLinkedQueue<>();
53+
3954
@Override
4055
public Object postProcessBeforeInstantiation(Class<?> cls, String name) {
4156
if (cls.getName().startsWith(BASE)) {
@@ -44,10 +59,21 @@ public Object postProcessBeforeInstantiation(Class<?> cls, String name) {
4459
if (parent != null) {
4560
edges.add(new Pair<>(parent, name));
4661
}
47-
log.debug("Instantiating: {} ← {}", name, parent != null ? parent : "root");
4862
stack.push(name);
63+
4964
int depth = stack.size();
50-
maxCallStackSize.updateAndGet(prev -> Math.max(prev, depth));
65+
66+
if (depth > STARTUP_MAX_DEPENDENCY_CHAIN_THRESHOLD) {
67+
exceedingThresholdChains.add(new ArrayList<>(stack));
68+
}
69+
if (depth > DEFERRED_INIT_MAX_DEPENDENCY_CHAIN_THRESHOLD) {
70+
deferredInstantiationExceedingThresholdChains.add(new ArrayList<>(stack));
71+
}
72+
73+
int prevMax = maxCallStackSize.get();
74+
if (depth > prevMax && maxCallStackSize.compareAndSet(prevMax, depth)) {
75+
longestChain.set(new ArrayList<>(stack));
76+
}
5177
}
5278
return null;
5379
}
@@ -63,10 +89,6 @@ public Object postProcessAfterInitialization(Object bean, String name) {
6389
return bean;
6490
}
6591

66-
/**
67-
* Prints the bean instantiation graph for the startup to a DOT file.
68-
* This file can be used to visualize the dependencies between beans using tools like Graphviz.
69-
*/
7092
@EventListener(PrintStartupBeansEvent.class)
7193
public void printDependencyGraph() {
7294
try (PrintWriter out = new PrintWriter("startupBeans.dot")) {
@@ -75,10 +97,37 @@ public void printDependencyGraph() {
7597
out.printf(" \"%s\" -> \"%s\";%n", edge.first(), edge.second());
7698
}
7799
out.println("}");
78-
log.debug("Bean instantiation graph exported to startupBeans.dot ({} edges, max call stack size: {})", edges.size(), maxCallStackSize.get());
100+
log.debug("Bean instantiation graph exported to startupBeans.dot ({} edges, longest dependency chain: {})", edges.size(), longestChain.get());
79101
}
80102
catch (IOException e) {
81103
log.error("Failed to write startupBeans.dot", e);
82104
}
105+
106+
int i = 1;
107+
for (List<String> chain : exceedingThresholdChains) {
108+
List<String> reversed = new ArrayList<>(chain);
109+
Collections.reverse(reversed);
110+
log.debug("Startup long bean instantiation chain {} (length {}): {}", i++, reversed.size(), String.join(" → ", reversed));
111+
}
112+
113+
// Log the single longest chain
114+
List<String> longest = longestChain.get();
115+
if (!longest.isEmpty()) {
116+
List<String> forward = new ArrayList<>(longest);
117+
Collections.reverse(forward);
118+
log.debug("Longest instantiation chain: {}", String.join(" → ", forward));
119+
}
120+
}
121+
122+
@EventListener(DeferredEagerBeanInitializationCompletedEvent.class)
123+
public void logDeferredInitChainsExceedingThreshold() {
124+
int i = 1;
125+
for (List<String> chain : deferredInstantiationExceedingThresholdChains) {
126+
List<String> reversed = new ArrayList<>(chain);
127+
Collections.reverse(reversed);
128+
log.debug("Deferred long bean instantiation chain {} (length {}): {}", i++, reversed.size(), String.join(" → ", reversed));
129+
}
130+
131+
log.debug("Maximum dependency chain length during deferred eager init: {}", maxCallStackSize.get());
83132
}
84133
}

src/main/java/de/tum/cit/aet/artemis/core/config/DeferredEagerBeanInitializer.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
import org.springframework.context.annotation.Profile;
1515
import org.springframework.stereotype.Component;
1616

17+
import de.tum.cit.aet.artemis.core.DeferredEagerBeanInitializationCompletedEvent;
18+
1719
/**
1820
* This component initializes all lazy singleton beans after the application is ready.
1921
* This allows us to benefit from the lazy initialization of beans during startup, without comprising end user experience as beans are initialized before the first request is
@@ -62,7 +64,7 @@ public void initializeDeferredEagerBeans() {
6264
log.warn("Deferred eager initialization of bean {} failed", name, ex);
6365
}
6466
});
65-
67+
context.publishEvent(new DeferredEagerBeanInitializationCompletedEvent());
6668
log.info("Deferred eager initialization of all beans completed");
6769
}
6870
}

src/main/java/de/tum/cit/aet/artemis/core/config/migration/MigrationRegistry.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import java.util.SortedMap;
88
import java.util.TreeMap;
99

10+
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
1011
import org.springframework.boot.context.event.ApplicationReadyEvent;
1112
import org.springframework.context.annotation.Lazy;
1213
import org.springframework.context.annotation.Profile;
@@ -19,6 +20,7 @@
1920
@Component
2021
@Lazy
2122
@Profile(PROFILE_CORE_AND_SCHEDULING)
23+
@ConditionalOnBooleanProperty(prefix = "spring.liquibase", name = "enabled", matchIfMissing = true)
2224
public class MigrationRegistry {
2325

2426
// Using SortedMap to allow sorting. I'm using a map because with a list entries could accidentally be switched.

0 commit comments

Comments
 (0)